Compare commits
No commits in common. "95aab372a54c62879e92aa7fcbc9fe4da3a17153" and "73ba1f81c702276d2d090f2b4e3323c4c2fec4f5" have entirely different histories.
95aab372a5
...
73ba1f81c7
1
.gitignore
vendored
1
.gitignore
vendored
@ -5,4 +5,3 @@ calibre.db
|
|||||||
bouquins.json
|
bouquins.json
|
||||||
Gopkg.lock
|
Gopkg.lock
|
||||||
vendor/
|
vendor/
|
||||||
users.db
|
|
||||||
|
37
README.md
37
README.md
@ -6,14 +6,12 @@ Bouquins in Go
|
|||||||
|
|
||||||
* translations
|
* translations
|
||||||
* tests
|
* tests
|
||||||
|
* auth downloads
|
||||||
* csrf
|
* csrf
|
||||||
* userdb commands (init, migrate, add/remove user/email)
|
|
||||||
* error pages
|
|
||||||
|
|
||||||
## Minify
|
## Minify JS
|
||||||
|
|
||||||
* JS: https://www.danstools.com/javascript-minify/
|
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
|
## Deployment archive
|
||||||
|
|
||||||
@ -28,39 +26,12 @@ Example:
|
|||||||
{
|
{
|
||||||
"calibre-path": "/usr/home/meutel/data/calibre",
|
"calibre-path": "/usr/home/meutel/data/calibre",
|
||||||
"bind-address": ":8080",
|
"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:
|
Options:
|
||||||
|
|
||||||
* calibre-path path to calibre data
|
* calibre-path path to calibre data
|
||||||
* db-path path to calibre SQLite database (default <calibre-path>/metadata.db)
|
* 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
|
* bind-address HTTP socket bind address
|
||||||
* prod (boolean) use minified javascript/CSS
|
* 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));
|
|
||||||
|
|
||||||
|
@ -1,14 +0,0 @@
|
|||||||
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
1
assets/css/bouquins.min.css
vendored
@ -1 +0,0 @@
|
|||||||
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
138
bouquins/auth.go
@ -1,138 +0,0 @@
|
|||||||
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,23 +12,18 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"golang.org/x/oauth2"
|
|
||||||
|
|
||||||
"github.com/c2h5oh/datasize"
|
"github.com/c2h5oh/datasize"
|
||||||
"github.com/gorilla/sessions"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// Version defines application version
|
|
||||||
Version = "master"
|
Version = "master"
|
||||||
|
|
||||||
tplBooks = "book.html"
|
tplBooks = "book.html"
|
||||||
tplAuthors = "author.html"
|
tplAuthors = "author.html"
|
||||||
tplSeries = "series.html"
|
tplSeries = "series.html"
|
||||||
tplIndex = "index.html"
|
tplIndex = "index.html"
|
||||||
tplSearch = "search.html"
|
tplSearch = "search.html"
|
||||||
tplAbout = "about.html"
|
tplAbout = "about.html"
|
||||||
tplProvider = "provider.html"
|
|
||||||
|
|
||||||
pList = "list"
|
pList = "list"
|
||||||
pOrder = "order"
|
pOrder = "order"
|
||||||
@ -39,12 +34,6 @@ const (
|
|||||||
|
|
||||||
// URLIndex url of index page
|
// URLIndex url of index page
|
||||||
URLIndex = "/"
|
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 url of books page
|
||||||
URLBooks = "/books/"
|
URLBooks = "/books/"
|
||||||
// URLAuthors url of authors page
|
// URLAuthors url of authors page
|
||||||
@ -65,42 +54,10 @@ const (
|
|||||||
URLCalibre = "/calibre/"
|
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
|
// Bouquins contains application common resources: templates, database
|
||||||
type Bouquins struct {
|
type Bouquins struct {
|
||||||
Tpl *template.Template
|
Tpl *template.Template
|
||||||
*sql.DB
|
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.
|
// Series is a book series.
|
||||||
@ -181,20 +138,14 @@ type SeriesFull struct {
|
|||||||
|
|
||||||
// Model is basic page model
|
// Model is basic page model
|
||||||
type Model struct {
|
type Model struct {
|
||||||
Title string
|
Title string
|
||||||
Page string
|
Page string
|
||||||
Version string
|
Version string
|
||||||
Username string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewModel constructor for Model
|
// NewModel constuctor for Model
|
||||||
func (app *Bouquins) NewModel(title, page string, req *http.Request) *Model {
|
func NewModel(title, page string) *Model {
|
||||||
return &Model{
|
return &Model{title, page, Version}
|
||||||
Title: title,
|
|
||||||
Page: page,
|
|
||||||
Version: Version,
|
|
||||||
Username: app.Username(req),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// IndexModel is the model for index page
|
// IndexModel is the model for index page
|
||||||
@ -204,13 +155,13 @@ type IndexModel struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewIndexModel constructor IndexModel
|
// NewIndexModel constructor IndexModel
|
||||||
func (app *Bouquins) NewIndexModel(title string, count int64, req *http.Request) *IndexModel {
|
func NewIndexModel(title string, count int64) *IndexModel {
|
||||||
return &IndexModel{*app.NewModel(title, "index", req), count}
|
return &IndexModel{*NewModel(title, "index"), count}
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewSearchModel constuctor for search page
|
// NewSearchModel constuctor for search page
|
||||||
func (app *Bouquins) NewSearchModel(req *http.Request) *Model {
|
func NewSearchModel() *Model {
|
||||||
return app.NewModel("Recherche", "search", req)
|
return NewModel("Recherche", "search")
|
||||||
}
|
}
|
||||||
|
|
||||||
// ResultsModel is a generic model for list pages
|
// ResultsModel is a generic model for list pages
|
||||||
@ -304,12 +255,6 @@ 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
|
// output page with template
|
||||||
func (app *Bouquins) render(res http.ResponseWriter, tpl string, model interface{}) error {
|
func (app *Bouquins) render(res http.ResponseWriter, tpl string, model interface{}) error {
|
||||||
return app.Tpl.ExecuteTemplate(res, tpl, model)
|
return app.Tpl.ExecuteTemplate(res, tpl, model)
|
||||||
@ -425,7 +370,7 @@ func (app *Bouquins) bookPage(idParam string, res http.ResponseWriter, req *http
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return app.render(res, tplBooks, &BookModel{*app.NewModel(book.Title, "book", req), book})
|
return app.render(res, tplBooks, &BookModel{*NewModel(book.Title, "book"), book})
|
||||||
}
|
}
|
||||||
func (app *Bouquins) authorPage(idParam string, res http.ResponseWriter, req *http.Request) error {
|
func (app *Bouquins) authorPage(idParam string, res http.ResponseWriter, req *http.Request) error {
|
||||||
id, err := strconv.Atoi(idParam)
|
id, err := strconv.Atoi(idParam)
|
||||||
@ -436,7 +381,7 @@ func (app *Bouquins) authorPage(idParam string, res http.ResponseWriter, req *ht
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return app.render(res, tplAuthors, &AuthorModel{*app.NewModel(author.Name, "author", req), author})
|
return app.render(res, tplAuthors, &AuthorModel{*NewModel(author.Name, "author"), author})
|
||||||
}
|
}
|
||||||
func (app *Bouquins) seriePage(idParam string, res http.ResponseWriter, req *http.Request) error {
|
func (app *Bouquins) seriePage(idParam string, res http.ResponseWriter, req *http.Request) error {
|
||||||
id, err := strconv.Atoi(idParam)
|
id, err := strconv.Atoi(idParam)
|
||||||
@ -447,7 +392,7 @@ func (app *Bouquins) seriePage(idParam string, res http.ResponseWriter, req *htt
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return app.render(res, tplSeries, &SeriesModel{*app.NewModel(series.Name, "series", req), series})
|
return app.render(res, tplSeries, &SeriesModel{*NewModel(series.Name, "series"), series})
|
||||||
}
|
}
|
||||||
|
|
||||||
// ROUTES //
|
// ROUTES //
|
||||||
@ -469,12 +414,12 @@ func (app *Bouquins) SeriesPage(res http.ResponseWriter, req *http.Request) erro
|
|||||||
|
|
||||||
// SearchPage displays search form and results
|
// SearchPage displays search form and results
|
||||||
func (app *Bouquins) SearchPage(res http.ResponseWriter, req *http.Request) error {
|
func (app *Bouquins) SearchPage(res http.ResponseWriter, req *http.Request) error {
|
||||||
return app.render(res, tplSearch, app.NewSearchModel(req))
|
return app.render(res, tplSearch, NewSearchModel())
|
||||||
}
|
}
|
||||||
|
|
||||||
// AboutPage displays about page
|
// AboutPage displays about page
|
||||||
func (app *Bouquins) AboutPage(res http.ResponseWriter, req *http.Request) error {
|
func (app *Bouquins) AboutPage(res http.ResponseWriter, req *http.Request) error {
|
||||||
return app.render(res, tplAbout, app.NewModel("A propos", "about", req))
|
return app.render(res, tplAbout, NewModel("A propos", "about"))
|
||||||
}
|
}
|
||||||
|
|
||||||
// IndexPage displays index page: list of books/authors/series
|
// IndexPage displays index page: list of books/authors/series
|
||||||
@ -483,27 +428,9 @@ func (app *Bouquins) IndexPage(res http.ResponseWriter, req *http.Request) error
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
model := app.NewIndexModel("", count, req)
|
model := NewIndexModel("", count)
|
||||||
if isJSON(req) {
|
if isJSON(req) {
|
||||||
return writeJSON(res, model)
|
return writeJSON(res, model)
|
||||||
}
|
}
|
||||||
return app.render(res, tplIndex, 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,8 +104,6 @@ const (
|
|||||||
AND authors.id != ? ORDER BY authors.id`
|
AND authors.id != ? ORDER BY authors.id`
|
||||||
sqlAuthor = "SELECT name FROM authors WHERE 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
|
defaultLimit = 10
|
||||||
|
|
||||||
qtBook QueryType = iota
|
qtBook QueryType = iota
|
||||||
@ -164,10 +162,7 @@ var queries = map[Query]string{
|
|||||||
Query{qtAuthorBooks, false, false}: sqlAuthorBooks,
|
Query{qtAuthorBooks, false, false}: sqlAuthorBooks,
|
||||||
Query{qtAuthorCoauthors, false, false}: sqlAuthorAuthors,
|
Query{qtAuthorCoauthors, false, false}: sqlAuthorAuthors,
|
||||||
}
|
}
|
||||||
var (
|
var stmts = make(map[Query]*sql.Stmt)
|
||||||
stmts = make(map[Query]*sql.Stmt)
|
|
||||||
stmtAccount *sql.Stmt
|
|
||||||
)
|
|
||||||
|
|
||||||
// QueryType is a type of query, with variants for sort and order
|
// QueryType is a type of query, with variants for sort and order
|
||||||
type QueryType uint
|
type QueryType uint
|
||||||
@ -219,13 +214,6 @@ func (app *Bouquins) PrepareAll() error {
|
|||||||
}
|
}
|
||||||
stmts[q] = stmt
|
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 {
|
if errcount > 0 {
|
||||||
return fmt.Errorf("%d errors on queries, see logs", errcount)
|
return fmt.Errorf("%d errors on queries, see logs", errcount)
|
||||||
}
|
}
|
||||||
|
@ -1,11 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
@ -1,94 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
@ -1,83 +0,0 @@
|
|||||||
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,17 +7,24 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"golang.org/x/oauth2"
|
|
||||||
|
|
||||||
"github.com/gorilla/sessions"
|
|
||||||
_ "github.com/mattn/go-sqlite3"
|
_ "github.com/mattn/go-sqlite3"
|
||||||
|
|
||||||
"meutel.net/meutel/go-bouquins/bouquins"
|
"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
|
// ReadConfig loads configuration file and initialize default value
|
||||||
func ReadConfig() (*bouquins.Conf, error) {
|
func ReadConfig() (*BouquinsConf, error) {
|
||||||
conf := new(bouquins.Conf)
|
conf := new(BouquinsConf)
|
||||||
confPath := "bouquins.json"
|
confPath := "bouquins.json"
|
||||||
if len(os.Args) > 1 {
|
if len(os.Args) > 1 {
|
||||||
confPath = os.Args[1]
|
confPath = os.Args[1]
|
||||||
@ -37,16 +44,13 @@ func ReadConfig() (*bouquins.Conf, error) {
|
|||||||
if conf.DbPath == "" {
|
if conf.DbPath == "" {
|
||||||
conf.DbPath = conf.CalibrePath + "/metadata.db"
|
conf.DbPath = conf.CalibrePath + "/metadata.db"
|
||||||
}
|
}
|
||||||
if conf.UserDbPath == "" {
|
|
||||||
conf.UserDbPath = "./users.db"
|
|
||||||
}
|
|
||||||
if conf.BindAddress == "" {
|
if conf.BindAddress == "" {
|
||||||
conf.BindAddress = ":9000"
|
conf.BindAddress = ":9000"
|
||||||
}
|
}
|
||||||
return conf, err
|
return conf, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func initApp() *bouquins.Bouquins {
|
func initApp() *BouquinsConf {
|
||||||
log.SetFlags(log.LstdFlags | log.Lshortfile)
|
log.SetFlags(log.LstdFlags | log.Lshortfile)
|
||||||
conf, err := ReadConfig()
|
conf, err := ReadConfig()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -57,38 +61,25 @@ func initApp() *bouquins.Bouquins {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalln(err)
|
log.Fatalln(err)
|
||||||
}
|
}
|
||||||
db, err := sql.Open("sqlite3", conf.DbPath)
|
db, err = sql.Open("sqlite3", conf.DbPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalln(err)
|
log.Fatalln(err)
|
||||||
}
|
}
|
||||||
userdb, err := sql.Open("sqlite3", conf.UserDbPath)
|
app := &bouquins.Bouquins{Tpl: tpl, DB: db}
|
||||||
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()
|
err = app.PrepareAll()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalln(err)
|
log.Fatalln(err)
|
||||||
}
|
}
|
||||||
|
assets(conf.CalibrePath)
|
||||||
router(app)
|
router(app)
|
||||||
return app
|
return conf
|
||||||
}
|
}
|
||||||
|
|
||||||
func assets(calibre string) {
|
func assets(calibre string) {
|
||||||
http.Handle(bouquins.URLJs, http.StripPrefix("/"+bouquins.Version, http.FileServer(http.Dir("assets"))))
|
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.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.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) {
|
func handle(f func(res http.ResponseWriter, req *http.Request) error) func(res http.ResponseWriter, req *http.Request) {
|
||||||
@ -106,12 +97,7 @@ func handleURL(url string, f func(res http.ResponseWriter, req *http.Request) er
|
|||||||
}
|
}
|
||||||
|
|
||||||
func router(app *bouquins.Bouquins) {
|
func router(app *bouquins.Bouquins) {
|
||||||
assets(app.Conf.CalibrePath)
|
|
||||||
http.Handle(bouquins.URLCalibre, app.CalibreFileServer())
|
|
||||||
handleURL(bouquins.URLIndex, app.IndexPage)
|
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.URLBooks, app.BooksPage)
|
||||||
handleURL(bouquins.URLAuthors, app.AuthorsPage)
|
handleURL(bouquins.URLAuthors, app.AuthorsPage)
|
||||||
handleURL(bouquins.URLSeries, app.SeriesPage)
|
handleURL(bouquins.URLSeries, app.SeriesPage)
|
||||||
@ -120,8 +106,7 @@ func router(app *bouquins.Bouquins) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
app := initApp()
|
conf := initApp()
|
||||||
defer app.DB.Close()
|
defer db.Close()
|
||||||
defer app.UserDB.Close()
|
http.ListenAndServe(conf.BindAddress, nil)
|
||||||
http.ListenAndServe(app.Conf.BindAddress, nil)
|
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,6 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<link rel="stylesheet" href="{{ assetUrl "bootstrap" "css" }}">
|
<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="preload" href="{{ assetUrl "vue" "js" }}" as="script">
|
||||||
<link rel="prefetch" href="{{ assetUrl "vue" "js" }}">
|
<link rel="prefetch" href="{{ assetUrl "vue" "js" }}">
|
||||||
<link rel="preload" href="{{ assetUrl "bouquins" "js" }}" as="script">
|
<link rel="preload" href="{{ assetUrl "bouquins" "js" }}" as="script">
|
||||||
@ -24,12 +23,5 @@
|
|||||||
<input name="q" type="text" class="form-control" placeholder="Recherche">
|
<input name="q" type="text" class="form-control" placeholder="Recherche">
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</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>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
@ -1,12 +0,0 @@
|
|||||||
{{ 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