Merge branch 'oauth2'
This commit is contained in:
commit
95aab372a5
1
.gitignore
vendored
1
.gitignore
vendored
@ -5,3 +5,4 @@ calibre.db
|
||||
bouquins.json
|
||||
Gopkg.lock
|
||||
vendor/
|
||||
users.db
|
||||
|
37
README.md
37
README.md
@ -6,12 +6,14 @@ Bouquins in Go
|
||||
|
||||
* translations
|
||||
* tests
|
||||
* auth downloads
|
||||
* csrf
|
||||
* userdb commands (init, migrate, add/remove user/email)
|
||||
* error pages
|
||||
|
||||
## Minify JS
|
||||
## Minify
|
||||
|
||||
https://www.danstools.com/javascript-minify/
|
||||
* JS: https://www.danstools.com/javascript-minify/
|
||||
* CSS: curl -X POST -s --data-urlencode 'input@assets/css/bouquins.css' https://cssminifier.com/raw > assets/css/bouquins.min.css
|
||||
|
||||
## Deployment archive
|
||||
|
||||
@ -26,12 +28,39 @@ Example:
|
||||
{
|
||||
"calibre-path": "/usr/home/meutel/data/calibre",
|
||||
"bind-address": ":8080",
|
||||
"prod": true
|
||||
"prod": true,
|
||||
"cookie-secret": "random",
|
||||
"external-url":"https://bouquins.meutel.net",
|
||||
"providers": [
|
||||
{
|
||||
"name": "github",
|
||||
"client-id": "ID client",
|
||||
"client-secret": "SECRET"
|
||||
},
|
||||
{
|
||||
"name": "google",
|
||||
"client-id":"ID client",
|
||||
"client-secret":"SECRET"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Options:
|
||||
|
||||
* calibre-path path to calibre data
|
||||
* db-path path to calibre SQLite database (default <calibre-path>/metadata.db)
|
||||
* user-db-path path to users SQLite database (default ./users.db)
|
||||
* bind-address HTTP socket bind address
|
||||
* prod (boolean) use minified javascript/CSS
|
||||
* cookie-secret random string for cookie encryption
|
||||
* external-url URL used by client browsers
|
||||
* providers configuration for OAuth 2 providers
|
||||
* name provider name
|
||||
* client-id OAuth client ID
|
||||
* client-secret OAuth secret
|
||||
|
||||
## Users SQL
|
||||
|
||||
CREATE TABLE accounts (id varchar(36) PRIMARY KEY NOT NULL, name varchar(255) NOT NULL);
|
||||
CREATE TABLE authentifiers (id varchar(36) NOT NULL, authentifier varchar(320) PRIMARY KEY NOT NULL, FOREIGN KEY(id) REFERENCES account(id));
|
||||
|
||||
|
14
assets/css/bouquins.css
Normal file
14
assets/css/bouquins.css
Normal file
@ -0,0 +1,14 @@
|
||||
span.providericon {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
background-size: 16px;
|
||||
background-repeat: no-repeat;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
.githubicon {
|
||||
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyRpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoTWFjaW50b3NoKSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDpFNTE3OEEyQTk5QTAxMUUyOUExNUJDMTA0NkE4OTA0RCIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDpFNTE3OEEyQjk5QTAxMUUyOUExNUJDMTA0NkE4OTA0RCI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOkU1MTc4QTI4OTlBMDExRTI5QTE1QkMxMDQ2QTg5MDREIiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOkU1MTc4QTI5OTlBMDExRTI5QTE1QkMxMDQ2QTg5MDREIi8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+m4QGuQAAAyRJREFUeNrEl21ojWEYx895TDPbMNlBK46IUiNmPvHBSUjaqc0H8pF5+aDUKPEBqU2NhRQpX5Rv5jWlDIWlMCv7MMSWsWwmb3tpXub4XXWdPHvc9/Gc41nu+nedc7/8r/99PffLdYdDPsvkwsgkTBwsA/PADJCnzX2gHTwBt8Hl7p537/3whn04XoDZDcpBlk+9P8AFcAghzRkJwPF4zGGw0Y9QS0mAM2AnQj77FqCzrtcwB1Hk81SYojHK4DyGuQ6mhIIrBWB9Xm7ug/6B/nZrBHBegrkFxoVGpnwBMSLR9EcEcC4qb8pP14BWcBcUgewMnF3T34VqhWMFkThLJAalwnENOAKiHpJq1FZgI2AT6HZtuxZwR9GidSHtI30jOrbawxlVX78/AbNfhHlomEUJJI89O2MqeE79T8/nk8nMBm/dK576hZgmA3cp/R4l9/UeSxiHLVIlNm4nFfT0bxyuIj7LHRTKai+zdJobwMKzcZSJb0ePV5PKN+BqAAKE47UlMnERELMM3EdYP/yrd+XYb2mOiYBiQ8OQnoRBlXrl9JZix7D1pHTazu4MoyBcnYamqAjIMTR8G4FT8LuhLsexXYYjICBiqhQBvYb6fLZIJCjPypVvaOoVAW2WcasCnL2Nq82xHJNSqlCeFcDshaPK0twkAhosjZL31QYw+1rlMpWGMArl23SBsZZO58F2tlJXmjOXS+s4WGvpMiBJT/I2PInZ6lIs9/hBsNS1hS6BG0DSqmYEDRlCXQrmy50P1oDRKTSegmNbUsA0zDMwRhPJXeCE3vWLPQMvan6X8AgIa1vcR4AkGZkDR4ejJ1UHpsaVI0g2LInpOsNFUud1rhxSV+fzC9Woz2EZkWQuja7/B+jUrgtIMpy9YCW4n4K41YfzRneW5E1KJTe4B2Zq1Q5EHEtj4U3AfEzR5SVY4l7QYQPJdN2as7RKBF0BPZqqH4VgMAMBL8Byxr7y8zCZiDlnOcEKIPmUpgB5Z2ww5RdOiiRiNajUmWda5IG6WbhsyY2fx6m8gLcoJDJFkH219M3We1+cnda93pfycZpIJEL/s/wSYADmOAwAQgdpBAAAAABJRU5ErkJggg==);
|
||||
}
|
||||
.googleicon {
|
||||
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAB1FBMVEUAAAD/AADsQzXrQzbqQzXpRDTqQzXqQjXqQzXqRDToRjbqQzXqQzXqQzXqRDXrRTHxRznqQzXpQzXqQzXpQzTjOTnrQTTqQzXqQzToRDPpQjfqQzXqQzXpQzbsRDjqQzXoRDfqQzXqQzXrQzXqRDXqQjXqQzbqQzXqQzXqQzTsQjn/rxDqRDTqQzXqRDXoRjr8vAX5sQrsTDHrQjT//wD7vAT7ugbvZif6vgX1jRjqQzX7vAT5sAo/jss9kb77uwRAieH6vQX6vQX6vAX6vQVBiOnkuA4/i9r6vAWstCQ1qVI3pFI9k7E+j8n4vAZvrT00qFM0p1M/jso+lLn8vAXkug1CqU00qFQ8lK45l6ffvxg3qFI0qFM9lqpBieU/jc00qFQzqFM1p09An2A0plk/jdA5lqwtpVo0qFM0qFM1qVQ1qFM1p1I0p1IzqVI1qFM1qFM+j8gzplM0qFNAi91Cl6o1qVM0qFM/jNQ7m602qFE0qFM1qFMxpVI0qVM0qFM0qFM0qFQktkk4p1A0qFM0qVM0qVMzo1IA/wA2p1M0qFIzqFIzp1QzqFI0p1PqQzX7vAVChfRChu9BhvA0qFNChfJBhfM1p1o9krs5mpQ3oHf////8WgVEAAAAj3RSTlMAATVylKafh24xIZXl5o8aEpD5940JJ9nWLUby8Tkp8Dje+rt8YF/7/pwbIOX2VhaV58xZAeP7xTfK63Kv47x8+6VgpGH+sfU1y+wcfd/7xv6d36SU6NJYYjEg5fdL/OePnx0IcPxQEdz7vX5gXXOl7tko8PgbRPH6OCbYzB+O+PaJByCT4+YZATRwpIZtMQ4TRwgAAAABYktHRJvv2FeEAAAAB3RJTUUH4QgKAjghFnOx6QAAAWBJREFUOMtjYCABMDIxs7CysXNwMmKV5uLm6YcCXj5+DGkBQaF+JCAsIooqLybejwYkJJHlpaTR5ftlZJHk5eQx5RWQ7VeEiiopq6iqqSiro+lnEIRIa2hqQf3DiqKfQVsHLK+rhxDSR/GBgaERSL8xrvAzMZ1gZq7Rr4kzgC0mAIGllRZOBdYgBRNsYFxbZGAHdgJYgT1MwURk4AAScQQrcMKqYBJIxBmswAWrgsmErHBFONINqwJ3kIgHSN7TyxvVbz5gBb7QgPLzD5gSiKogCKwgGMwOCQ2bMmVKQDiyfMRUkPy0SDAnKnoKCMQgqYiNAxsQD+UmgBVMCUhMgvCTU1LB8lPToArSMyAqpmRmZefk5uUXTJk+A6SgEG5iUfEUdDBz8sSSUoSdZeUYKmZVVCK7uqoaXUFNLaq/0+vqkaUbGpsw0kVzSytMui2hHWvS6ejsaulO7Ont6yAlywMAh+DsfszQdOIAAAAldEVYdGRhdGU6Y3JlYXRlADIwMTctMDgtMTBUMDI6NTY6MzMrMDA6MDAy1cN5AAAAJXRFWHRkYXRlOm1vZGlmeQAyMDE3LTA4LTEwVDAyOjU2OjMzKzAwOjAwQ4h7xQAAAABJRU5ErkJggg==);
|
||||
}
|
1
assets/css/bouquins.min.css
vendored
Normal file
1
assets/css/bouquins.min.css
vendored
Normal file
@ -0,0 +1 @@
|
||||
span.providericon{display:inline-block;vertical-align:middle;background-size:16px;background-repeat:no-repeat;width:16px;height:16px}.githubicon{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyRpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoTWFjaW50b3NoKSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDpFNTE3OEEyQTk5QTAxMUUyOUExNUJDMTA0NkE4OTA0RCIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDpFNTE3OEEyQjk5QTAxMUUyOUExNUJDMTA0NkE4OTA0RCI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOkU1MTc4QTI4OTlBMDExRTI5QTE1QkMxMDQ2QTg5MDREIiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOkU1MTc4QTI5OTlBMDExRTI5QTE1QkMxMDQ2QTg5MDREIi8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+m4QGuQAAAyRJREFUeNrEl21ojWEYx895TDPbMNlBK46IUiNmPvHBSUjaqc0H8pF5+aDUKPEBqU2NhRQpX5Rv5jWlDIWlMCv7MMSWsWwmb3tpXub4XXWdPHvc9/Gc41nu+nedc7/8r/99PffLdYdDPsvkwsgkTBwsA/PADJCnzX2gHTwBt8Hl7p537/3whn04XoDZDcpBlk+9P8AFcAghzRkJwPF4zGGw0Y9QS0mAM2AnQj77FqCzrtcwB1Hk81SYojHK4DyGuQ6mhIIrBWB9Xm7ug/6B/nZrBHBegrkFxoVGpnwBMSLR9EcEcC4qb8pP14BWcBcUgewMnF3T34VqhWMFkThLJAalwnENOAKiHpJq1FZgI2AT6HZtuxZwR9GidSHtI30jOrbawxlVX78/AbNfhHlomEUJJI89O2MqeE79T8/nk8nMBm/dK576hZgmA3cp/R4l9/UeSxiHLVIlNm4nFfT0bxyuIj7LHRTKai+zdJobwMKzcZSJb0ePV5PKN+BqAAKE47UlMnERELMM3EdYP/yrd+XYb2mOiYBiQ8OQnoRBlXrl9JZix7D1pHTazu4MoyBcnYamqAjIMTR8G4FT8LuhLsexXYYjICBiqhQBvYb6fLZIJCjPypVvaOoVAW2WcasCnL2Nq82xHJNSqlCeFcDshaPK0twkAhosjZL31QYw+1rlMpWGMArl23SBsZZO58F2tlJXmjOXS+s4WGvpMiBJT/I2PInZ6lIs9/hBsNS1hS6BG0DSqmYEDRlCXQrmy50P1oDRKTSegmNbUsA0zDMwRhPJXeCE3vWLPQMvan6X8AgIa1vcR4AkGZkDR4ejJ1UHpsaVI0g2LInpOsNFUud1rhxSV+fzC9Woz2EZkWQuja7/B+jUrgtIMpy9YCW4n4K41YfzRneW5E1KJTe4B2Zq1Q5EHEtj4U3AfEzR5SVY4l7QYQPJdN2as7RKBF0BPZqqH4VgMAMBL8Byxr7y8zCZiDlnOcEKIPmUpgB5Z2ww5RdOiiRiNajUmWda5IG6WbhsyY2fx6m8gLcoJDJFkH219M3We1+cnda93pfycZpIJEL/s/wSYADmOAwAQgdpBAAAAABJRU5ErkJggg==)}.googleicon{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAB1FBMVEUAAAD/AADsQzXrQzbqQzXpRDTqQzXqQjXqQzXqRDToRjbqQzXqQzXqQzXqRDXrRTHxRznqQzXpQzXqQzXpQzTjOTnrQTTqQzXqQzToRDPpQjfqQzXqQzXpQzbsRDjqQzXoRDfqQzXqQzXrQzXqRDXqQjXqQzbqQzXqQzXqQzTsQjn/rxDqRDTqQzXqRDXoRjr8vAX5sQrsTDHrQjT//wD7vAT7ugbvZif6vgX1jRjqQzX7vAT5sAo/jss9kb77uwRAieH6vQX6vQX6vAX6vQVBiOnkuA4/i9r6vAWstCQ1qVI3pFI9k7E+j8n4vAZvrT00qFM0p1M/jso+lLn8vAXkug1CqU00qFQ8lK45l6ffvxg3qFI0qFM9lqpBieU/jc00qFQzqFM1p09An2A0plk/jdA5lqwtpVo0qFM0qFM1qVQ1qFM1p1I0p1IzqVI1qFM1qFM+j8gzplM0qFNAi91Cl6o1qVM0qFM/jNQ7m602qFE0qFM1qFMxpVI0qVM0qFM0qFM0qFQktkk4p1A0qFM0qVM0qVMzo1IA/wA2p1M0qFIzqFIzp1QzqFI0p1PqQzX7vAVChfRChu9BhvA0qFNChfJBhfM1p1o9krs5mpQ3oHf////8WgVEAAAAj3RSTlMAATVylKafh24xIZXl5o8aEpD5940JJ9nWLUby8Tkp8Dje+rt8YF/7/pwbIOX2VhaV58xZAeP7xTfK63Kv47x8+6VgpGH+sfU1y+wcfd/7xv6d36SU6NJYYjEg5fdL/OePnx0IcPxQEdz7vX5gXXOl7tko8PgbRPH6OCbYzB+O+PaJByCT4+YZATRwpIZtMQ4TRwgAAAABYktHRJvv2FeEAAAAB3RJTUUH4QgKAjghFnOx6QAAAWBJREFUOMtjYCABMDIxs7CysXNwMmKV5uLm6YcCXj5+DGkBQaF+JCAsIooqLybejwYkJJHlpaTR5ftlZJHk5eQx5RWQ7VeEiiopq6iqqSiro+lnEIRIa2hqQf3DiqKfQVsHLK+rhxDSR/GBgaERSL8xrvAzMZ1gZq7Rr4kzgC0mAIGllRZOBdYgBRNsYFxbZGAHdgJYgT1MwURk4AAScQQrcMKqYBJIxBmswAWrgsmErHBFONINqwJ3kIgHSN7TyxvVbz5gBb7QgPLzD5gSiKogCKwgGMwOCQ2bMmVKQDiyfMRUkPy0SDAnKnoKCMQgqYiNAxsQD+UmgBVMCUhMgvCTU1LB8lPToArSMyAqpmRmZefk5uUXTJk+A6SgEG5iUfEUdDBz8sSSUoSdZeUYKmZVVCK7uqoaXUFNLaq/0+vqkaUbGpsw0kVzSytMui2hHWvS6ejsaulO7Ont6yAlywMAh+DsfszQdOIAAAAldEVYdGRhdGU6Y3JlYXRlADIwMTctMDgtMTBUMDI6NTY6MzMrMDA6MDAy1cN5AAAAJXRFWHRkYXRlOm1vZGlmeQAyMDE3LTA4LTEwVDAyOjU2OjMzKzAwOjAwQ4h7xQAAAABJRU5ErkJggg==)}
|
138
bouquins/auth.go
Normal file
138
bouquins/auth.go
Normal file
@ -0,0 +1,138 @@
|
||||
package bouquins
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/sessions"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
const (
|
||||
alphanums = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
|
||||
sessionName = "bouquins"
|
||||
sessionOAuthState = "oauthState"
|
||||
sessionOAuthProvider = "provider"
|
||||
sessionUser = "username"
|
||||
|
||||
pProvider = "provider"
|
||||
)
|
||||
|
||||
var (
|
||||
// Providers contains OAuth2 providers implementations
|
||||
Providers []OAuth2Provider
|
||||
)
|
||||
|
||||
// LoginModel is login page model
|
||||
type LoginModel struct {
|
||||
Model
|
||||
Providers []OAuth2Provider
|
||||
}
|
||||
|
||||
// NewLoginModel constructor for LoginModel
|
||||
func (app *Bouquins) NewLoginModel(req *http.Request) *LoginModel {
|
||||
return &LoginModel{*app.NewModel("Authentification", "provider", req), Providers}
|
||||
}
|
||||
|
||||
// OAuth2Provider allows to get a user from an OAuth2 token
|
||||
type OAuth2Provider interface {
|
||||
GetUser(token *oauth2.Token) (string, error)
|
||||
Config(conf *Conf) *oauth2.Config
|
||||
Name() string
|
||||
Label() string
|
||||
Icon() string
|
||||
}
|
||||
|
||||
// generates a 16 characters long random string
|
||||
func securedRandString() string {
|
||||
b := make([]byte, 16)
|
||||
for i := range b {
|
||||
b[i] = alphanums[rand.Intn(len(alphanums))]
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
// Session returns current session
|
||||
func (app *Bouquins) Session(req *http.Request) *sessions.Session {
|
||||
session, _ := app.Cookies.Get(req, sessionName)
|
||||
return session
|
||||
}
|
||||
|
||||
// Username returns logged in username
|
||||
func (app *Bouquins) Username(req *http.Request) string {
|
||||
username := app.Session(req).Values[sessionUser]
|
||||
if username != nil {
|
||||
return username.(string)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// SessionSet sets a value in session
|
||||
func (app *Bouquins) SessionSet(name string, value string, res http.ResponseWriter, req *http.Request) {
|
||||
session := app.Session(req)
|
||||
session.Values[name] = value
|
||||
session.Save(req, res)
|
||||
}
|
||||
|
||||
// LoginPage redirects to OAuth login page (github)
|
||||
func (app *Bouquins) LoginPage(res http.ResponseWriter, req *http.Request) error {
|
||||
provider := req.URL.Query().Get(pProvider)
|
||||
oauth := app.OAuthConf[provider]
|
||||
if oauth != nil {
|
||||
app.SessionSet(sessionOAuthProvider, provider, res, req)
|
||||
state := securedRandString()
|
||||
app.SessionSet(sessionOAuthState, state, res, req)
|
||||
url := oauth.AuthCodeURL(state)
|
||||
log.Println("OAuth redirect", url)
|
||||
http.Redirect(res, req, url, http.StatusTemporaryRedirect)
|
||||
return nil
|
||||
}
|
||||
// choose provider
|
||||
return app.render(res, tplProvider, app.NewLoginModel(req))
|
||||
}
|
||||
|
||||
// LogoutPage logout connected user
|
||||
func (app *Bouquins) LogoutPage(res http.ResponseWriter, req *http.Request) error {
|
||||
app.SessionSet(sessionUser, "", res, req)
|
||||
return RedirectHome(res, req)
|
||||
}
|
||||
|
||||
// CallbackPage handle OAuth 2 callback
|
||||
func (app *Bouquins) CallbackPage(res http.ResponseWriter, req *http.Request) error {
|
||||
savedState := app.Session(req).Values[sessionOAuthState]
|
||||
providerParam := app.Session(req).Values[sessionOAuthProvider]
|
||||
if savedState == "" || providerParam == "" {
|
||||
return fmt.Errorf("missing oauth data")
|
||||
}
|
||||
providerName := providerParam.(string)
|
||||
oauth := app.OAuthConf[providerName]
|
||||
provider := findProvider(providerName)
|
||||
if oauth == nil || provider == nil {
|
||||
return fmt.Errorf("missing oauth configuration")
|
||||
}
|
||||
app.SessionSet(sessionOAuthState, "", res, req)
|
||||
app.SessionSet(sessionOAuthProvider, "", res, req)
|
||||
state := req.FormValue("state")
|
||||
if state != savedState {
|
||||
return fmt.Errorf("invalid oauth state, expected '%s', got '%s'", "state", state)
|
||||
}
|
||||
code := req.FormValue("code")
|
||||
token, err := oauth.Exchange(oauth2.NoContext, code)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Code exchange failed with '%s'", err)
|
||||
}
|
||||
userEmail, err := provider.GetUser(token)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
user, err := Account(userEmail)
|
||||
if err != nil {
|
||||
log.Println("Error loading user", err)
|
||||
return fmt.Errorf("Unknown user")
|
||||
}
|
||||
app.SessionSet(sessionUser, user.DisplayName, res, req)
|
||||
log.Println("User logged in", user.DisplayName)
|
||||
return RedirectHome(res, req)
|
||||
}
|
@ -12,18 +12,23 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
|
||||
"github.com/c2h5oh/datasize"
|
||||
"github.com/gorilla/sessions"
|
||||
)
|
||||
|
||||
const (
|
||||
// Version defines application version
|
||||
Version = "master"
|
||||
|
||||
tplBooks = "book.html"
|
||||
tplAuthors = "author.html"
|
||||
tplSeries = "series.html"
|
||||
tplIndex = "index.html"
|
||||
tplSearch = "search.html"
|
||||
tplAbout = "about.html"
|
||||
tplBooks = "book.html"
|
||||
tplAuthors = "author.html"
|
||||
tplSeries = "series.html"
|
||||
tplIndex = "index.html"
|
||||
tplSearch = "search.html"
|
||||
tplAbout = "about.html"
|
||||
tplProvider = "provider.html"
|
||||
|
||||
pList = "list"
|
||||
pOrder = "order"
|
||||
@ -34,6 +39,12 @@ const (
|
||||
|
||||
// URLIndex url of index page
|
||||
URLIndex = "/"
|
||||
// URLLogin url of login page (OAuth 2)
|
||||
URLLogin = "/login"
|
||||
// URLLogout url of logout page
|
||||
URLLogout = "/logout"
|
||||
// URLCallback url of OAuth callback
|
||||
URLCallback = "/callback"
|
||||
// URLBooks url of books page
|
||||
URLBooks = "/books/"
|
||||
// URLAuthors url of authors page
|
||||
@ -54,10 +65,42 @@ const (
|
||||
URLCalibre = "/calibre/"
|
||||
)
|
||||
|
||||
// UnprotectedCalibreSuffix lists suffixe of calibre file not protected by auth
|
||||
var UnprotectedCalibreSuffix = [1]string{"jpg"}
|
||||
|
||||
// Conf App configuration
|
||||
type Conf struct {
|
||||
BindAddress string `json:"bind-address"`
|
||||
DbPath string `json:"db-path"`
|
||||
CalibrePath string `json:"calibre-path"`
|
||||
Prod bool `json:"prod"`
|
||||
UserDbPath string `json:"user-db-path"`
|
||||
CookieSecret string `json:"cookie-secret"`
|
||||
ExternalURL string `json:"external-url"`
|
||||
ProvidersConf []ProviderConf `json:"providers"`
|
||||
}
|
||||
|
||||
// ProviderConf OAuth2 provider configuration
|
||||
type ProviderConf struct {
|
||||
Name string `json:"name"`
|
||||
ClientID string `json:"client-id"`
|
||||
ClientSecret string `json:"client-secret"`
|
||||
}
|
||||
|
||||
// Bouquins contains application common resources: templates, database
|
||||
type Bouquins struct {
|
||||
Tpl *template.Template
|
||||
DB *sql.DB
|
||||
*sql.DB
|
||||
UserDB *sql.DB
|
||||
*Conf
|
||||
OAuthConf map[string]*oauth2.Config
|
||||
Cookies *sessions.CookieStore
|
||||
}
|
||||
|
||||
// UserAccount is an user account
|
||||
type UserAccount struct {
|
||||
ID string // UUID
|
||||
DisplayName string
|
||||
}
|
||||
|
||||
// Series is a book series.
|
||||
@ -138,14 +181,20 @@ type SeriesFull struct {
|
||||
|
||||
// Model is basic page model
|
||||
type Model struct {
|
||||
Title string
|
||||
Page string
|
||||
Version string
|
||||
Title string
|
||||
Page string
|
||||
Version string
|
||||
Username string
|
||||
}
|
||||
|
||||
// NewModel constuctor for Model
|
||||
func NewModel(title, page string) *Model {
|
||||
return &Model{title, page, Version}
|
||||
// NewModel constructor for Model
|
||||
func (app *Bouquins) NewModel(title, page string, req *http.Request) *Model {
|
||||
return &Model{
|
||||
Title: title,
|
||||
Page: page,
|
||||
Version: Version,
|
||||
Username: app.Username(req),
|
||||
}
|
||||
}
|
||||
|
||||
// IndexModel is the model for index page
|
||||
@ -155,13 +204,13 @@ type IndexModel struct {
|
||||
}
|
||||
|
||||
// NewIndexModel constructor IndexModel
|
||||
func NewIndexModel(title string, count int64) *IndexModel {
|
||||
return &IndexModel{*NewModel(title, "index"), count}
|
||||
func (app *Bouquins) NewIndexModel(title string, count int64, req *http.Request) *IndexModel {
|
||||
return &IndexModel{*app.NewModel(title, "index", req), count}
|
||||
}
|
||||
|
||||
// NewSearchModel constuctor for search page
|
||||
func NewSearchModel() *Model {
|
||||
return NewModel("Recherche", "search")
|
||||
func (app *Bouquins) NewSearchModel(req *http.Request) *Model {
|
||||
return app.NewModel("Recherche", "search", req)
|
||||
}
|
||||
|
||||
// ResultsModel is a generic model for list pages
|
||||
@ -255,6 +304,12 @@ func TemplatesFunc(prod bool) *template.Template {
|
||||
})
|
||||
}
|
||||
|
||||
// RedirectHome redirects to home page
|
||||
func RedirectHome(res http.ResponseWriter, req *http.Request) error {
|
||||
http.Redirect(res, req, "/", http.StatusTemporaryRedirect)
|
||||
return nil
|
||||
}
|
||||
|
||||
// output page with template
|
||||
func (app *Bouquins) render(res http.ResponseWriter, tpl string, model interface{}) error {
|
||||
return app.Tpl.ExecuteTemplate(res, tpl, model)
|
||||
@ -370,7 +425,7 @@ func (app *Bouquins) bookPage(idParam string, res http.ResponseWriter, req *http
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return app.render(res, tplBooks, &BookModel{*NewModel(book.Title, "book"), book})
|
||||
return app.render(res, tplBooks, &BookModel{*app.NewModel(book.Title, "book", req), book})
|
||||
}
|
||||
func (app *Bouquins) authorPage(idParam string, res http.ResponseWriter, req *http.Request) error {
|
||||
id, err := strconv.Atoi(idParam)
|
||||
@ -381,7 +436,7 @@ func (app *Bouquins) authorPage(idParam string, res http.ResponseWriter, req *ht
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return app.render(res, tplAuthors, &AuthorModel{*NewModel(author.Name, "author"), author})
|
||||
return app.render(res, tplAuthors, &AuthorModel{*app.NewModel(author.Name, "author", req), author})
|
||||
}
|
||||
func (app *Bouquins) seriePage(idParam string, res http.ResponseWriter, req *http.Request) error {
|
||||
id, err := strconv.Atoi(idParam)
|
||||
@ -392,7 +447,7 @@ func (app *Bouquins) seriePage(idParam string, res http.ResponseWriter, req *htt
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return app.render(res, tplSeries, &SeriesModel{*NewModel(series.Name, "series"), series})
|
||||
return app.render(res, tplSeries, &SeriesModel{*app.NewModel(series.Name, "series", req), series})
|
||||
}
|
||||
|
||||
// ROUTES //
|
||||
@ -414,12 +469,12 @@ func (app *Bouquins) SeriesPage(res http.ResponseWriter, req *http.Request) erro
|
||||
|
||||
// SearchPage displays search form and results
|
||||
func (app *Bouquins) SearchPage(res http.ResponseWriter, req *http.Request) error {
|
||||
return app.render(res, tplSearch, NewSearchModel())
|
||||
return app.render(res, tplSearch, app.NewSearchModel(req))
|
||||
}
|
||||
|
||||
// AboutPage displays about page
|
||||
func (app *Bouquins) AboutPage(res http.ResponseWriter, req *http.Request) error {
|
||||
return app.render(res, tplAbout, NewModel("A propos", "about"))
|
||||
return app.render(res, tplAbout, app.NewModel("A propos", "about", req))
|
||||
}
|
||||
|
||||
// IndexPage displays index page: list of books/authors/series
|
||||
@ -428,9 +483,27 @@ func (app *Bouquins) IndexPage(res http.ResponseWriter, req *http.Request) error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
model := NewIndexModel("", count)
|
||||
model := app.NewIndexModel("", count, req)
|
||||
if isJSON(req) {
|
||||
return writeJSON(res, model)
|
||||
}
|
||||
return app.render(res, tplIndex, model)
|
||||
}
|
||||
|
||||
func (app *Bouquins) CalibreFileServer() http.Handler {
|
||||
calibre := app.Conf.CalibrePath
|
||||
handler := http.StripPrefix(URLCalibre, http.FileServer(http.Dir(calibre)))
|
||||
return http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
|
||||
for _, suffix := range UnprotectedCalibreSuffix {
|
||||
if strings.HasSuffix(req.URL.Path, suffix) {
|
||||
handler.ServeHTTP(res, req)
|
||||
}
|
||||
}
|
||||
// check auth
|
||||
if app.Username(req) == "" {
|
||||
http.Error(res, "401 Unauthorized", http.StatusUnauthorized)
|
||||
} else {
|
||||
handler.ServeHTTP(res, req)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -104,6 +104,8 @@ const (
|
||||
AND authors.id != ? ORDER BY authors.id`
|
||||
sqlAuthor = "SELECT name FROM authors WHERE id = ?"
|
||||
|
||||
sqlAccount = "SELECT accounts.id, name FROM accounts, authentifiers WHERE authentifiers.id = accounts.id AND authentifiers.authentifier = ?"
|
||||
|
||||
defaultLimit = 10
|
||||
|
||||
qtBook QueryType = iota
|
||||
@ -162,7 +164,10 @@ var queries = map[Query]string{
|
||||
Query{qtAuthorBooks, false, false}: sqlAuthorBooks,
|
||||
Query{qtAuthorCoauthors, false, false}: sqlAuthorAuthors,
|
||||
}
|
||||
var stmts = make(map[Query]*sql.Stmt)
|
||||
var (
|
||||
stmts = make(map[Query]*sql.Stmt)
|
||||
stmtAccount *sql.Stmt
|
||||
)
|
||||
|
||||
// QueryType is a type of query, with variants for sort and order
|
||||
type QueryType uint
|
||||
@ -214,6 +219,13 @@ func (app *Bouquins) PrepareAll() error {
|
||||
}
|
||||
stmts[q] = stmt
|
||||
}
|
||||
// users.db
|
||||
var err error
|
||||
stmtAccount, err = app.UserDB.Prepare(sqlAccount)
|
||||
if err != nil {
|
||||
log.Println(err, sqlAccount)
|
||||
errcount++
|
||||
}
|
||||
if errcount > 0 {
|
||||
return fmt.Errorf("%d errors on queries, see logs", errcount)
|
||||
}
|
||||
|
11
bouquins/dbusers.go
Normal file
11
bouquins/dbusers.go
Normal file
@ -0,0 +1,11 @@
|
||||
package bouquins
|
||||
|
||||
// Account returns user account from authentifier
|
||||
func Account(authentifier string) (*UserAccount, error) {
|
||||
account := new(UserAccount)
|
||||
err := stmtAccount.QueryRow(authentifier).Scan(&account.ID, &account.DisplayName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return account, nil
|
||||
}
|
94
bouquins/github.go
Normal file
94
bouquins/github.go
Normal file
@ -0,0 +1,94 @@
|
||||
package bouquins
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
"golang.org/x/oauth2/github"
|
||||
)
|
||||
|
||||
// GithubProvider implements OAuth2 client with github.com
|
||||
type GithubProvider string
|
||||
|
||||
type githubEmail struct {
|
||||
Email string `json:"email"`
|
||||
Primary bool `json:"primary"`
|
||||
Verified bool `json:"verified"`
|
||||
Visibility string `json:"visibility"`
|
||||
}
|
||||
|
||||
func init() {
|
||||
Providers = append(Providers, GithubProvider("github"))
|
||||
}
|
||||
|
||||
// Name returns name of provider
|
||||
func (p GithubProvider) Name() string {
|
||||
return string(p)
|
||||
}
|
||||
|
||||
// Label returns label of provider
|
||||
func (p GithubProvider) Label() string {
|
||||
return "Github"
|
||||
}
|
||||
|
||||
// Icon returns icon CSS class for provider
|
||||
func (p GithubProvider) Icon() string {
|
||||
return "githubicon"
|
||||
}
|
||||
|
||||
// Config returns OAuth configuration for this provider
|
||||
func (p GithubProvider) Config(conf *Conf) *oauth2.Config {
|
||||
for _, c := range conf.ProvidersConf {
|
||||
if c.Name == p.Name() {
|
||||
return &oauth2.Config{
|
||||
ClientID: c.ClientID,
|
||||
ClientSecret: c.ClientSecret,
|
||||
Scopes: []string{"user:email"},
|
||||
Endpoint: github.Endpoint,
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetUser returns github primary email
|
||||
func (p GithubProvider) GetUser(token *oauth2.Token) (string, error) {
|
||||
apiReq, err := http.NewRequest("GET", "https://api.github.com/user/emails", nil)
|
||||
apiReq.Header.Add("Accept", "application/vnd.github.v3+json")
|
||||
apiReq.Header.Add("Authorization", "token "+token.AccessToken)
|
||||
client := &http.Client{}
|
||||
response, err := client.Do(apiReq)
|
||||
defer response.Body.Close()
|
||||
if err != nil {
|
||||
log.Println("Auth error", err)
|
||||
return "", fmt.Errorf("Authentification error")
|
||||
}
|
||||
|
||||
dec := json.NewDecoder(response.Body)
|
||||
var emails []githubEmail
|
||||
err = dec.Decode(&emails)
|
||||
if err != nil {
|
||||
log.Println("Error reading github API response", err)
|
||||
return "", fmt.Errorf("Error reading github API response")
|
||||
}
|
||||
var userEmail string
|
||||
for _, email := range emails {
|
||||
if email.Primary && email.Verified {
|
||||
userEmail = email.Email
|
||||
}
|
||||
}
|
||||
log.Println("User email:", userEmail)
|
||||
return userEmail, nil
|
||||
}
|
||||
|
||||
func findProvider(name string) OAuth2Provider {
|
||||
for _, p := range Providers {
|
||||
if p.Name() == name {
|
||||
return p
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
83
bouquins/google.go
Normal file
83
bouquins/google.go
Normal file
@ -0,0 +1,83 @@
|
||||
package bouquins
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
"golang.org/x/oauth2/google"
|
||||
)
|
||||
|
||||
// GoogleProvider implements OAuth2 client with google account
|
||||
type GoogleProvider string
|
||||
|
||||
type googleTokenInfo struct {
|
||||
IssuedTo string `json:"issued_to"`
|
||||
Audience string `json:"audience"`
|
||||
UserID string `json:"user_id"`
|
||||
Scope string `json:"scope"`
|
||||
ExpiresIn int64 `json:"expires_in"`
|
||||
Email string `json:"email"`
|
||||
VerifiedEmail bool `json:"verified_email"`
|
||||
AccessType string `json:"access_type"`
|
||||
}
|
||||
|
||||
func init() {
|
||||
Providers = append(Providers, GoogleProvider("google"))
|
||||
}
|
||||
|
||||
// Name returns name of provider
|
||||
func (p GoogleProvider) Name() string {
|
||||
return string(p)
|
||||
}
|
||||
|
||||
// Label returns label of provider
|
||||
func (p GoogleProvider) Label() string {
|
||||
return "Google"
|
||||
}
|
||||
|
||||
// Icon returns icon path for provider
|
||||
func (p GoogleProvider) Icon() string {
|
||||
return "googleicon"
|
||||
}
|
||||
|
||||
// Config returns OAuth configuration for this provider
|
||||
func (p GoogleProvider) Config(conf *Conf) *oauth2.Config {
|
||||
for _, c := range conf.ProvidersConf {
|
||||
if c.Name == p.Name() {
|
||||
return &oauth2.Config{
|
||||
ClientID: c.ClientID,
|
||||
ClientSecret: c.ClientSecret,
|
||||
Scopes: []string{"https://www.googleapis.com/auth/userinfo.email"},
|
||||
Endpoint: google.Endpoint,
|
||||
RedirectURL: conf.ExternalURL + URLCallback,
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetUser returns github primary email
|
||||
func (p GoogleProvider) GetUser(token *oauth2.Token) (string, error) {
|
||||
apiRes, err := http.Post("https://www.googleapis.com/oauth2/v2/tokeninfo?access_token="+token.AccessToken, "application/json", nil)
|
||||
defer apiRes.Body.Close()
|
||||
if err != nil {
|
||||
log.Println("Auth error", err)
|
||||
return "", fmt.Errorf("Authentification error")
|
||||
}
|
||||
dec := json.NewDecoder(apiRes.Body)
|
||||
var tokenInfo googleTokenInfo
|
||||
err = dec.Decode(&tokenInfo)
|
||||
if err != nil {
|
||||
log.Println("Error reading google API response", err)
|
||||
return "", fmt.Errorf("Error reading google API response")
|
||||
}
|
||||
var userEmail string
|
||||
if tokenInfo.VerifiedEmail {
|
||||
userEmail = tokenInfo.Email
|
||||
}
|
||||
log.Println("User email:", userEmail)
|
||||
return userEmail, nil
|
||||
}
|
57
main.go
57
main.go
@ -7,24 +7,17 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
|
||||
"github.com/gorilla/sessions"
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
|
||||
"meutel.net/meutel/go-bouquins/bouquins"
|
||||
)
|
||||
|
||||
// BouquinsConf App configuration
|
||||
type BouquinsConf struct {
|
||||
BindAddress string `json:"bind-address"`
|
||||
DbPath string `json:"db-path"`
|
||||
CalibrePath string `json:"calibre-path"`
|
||||
Prod bool `json:"prod"`
|
||||
}
|
||||
|
||||
var db *sql.DB
|
||||
|
||||
// ReadConfig loads configuration file and initialize default value
|
||||
func ReadConfig() (*BouquinsConf, error) {
|
||||
conf := new(BouquinsConf)
|
||||
func ReadConfig() (*bouquins.Conf, error) {
|
||||
conf := new(bouquins.Conf)
|
||||
confPath := "bouquins.json"
|
||||
if len(os.Args) > 1 {
|
||||
confPath = os.Args[1]
|
||||
@ -44,13 +37,16 @@ func ReadConfig() (*BouquinsConf, error) {
|
||||
if conf.DbPath == "" {
|
||||
conf.DbPath = conf.CalibrePath + "/metadata.db"
|
||||
}
|
||||
if conf.UserDbPath == "" {
|
||||
conf.UserDbPath = "./users.db"
|
||||
}
|
||||
if conf.BindAddress == "" {
|
||||
conf.BindAddress = ":9000"
|
||||
}
|
||||
return conf, err
|
||||
}
|
||||
|
||||
func initApp() *BouquinsConf {
|
||||
func initApp() *bouquins.Bouquins {
|
||||
log.SetFlags(log.LstdFlags | log.Lshortfile)
|
||||
conf, err := ReadConfig()
|
||||
if err != nil {
|
||||
@ -61,25 +57,38 @@ func initApp() *BouquinsConf {
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
db, err = sql.Open("sqlite3", conf.DbPath)
|
||||
db, err := sql.Open("sqlite3", conf.DbPath)
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
app := &bouquins.Bouquins{Tpl: tpl, DB: db}
|
||||
userdb, err := sql.Open("sqlite3", conf.UserDbPath)
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
|
||||
app := &bouquins.Bouquins{
|
||||
Tpl: tpl,
|
||||
DB: db,
|
||||
UserDB: userdb,
|
||||
Conf: conf,
|
||||
OAuthConf: make(map[string]*oauth2.Config),
|
||||
Cookies: sessions.NewCookieStore([]byte(conf.CookieSecret)),
|
||||
}
|
||||
for _, provider := range bouquins.Providers {
|
||||
app.OAuthConf[provider.Name()] = provider.Config(conf)
|
||||
}
|
||||
err = app.PrepareAll()
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
assets(conf.CalibrePath)
|
||||
router(app)
|
||||
return conf
|
||||
return app
|
||||
}
|
||||
|
||||
func assets(calibre string) {
|
||||
http.Handle(bouquins.URLJs, http.StripPrefix("/"+bouquins.Version, http.FileServer(http.Dir("assets"))))
|
||||
http.Handle(bouquins.URLCss, http.StripPrefix("/"+bouquins.Version, http.FileServer(http.Dir("assets"))))
|
||||
http.Handle(bouquins.URLFonts, http.StripPrefix("/"+bouquins.Version, http.FileServer(http.Dir("assets"))))
|
||||
http.Handle(bouquins.URLCalibre, http.StripPrefix(bouquins.URLCalibre, http.FileServer(http.Dir(calibre))))
|
||||
}
|
||||
|
||||
func handle(f func(res http.ResponseWriter, req *http.Request) error) func(res http.ResponseWriter, req *http.Request) {
|
||||
@ -97,7 +106,12 @@ func handleURL(url string, f func(res http.ResponseWriter, req *http.Request) er
|
||||
}
|
||||
|
||||
func router(app *bouquins.Bouquins) {
|
||||
assets(app.Conf.CalibrePath)
|
||||
http.Handle(bouquins.URLCalibre, app.CalibreFileServer())
|
||||
handleURL(bouquins.URLIndex, app.IndexPage)
|
||||
handleURL(bouquins.URLLogin, app.LoginPage)
|
||||
handleURL(bouquins.URLLogout, app.LogoutPage)
|
||||
handleURL(bouquins.URLCallback, app.CallbackPage)
|
||||
handleURL(bouquins.URLBooks, app.BooksPage)
|
||||
handleURL(bouquins.URLAuthors, app.AuthorsPage)
|
||||
handleURL(bouquins.URLSeries, app.SeriesPage)
|
||||
@ -106,7 +120,8 @@ func router(app *bouquins.Bouquins) {
|
||||
}
|
||||
|
||||
func main() {
|
||||
conf := initApp()
|
||||
defer db.Close()
|
||||
http.ListenAndServe(conf.BindAddress, nil)
|
||||
app := initApp()
|
||||
defer app.DB.Close()
|
||||
defer app.UserDB.Close()
|
||||
http.ListenAndServe(app.Conf.BindAddress, nil)
|
||||
}
|
||||
|
@ -5,6 +5,7 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta charset="utf-8" />
|
||||
<link rel="stylesheet" href="{{ assetUrl "bootstrap" "css" }}">
|
||||
<link rel="stylesheet" href="{{ assetUrl "bouquins" "css" }}">
|
||||
<link rel="preload" href="{{ assetUrl "vue" "js" }}" as="script">
|
||||
<link rel="prefetch" href="{{ assetUrl "vue" "js" }}">
|
||||
<link rel="preload" href="{{ assetUrl "bouquins" "js" }}" as="script">
|
||||
@ -23,5 +24,12 @@
|
||||
<input name="q" type="text" class="form-control" placeholder="Recherche">
|
||||
</div>
|
||||
</form>
|
||||
<ul class="nav navbar-nav navbar-right">
|
||||
{{ if .Username }}
|
||||
<li><a href="/logout">{{ .Username }} <span title="Déconnexion" class="glyphicon glyphicon-log-out"></span></a></li>
|
||||
{{ else }}
|
||||
<li><a href="/login">Connexion <span class="glyphicon glyphicon-log-in"></span></a></li>
|
||||
{{ end }}
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
|
12
templates/provider.html
Normal file
12
templates/provider.html
Normal file
@ -0,0 +1,12 @@
|
||||
{{ template "header.html" . }}
|
||||
<div class="container" id="provider">
|
||||
<div class="jumbotron">
|
||||
<h1>Connexion</h1>
|
||||
<p>Pour vous connecter, vous devez disposer d'un compte et vous authentifier chez un des fournisseurs ci-dessous. En cliquant sur un fournisseur, vous allez être redirigé vers une page d'authentification de ce fournisseur.</p>
|
||||
{{ range .Providers }}
|
||||
<!-- TODO icon -->
|
||||
<a class="btn btn-default btn-lg" role="button" href="/login?provider={{ .Name }}">{{ if .Icon }}<span class="providericon {{ .Icon }}"></span> {{ end }}{{ .Label }}</a>
|
||||
{{ end }}
|
||||
</ul>
|
||||
</div>
|
||||
{{ template "footer.html" . }}
|
Loading…
Reference in New Issue
Block a user