start
Done so far Basic backend skeleton - technology choices - database migration outline - basic self hosting facade - basic REST outline - proof of concept for auth and privileges Basic frontend skeleton - technology choices - pretty robust frontend compilation - top navigation - proof of concept for registration form
This commit is contained in:
		
						commit
						797ace982f
					
				
							
								
								
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@ -0,0 +1,2 @@
 | 
			
		||||
config.ini
 | 
			
		||||
node_modules
 | 
			
		||||
							
								
								
									
										5
									
								
								.jscsrc
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								.jscsrc
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,5 @@
 | 
			
		||||
{
 | 
			
		||||
    "preset": "google",
 | 
			
		||||
    "fileExtensions": [".js", "jscs"],
 | 
			
		||||
    "validateIndentation": 4,
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										87
									
								
								INSTALL.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										87
									
								
								INSTALL.md
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,87 @@
 | 
			
		||||
This guide assumes Arch Linux. Although exact instructions for other
 | 
			
		||||
distributions are different, the steps stay roughly the same.
 | 
			
		||||
 | 
			
		||||
#### Installing hard dependencies
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
user@host:~$ sudo pacman -S postgres
 | 
			
		||||
user@host:~$ sudo pacman -S python
 | 
			
		||||
user@host:~$ sudo pacman -S python-pip
 | 
			
		||||
user@host:~$ sudo pacman -S npm
 | 
			
		||||
user@host:~$ python --version
 | 
			
		||||
Python 3.5.1
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
#### Setting up a database
 | 
			
		||||
 | 
			
		||||
First, basic `postgres` configuration:
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
user@host:~$ sudo -i -u postgres initdb --locale en_US.UTF-8 -E UTF8 -D /var/lib/postgres/data
 | 
			
		||||
user@host:~$ sudo systemctl start postgresql
 | 
			
		||||
user@host:~$ sudo systemctl enable postgresql
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
Then creating a database:
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
user@host:~$ sudo -i -u postgres createuser --interactive
 | 
			
		||||
Enter name of role to add: szuru
 | 
			
		||||
Shall the new role be a superuser? (y/n) n
 | 
			
		||||
Shall the new role be allowed to create databases? (y/n) n
 | 
			
		||||
Shall the new role be allowed to create more new roles? (y/n) n
 | 
			
		||||
user@host:~$ sudo -i -u postgres createdb szuru
 | 
			
		||||
user@host:~$ sudo -i -u postgres psql -c "ALTER USER szuru PASSWORD 'dog';"
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
#### Installing `szurubooru's` dependencies
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
user@host:path/to/szurubooru$ sudo pip install -r requirements.txt # installs backend deps
 | 
			
		||||
user@host:path/to/szurubooru$ npm install # installs frontend deps
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
#### Preparing `szurubooru` for first use
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
user@host:path/to/szurubooru$ npm run build # compiles frontend
 | 
			
		||||
user@host:path/to/szurubooru$ alembic update head  # runs all DB upgrades
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
Time to configure things:
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
user@host:path/to/szurubooru$ cp config.ini.dist config.ini
 | 
			
		||||
user@host:path/to/szurubooru$ vim config.ini
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
Pay extra attention to the `[database]` and `[smtp]` sections, and API URL in
 | 
			
		||||
`[basic]`.
 | 
			
		||||
 | 
			
		||||
### Upgrading the database
 | 
			
		||||
 | 
			
		||||
    [user@host:path/to/szurubooru] alembic upgrade HEAD
 | 
			
		||||
 | 
			
		||||
Alembic should have been installed during installation of `szurubooru`'s
 | 
			
		||||
dependencies.
 | 
			
		||||
 | 
			
		||||
### Wiring `szurubooru` to the web server
 | 
			
		||||
 | 
			
		||||
`szurubooru` is divided into two parts: public static files, and the API. It
 | 
			
		||||
tries not to impose any networking configurations on the user, so it is the
 | 
			
		||||
user's responsibility to wire these to their web server.
 | 
			
		||||
 | 
			
		||||
Below are described the methods to integrate the API into a web server:
 | 
			
		||||
 | 
			
		||||
1. Run API locally with `waitress`, and bind it with a reverse proxy. In this
 | 
			
		||||
   approach, the user needs to install `waitress` with `pip install waitress`
 | 
			
		||||
   and then start `szurubooru` with `./bin/szurubooru` (see `--help` for
 | 
			
		||||
   details). Then the user needs to add a virtual host that delegates the API
 | 
			
		||||
   requests to the local API server, and the browser requests to the `public/`
 | 
			
		||||
   directory.
 | 
			
		||||
2. Alternatively, Apache users can use `mod_wsgi`.
 | 
			
		||||
3. Alternatively, users can use other WSGI frontends such as `gunicorn` or
 | 
			
		||||
   `uwsgi`, but they'll need to write wrapper scripts themselves.
 | 
			
		||||
 | 
			
		||||
Note that the API URL in the virtual host configuration needs to be the same as
 | 
			
		||||
the one in the `config.ini`, so that client knows how to access the backend!
 | 
			
		||||
							
								
								
									
										23
									
								
								README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								README.md
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,23 @@
 | 
			
		||||
This is rewrite of `szurubooru` 0.9.x that intends to
 | 
			
		||||
 | 
			
		||||
- Improve user experience within frontend. No more vertical user list. Better
 | 
			
		||||
  upload form, larger thumbnails, make top navigation stay out of user way.
 | 
			
		||||
  Maybe other goodies!
 | 
			
		||||
- Finally define sane REST API (with no bullshit such as SQL queries, request
 | 
			
		||||
  timings or exception stack traces this time)
 | 
			
		||||
- Simplify registration - user registers, and they're able to post. No
 | 
			
		||||
  activation e-mails, no nothing (email's going to be used **ONLY** for
 | 
			
		||||
  password reminders, yes, *not even* for confirmation). Note that you will
 | 
			
		||||
  have control over permissions, user ranks and the default user rank, so you
 | 
			
		||||
  might be able to setup a system where user needs to be approved by mod to
 | 
			
		||||
  join the community.
 | 
			
		||||
- Maybe simplify permission system
 | 
			
		||||
- Ditch PHP in favor of something more serious (python 3.5)
 | 
			
		||||
- Ditch in-house JS monstrosities in favor of something more serious (I've got
 | 
			
		||||
  EmberJS on my radar)
 | 
			
		||||
- Replace dependencies such as composer, npm, grunt, and all that crap with
 | 
			
		||||
  just python, and a few pip packages
 | 
			
		||||
- Simplify hosting: offer simple self hosted app combinable with reverse proxies
 | 
			
		||||
- Replace MySQL (/ MariaDB) with Postgres
 | 
			
		||||
- Less god damn code! 24KSLOC? For a thing this simple? The goal is to fit
 | 
			
		||||
  within 15KSLOC. Let's see if I can accomplish this.
 | 
			
		||||
							
								
								
									
										39
									
								
								alembic.ini
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								alembic.ini
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,39 @@
 | 
			
		||||
[alembic]
 | 
			
		||||
script_location = szurubooru/migrations
 | 
			
		||||
 | 
			
		||||
# overriden from within config.ini
 | 
			
		||||
sqlalchemy.url =
 | 
			
		||||
 | 
			
		||||
[loggers]
 | 
			
		||||
keys = root,sqlalchemy,alembic
 | 
			
		||||
 | 
			
		||||
[handlers]
 | 
			
		||||
keys = console
 | 
			
		||||
 | 
			
		||||
[formatters]
 | 
			
		||||
keys = generic
 | 
			
		||||
 | 
			
		||||
[logger_root]
 | 
			
		||||
level = WARN
 | 
			
		||||
handlers = console
 | 
			
		||||
qualname =
 | 
			
		||||
 | 
			
		||||
[logger_sqlalchemy]
 | 
			
		||||
level = WARN
 | 
			
		||||
handlers =
 | 
			
		||||
qualname = sqlalchemy.engine
 | 
			
		||||
 | 
			
		||||
[logger_alembic]
 | 
			
		||||
level = INFO
 | 
			
		||||
handlers =
 | 
			
		||||
qualname = alembic
 | 
			
		||||
 | 
			
		||||
[handler_console]
 | 
			
		||||
class = StreamHandler
 | 
			
		||||
args = (sys.stderr,)
 | 
			
		||||
level = NOTSET
 | 
			
		||||
formatter = generic
 | 
			
		||||
 | 
			
		||||
[formatter_generic]
 | 
			
		||||
format = %(levelname)-5.5s [%(name)s] %(message)s
 | 
			
		||||
datefmt = %H:%M:%S
 | 
			
		||||
							
								
								
									
										30
									
								
								bin/szurubooru
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										30
									
								
								bin/szurubooru
									
									
									
									
									
										Executable file
									
								
							@ -0,0 +1,30 @@
 | 
			
		||||
#!/bin/python3
 | 
			
		||||
 | 
			
		||||
'''
 | 
			
		||||
Script facade for direct execution with waitress WSGI server.
 | 
			
		||||
Note that szurubooru can be also run using ``python -m szurubooru``, when in
 | 
			
		||||
the repository's root directory.
 | 
			
		||||
'''
 | 
			
		||||
 | 
			
		||||
import argparse
 | 
			
		||||
import os.path
 | 
			
		||||
import sys
 | 
			
		||||
import waitress
 | 
			
		||||
 | 
			
		||||
sys.path.append(os.path.join(os.path.dirname(os.path.realpath(__file__)), os.path.pardir))
 | 
			
		||||
from szurubooru.app import create_app
 | 
			
		||||
 | 
			
		||||
def main():
 | 
			
		||||
    parser = argparse.ArgumentParser('Starts szurubooru using waitress.')
 | 
			
		||||
    parser.add_argument(
 | 
			
		||||
        '-p', '--port',
 | 
			
		||||
        type=int, help='port to listen on', default=6666)
 | 
			
		||||
    parser.add_argument(
 | 
			
		||||
        '--host', help='IP to listen on', default='0.0.0.0')
 | 
			
		||||
    args = parser.parse_args()
 | 
			
		||||
 | 
			
		||||
    app = create_app()
 | 
			
		||||
    waitress.serve(app, host=args.host, port=args.port)
 | 
			
		||||
 | 
			
		||||
if __name__ == '__main__':
 | 
			
		||||
    main()
 | 
			
		||||
							
								
								
									
										90
									
								
								config.ini.dist
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										90
									
								
								config.ini.dist
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,90 @@
 | 
			
		||||
# rather than editing this file, it is strongly suggested to create config.ini
 | 
			
		||||
# and override only what you need.
 | 
			
		||||
 | 
			
		||||
[basic]
 | 
			
		||||
name = szurubooru
 | 
			
		||||
debug = 0
 | 
			
		||||
secret = change
 | 
			
		||||
api_url = http://api.example.com/ # see INSTALL.md
 | 
			
		||||
 | 
			
		||||
[database]
 | 
			
		||||
schema = postgres
 | 
			
		||||
host = localhost
 | 
			
		||||
port = 5432
 | 
			
		||||
user = szuru
 | 
			
		||||
pass = dog
 | 
			
		||||
name = szuru
 | 
			
		||||
 | 
			
		||||
[smtp]
 | 
			
		||||
# used to send password reminders
 | 
			
		||||
host = localhost
 | 
			
		||||
port = 25
 | 
			
		||||
user = bot
 | 
			
		||||
pass = groovy123
 | 
			
		||||
 | 
			
		||||
[service]
 | 
			
		||||
# note: anonymous, admin and nobody are always reserved
 | 
			
		||||
user_ranks = regular_user, power_user, mod
 | 
			
		||||
default_user_rank = regular_user
 | 
			
		||||
users_per_page = 20
 | 
			
		||||
posts_per_page = 40
 | 
			
		||||
max_comment_length = 5000
 | 
			
		||||
tag_categories = meta, artist, character, copyright, other unique
 | 
			
		||||
 | 
			
		||||
# don't change these regexes, unless you want to annoy people. but if you do
 | 
			
		||||
# customize them, make sure to update the instructions in the registration form
 | 
			
		||||
# template as well.
 | 
			
		||||
password_regex = ^.{5,}$
 | 
			
		||||
user_name_regex = ^[a-zA-Z0-9_-]{1,32}$
 | 
			
		||||
 | 
			
		||||
[privileges]
 | 
			
		||||
users:create = anonymous
 | 
			
		||||
users:list = regular_user
 | 
			
		||||
users:view = regular_user
 | 
			
		||||
users:edit:any:name = mod
 | 
			
		||||
users:edit:any:pass = mod
 | 
			
		||||
users:edit:any:email = mod
 | 
			
		||||
users:edit:any:avatar = mod
 | 
			
		||||
# note: promoting people to higher rank than one's own is always impossible
 | 
			
		||||
users:edit:any:rank = mod
 | 
			
		||||
users:edit:self:name = regular_user
 | 
			
		||||
users:edit:self:pass = regular_user
 | 
			
		||||
users:edit:self:email = regular_user
 | 
			
		||||
users:edit:self:avatar = regular_user
 | 
			
		||||
users:edit:self:rank = mod
 | 
			
		||||
users:delete:any = admin
 | 
			
		||||
users:delete:self = restricted_user
 | 
			
		||||
 | 
			
		||||
posts:create:anonymous = regular_user
 | 
			
		||||
posts:create:identified = regular_user
 | 
			
		||||
posts:list = anonymous
 | 
			
		||||
posts:view = anonymous
 | 
			
		||||
posts:edit:content = power_user
 | 
			
		||||
posts:edit:flags = regular_user
 | 
			
		||||
posts:edit:notes = regular_user
 | 
			
		||||
posts:edit:relations = regular_user
 | 
			
		||||
posts:edit:safety = power_user
 | 
			
		||||
posts:edit:source = regular_user
 | 
			
		||||
posts:edit:tags = regular_user
 | 
			
		||||
posts:edit:thumbnail = power_user
 | 
			
		||||
posts:feature = mod
 | 
			
		||||
posts:delete = mod
 | 
			
		||||
 | 
			
		||||
tags:create = regular_user
 | 
			
		||||
tags:edit:name = power_user
 | 
			
		||||
tags:edit:category = power_user
 | 
			
		||||
tags:edit:implications = power_user
 | 
			
		||||
tags:edit:suggestions = power_user
 | 
			
		||||
tags:list = regular_user
 | 
			
		||||
tags:masstag = power_user
 | 
			
		||||
tags:merge = mod
 | 
			
		||||
tags:delete = mod
 | 
			
		||||
 | 
			
		||||
comments:create = regular_user
 | 
			
		||||
comments:delete:any = mod
 | 
			
		||||
comments:delete:own = regular_user
 | 
			
		||||
comments:edit:any = mod
 | 
			
		||||
comments:edit:own = regular_user
 | 
			
		||||
comments:list = regular_user
 | 
			
		||||
 | 
			
		||||
history:view = power_user
 | 
			
		||||
							
								
								
									
										23
									
								
								package.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								package.json
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,23 @@
 | 
			
		||||
{
 | 
			
		||||
  "name": "szurubooru",
 | 
			
		||||
  "private": true,
 | 
			
		||||
  "scripts": {
 | 
			
		||||
    "browserify": "browserify static/js/*.js -o public/bundle.js",
 | 
			
		||||
    "build:html": "cat static/html/index.htm >public/index.htm",
 | 
			
		||||
    "build:css": "cat static/css/*.css >public/bundle.css && cssmin public/bundle.css >public/bundle.min.css",
 | 
			
		||||
    "build:js": "node static/prepare_config.js && npm run browserify -o public/bundle.js && uglifyjs <public/bundle.js >public/bundle.min.js",
 | 
			
		||||
    "build": "npm run build:html && npm run build:js && npm run build:css",
 | 
			
		||||
    "watch": "watch 'npm run build' static --wait=0 --ignoreDotFiles"
 | 
			
		||||
  },
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "browserify": "^13.0.0",
 | 
			
		||||
    "camelcase-keys": "^2.1.0",
 | 
			
		||||
    "cssmin": "^0.4.3",
 | 
			
		||||
    "handlebars": "^4.0.5",
 | 
			
		||||
    "ini": "^1.3.4",
 | 
			
		||||
    "merge": "^1.2.0",
 | 
			
		||||
    "page": "^1.7.1",
 | 
			
		||||
    "uglify-js": "git://github.com/mishoo/UglifyJS2.git#harmony",
 | 
			
		||||
    "watch": "latest"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										2
									
								
								public/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								public/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@ -0,0 +1,2 @@
 | 
			
		||||
*.*
 | 
			
		||||
!.gitignore
 | 
			
		||||
							
								
								
									
										5
									
								
								requirements.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								requirements.txt
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,5 @@
 | 
			
		||||
alembic>=0.8.5
 | 
			
		||||
configobj>=5.0.6
 | 
			
		||||
falcon>=0.3.0
 | 
			
		||||
psycopg2>=2.6.1
 | 
			
		||||
SQLAlchemy>=1.0.12
 | 
			
		||||
							
								
								
									
										79
									
								
								static/css/forms.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										79
									
								
								static/css/forms.css
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,79 @@
 | 
			
		||||
form fieldset {
 | 
			
		||||
    margin: 0;
 | 
			
		||||
    padding: 0;
 | 
			
		||||
    border: 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
form fieldset legend {
 | 
			
		||||
    display: block;
 | 
			
		||||
    text-align: center;
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    font-size: 17pt;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
form ul {
 | 
			
		||||
    list-style-type: none;
 | 
			
		||||
    margin: 0;
 | 
			
		||||
    padding: 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
form.tabular ul {
 | 
			
		||||
    display: table;
 | 
			
		||||
    border-spacing: 0.5em;
 | 
			
		||||
    margin: 0.5em -0.5em;
 | 
			
		||||
    width: 100%;
 | 
			
		||||
}
 | 
			
		||||
form.tabular ul li {
 | 
			
		||||
    display: table-row;
 | 
			
		||||
}
 | 
			
		||||
form.tabular ul li label {
 | 
			
		||||
    display: table-cell;
 | 
			
		||||
    width: 33%;
 | 
			
		||||
    padding: 0;
 | 
			
		||||
}
 | 
			
		||||
form.tabular .buttons {
 | 
			
		||||
    margin-left: 33%;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
form:not(.tabular) ul li label {
 | 
			
		||||
    display: block;
 | 
			
		||||
    padding: 0.5em 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
textarea,
 | 
			
		||||
input[type=text],
 | 
			
		||||
input[type=email],
 | 
			
		||||
input[type=password] {
 | 
			
		||||
    font-size: 100%;
 | 
			
		||||
    font-family: 'Inconsolata', monospace;
 | 
			
		||||
    padding: 0.3em;
 | 
			
		||||
    border: 1px solid #EEE;
 | 
			
		||||
    background: #FAFAFA;
 | 
			
		||||
    text-overflow: ellipsis;
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    box-sizing: border-box;
 | 
			
		||||
    box-shadow: none; /* :-moz-submit-invalid on FF */
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
form.show-validation fieldset.input input:invalid {
 | 
			
		||||
    outline: none;
 | 
			
		||||
    border: 1px solid #FCC;
 | 
			
		||||
    background: #FFF5F5;
 | 
			
		||||
}
 | 
			
		||||
form.show-validation fieldset.input input:valid {
 | 
			
		||||
    outline: none;
 | 
			
		||||
    border: 1px solid #D3E3D3;
 | 
			
		||||
    background: #F5FFF5;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
button,
 | 
			
		||||
input[type=button],
 | 
			
		||||
input[type=submit] {
 | 
			
		||||
    cursor: pointer;
 | 
			
		||||
    font-size: 100%;
 | 
			
		||||
    line-height: 100%;
 | 
			
		||||
    padding: 0.3em 0.7em;
 | 
			
		||||
    border: 1px solid #24AADD;
 | 
			
		||||
    background: #24AADD;
 | 
			
		||||
    color: white;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										74
									
								
								static/css/main.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										74
									
								
								static/css/main.css
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,74 @@
 | 
			
		||||
body {
 | 
			
		||||
    margin: 0;
 | 
			
		||||
    color: #111;
 | 
			
		||||
    font-family: 'Droid Sans' !important;
 | 
			
		||||
    font-size: 12pt;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#content-holder {
 | 
			
		||||
    margin-top: 1em;
 | 
			
		||||
    text-align: center;
 | 
			
		||||
}
 | 
			
		||||
#content-holder>.center {
 | 
			
		||||
    text-align: left;
 | 
			
		||||
    display: inline-block;
 | 
			
		||||
    margin: 0 auto;
 | 
			
		||||
}
 | 
			
		||||
hr {
 | 
			
		||||
    border: 0;
 | 
			
		||||
    border-top: 1px solid #ddd;
 | 
			
		||||
    margin: 1em 0;
 | 
			
		||||
    padding: 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
nav ul {
 | 
			
		||||
    list-style-type: none;
 | 
			
		||||
    padding: 0;
 | 
			
		||||
    margin: 0;
 | 
			
		||||
    display: inline-block;
 | 
			
		||||
}
 | 
			
		||||
nav ul li {
 | 
			
		||||
    display: inline-block;
 | 
			
		||||
    padding: 0;
 | 
			
		||||
    margin: 0;
 | 
			
		||||
}
 | 
			
		||||
nav ul li a {
 | 
			
		||||
    display: inline-block;
 | 
			
		||||
}
 | 
			
		||||
nav ul li img {
 | 
			
		||||
    margin: 0;
 | 
			
		||||
    vertical-align: top; /* fix ghost margin under the image */
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
nav.text-nav {
 | 
			
		||||
    margin: 1em 0;
 | 
			
		||||
}
 | 
			
		||||
nav.text-nav ul li a {
 | 
			
		||||
    padding: 0.3em 1.2em;
 | 
			
		||||
    text-decoration: none;
 | 
			
		||||
}
 | 
			
		||||
nav.text-nav ul li:not(.active) a {
 | 
			
		||||
    color: #888;
 | 
			
		||||
}
 | 
			
		||||
nav.text-nav ul li.active a {
 | 
			
		||||
    background: #24AADD;
 | 
			
		||||
    color: white;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#top-nav {
 | 
			
		||||
    background: #F5F5F5;
 | 
			
		||||
    margin: 0;
 | 
			
		||||
}
 | 
			
		||||
#top-nav ul {
 | 
			
		||||
    display: block;
 | 
			
		||||
    text-align: right;
 | 
			
		||||
}
 | 
			
		||||
#top-nav ul li {
 | 
			
		||||
    float: left;
 | 
			
		||||
}
 | 
			
		||||
#top-nav ul li[data-name=register],
 | 
			
		||||
#top-nav ul li[data-name=login],
 | 
			
		||||
#top-nav ul li[data-name=logout],
 | 
			
		||||
#top-nav ul li[data-name=help] {
 | 
			
		||||
    float: none;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										16
									
								
								static/css/users.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								static/css/users.css
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,16 @@
 | 
			
		||||
#user-registration form {
 | 
			
		||||
    display: block;
 | 
			
		||||
    width: 20em;
 | 
			
		||||
    float: left;
 | 
			
		||||
}
 | 
			
		||||
#user-registration div.info {
 | 
			
		||||
    float: left;
 | 
			
		||||
    margin-left: 3em;
 | 
			
		||||
    border-radius: 0.2em;
 | 
			
		||||
    width: 20em;
 | 
			
		||||
}
 | 
			
		||||
#user-registration .info p:first-child,
 | 
			
		||||
#user-registration form li:first-child label {
 | 
			
		||||
    padding-top: 0;
 | 
			
		||||
    margin-top: 0;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										71
									
								
								static/html/index.htm
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										71
									
								
								static/html/index.htm
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,71 @@
 | 
			
		||||
<!DOCTYPE html>
 | 
			
		||||
<html>
 | 
			
		||||
<head>
 | 
			
		||||
    <title>szurubooru</title>
 | 
			
		||||
    <meta charset='utf-8'/>
 | 
			
		||||
    <link href='/bundle.min.css' rel='stylesheet' type='text/css'>
 | 
			
		||||
    <link href='//fonts.googleapis.com/css?family=Droid+Sans' rel='stylesheet' type='text/css'>
 | 
			
		||||
    <link href='//fonts.googleapis.com/css?family=Inconsolata' rel='stylesheet' type='text/css'>
 | 
			
		||||
    <!-- TODO: configurable favicon -->
 | 
			
		||||
    <template id='top-nav-template'>
 | 
			
		||||
        <nav id='top-nav' class='text-nav'>
 | 
			
		||||
            <ul><!--
 | 
			
		||||
                -->{{#each items}}<!--
 | 
			
		||||
                    -->{{#if this.available}}<!--
 | 
			
		||||
                        --><li data-name='{{@key}}'><!--
 | 
			
		||||
                            --><a href='{{this.url}}'>{{this.name}}</a><!--
 | 
			
		||||
                        --></li><!--
 | 
			
		||||
                    -->{{/if}}<!--
 | 
			
		||||
                -->{{/each}}<!--
 | 
			
		||||
            --></ul>
 | 
			
		||||
        </nav>
 | 
			
		||||
    </template>
 | 
			
		||||
 | 
			
		||||
    <template id='user-registration-template'>
 | 
			
		||||
        <div class='center' id='user-registration'>
 | 
			
		||||
            <h1>Registration</h1>
 | 
			
		||||
            <br/>
 | 
			
		||||
            <form autocomplete='off'>
 | 
			
		||||
                <fieldset class='input'>
 | 
			
		||||
                    <ul>
 | 
			
		||||
                        <li>
 | 
			
		||||
                            <label for='user-name'>User name:</label>
 | 
			
		||||
                            <input id='user-name' name='user-name' type='text' autocomplete='off' placeholder='e.g. darth_vader' required/>
 | 
			
		||||
                        </li>
 | 
			
		||||
                        <li>
 | 
			
		||||
                            <label for='user-password'>Password:</label>
 | 
			
		||||
                            <input id='user-password' name='user-password' type='password' autocomplete='off' placeholder='e.g. cupcake' required/>
 | 
			
		||||
                        </li>
 | 
			
		||||
                        <li>
 | 
			
		||||
                            <label for='user-email'>Email (optional):</label>
 | 
			
		||||
                            <input id='user-email' name='user-email' type='email' autocomplete='off' placeholder='e.g. vader@empire.gov'/>
 | 
			
		||||
                        </li>
 | 
			
		||||
                    </ul>
 | 
			
		||||
                </fieldset>
 | 
			
		||||
                <hr/>
 | 
			
		||||
                <p>By clicking "Create an account" button below, you are agreeing to the <a href='/help/tos'>Terms of Service</a>.</p>
 | 
			
		||||
                <hr/>
 | 
			
		||||
                <fieldset class='buttons'>
 | 
			
		||||
                    <input type='submit' value='Create an account'/>
 | 
			
		||||
                </fieldset>
 | 
			
		||||
            </form>
 | 
			
		||||
            <div class='info'>
 | 
			
		||||
                <p>Registered users can:</p>
 | 
			
		||||
                <ul>
 | 
			
		||||
                    <li>upload new posts</li>
 | 
			
		||||
                    <li>mark them as favorite</li>
 | 
			
		||||
                    <li>add comments</li>
 | 
			
		||||
                    <li>vote up/down on posts and comments</li>
 | 
			
		||||
                </ul>
 | 
			
		||||
                <p>Your e-mail will be used to show your <a href='http://gravatar.com/'>Gravatar</a> and for password reminders only. Leave blank for random Gravatar.</p>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </template>
 | 
			
		||||
 | 
			
		||||
</head>
 | 
			
		||||
<body>
 | 
			
		||||
    <div id='top-nav-holder'></div>
 | 
			
		||||
    <div id='content-holder'></div>
 | 
			
		||||
<script type='text/javascript' src='/bundle.min.js'></script>
 | 
			
		||||
</body>
 | 
			
		||||
</html>
 | 
			
		||||
							
								
								
									
										1
									
								
								static/js/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								static/js/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@ -0,0 +1 @@
 | 
			
		||||
.config.autogen.json
 | 
			
		||||
							
								
								
									
										4
									
								
								static/js/config.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								static/js/config.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,4 @@
 | 
			
		||||
'use strict';
 | 
			
		||||
 | 
			
		||||
const config = require('./.config.autogen.json');
 | 
			
		||||
module.exports = config;
 | 
			
		||||
							
								
								
									
										34
									
								
								static/js/controllers/auth_controller.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								static/js/controllers/auth_controller.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,34 @@
 | 
			
		||||
'use strict';
 | 
			
		||||
 | 
			
		||||
class AuthController {
 | 
			
		||||
    constructor(topNavigationController) {
 | 
			
		||||
        this.topNavigationController = topNavigationController;
 | 
			
		||||
        this.currentUser = null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    isLoggedIn() {
 | 
			
		||||
        return this.currentUser !== null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    hasPrivilege() {
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    login(user) {
 | 
			
		||||
        this.currentUser = user;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    logout(user) {
 | 
			
		||||
        this.currentUser = null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    loginRoute() {
 | 
			
		||||
        this.topNavigationController.activate('login');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    logoutRoute() {
 | 
			
		||||
        this.topNavigationController.activate('logout');
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
module.exports = AuthController;
 | 
			
		||||
							
								
								
									
										13
									
								
								static/js/controllers/comments_controller.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								static/js/controllers/comments_controller.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,13 @@
 | 
			
		||||
'use strict';
 | 
			
		||||
 | 
			
		||||
class CommentsController {
 | 
			
		||||
    constructor(topNavigationController) {
 | 
			
		||||
        this.topNavigationController = topNavigationController;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    listCommentsRoute() {
 | 
			
		||||
        this.topNavigationController.activate('comments');
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
module.exports = CommentsController;
 | 
			
		||||
							
								
								
									
										13
									
								
								static/js/controllers/help_controller.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								static/js/controllers/help_controller.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,13 @@
 | 
			
		||||
'use strict';
 | 
			
		||||
 | 
			
		||||
class HelpController {
 | 
			
		||||
    constructor(topNavigationController) {
 | 
			
		||||
        this.topNavigationController = topNavigationController;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    showHelpRoute() {
 | 
			
		||||
        this.topNavigationController.activate('help');
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
module.exports = HelpController;
 | 
			
		||||
							
								
								
									
										13
									
								
								static/js/controllers/history_controller.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								static/js/controllers/history_controller.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,13 @@
 | 
			
		||||
'use strict';
 | 
			
		||||
 | 
			
		||||
class HistoryController {
 | 
			
		||||
    constructor(topNavigationController) {
 | 
			
		||||
        this.topNavigationController = topNavigationController;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    listHistoryRoute() {
 | 
			
		||||
        this.topNavigationController.activate('');
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
module.exports = HistoryController;
 | 
			
		||||
							
								
								
									
										17
									
								
								static/js/controllers/home_controller.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								static/js/controllers/home_controller.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,17 @@
 | 
			
		||||
'use strict';
 | 
			
		||||
 | 
			
		||||
class HomeController {
 | 
			
		||||
    constructor(topNavigationController) {
 | 
			
		||||
        this.topNavigationController = topNavigationController;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    indexRoute() {
 | 
			
		||||
        this.topNavigationController.activate('home');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    notFoundRoute() {
 | 
			
		||||
        this.topNavigationController.activate('');
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
module.exports = HomeController;
 | 
			
		||||
							
								
								
									
										25
									
								
								static/js/controllers/posts_controller.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								static/js/controllers/posts_controller.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,25 @@
 | 
			
		||||
'use strict';
 | 
			
		||||
 | 
			
		||||
class PostsController {
 | 
			
		||||
    constructor(topNavigationController) {
 | 
			
		||||
        this.topNavigationController = topNavigationController;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    uploadPostsRoute() {
 | 
			
		||||
        this.topNavigationController.activate('upload');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    listPostsRoute() {
 | 
			
		||||
        this.topNavigationController.activate('posts');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    showPostRoute(id) {
 | 
			
		||||
        this.topNavigationController.activate('posts');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    editPostRoute(id) {
 | 
			
		||||
        this.topNavigationController.activate('posts');
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
module.exports = PostsController;
 | 
			
		||||
							
								
								
									
										13
									
								
								static/js/controllers/tags_controller.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								static/js/controllers/tags_controller.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,13 @@
 | 
			
		||||
'use strict';
 | 
			
		||||
 | 
			
		||||
class TagsController {
 | 
			
		||||
    constructor(topNavigationController) {
 | 
			
		||||
        this.topNavigationController = topNavigationController;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    listTagsRoute() {
 | 
			
		||||
        this.topNavigationController.activate('tags');
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
module.exports = TagsController;
 | 
			
		||||
							
								
								
									
										69
									
								
								static/js/controllers/top_navigation_controller.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										69
									
								
								static/js/controllers/top_navigation_controller.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,69 @@
 | 
			
		||||
'use strict';
 | 
			
		||||
 | 
			
		||||
class NavigationItem {
 | 
			
		||||
    constructor(name, url) {
 | 
			
		||||
        this.name = name;
 | 
			
		||||
        this.url = url;
 | 
			
		||||
        this.available = true;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class TopNavigationController {
 | 
			
		||||
    constructor(topNavigationView, authController) {
 | 
			
		||||
        this.authController = authController;
 | 
			
		||||
        this.topNavigationView = topNavigationView;
 | 
			
		||||
 | 
			
		||||
        this.items = {
 | 
			
		||||
            'home':     new NavigationItem('Home',     '/'),
 | 
			
		||||
            'posts':    new NavigationItem('Posts',    '/posts'),
 | 
			
		||||
            'upload':   new NavigationItem('Upload',   '/upload'),
 | 
			
		||||
            'comments': new NavigationItem('Comments', '/comments'),
 | 
			
		||||
            'tags':     new NavigationItem('Tags',     '/tags'),
 | 
			
		||||
            'users':    new NavigationItem('Users',    '/users'),
 | 
			
		||||
            'account':  new NavigationItem('Account',  '/user/{me}'),
 | 
			
		||||
            'register': new NavigationItem('Register', '/register'),
 | 
			
		||||
            'login':    new NavigationItem('Login',    '/login'),
 | 
			
		||||
            'logout':   new NavigationItem('Logout',   '/logout'),
 | 
			
		||||
            'help':     new NavigationItem('Help',     '/help'),
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        this.updateVisibility();
 | 
			
		||||
 | 
			
		||||
        this.topNavigationView.render(this.items);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    updateVisibility() {
 | 
			
		||||
        const b = Object.keys(this.items);
 | 
			
		||||
        for (let key of b) {
 | 
			
		||||
            this.items[key].available = true;
 | 
			
		||||
        }
 | 
			
		||||
        if (!this.authController.hasPrivilege('posts:list')) {
 | 
			
		||||
            this.items.posts.available = false;
 | 
			
		||||
        }
 | 
			
		||||
        if (!this.authController.hasPrivilege('posts:upload')) {
 | 
			
		||||
            this.items.upload.available = false;
 | 
			
		||||
        }
 | 
			
		||||
        if (!this.authController.hasPrivilege('comments:list')) {
 | 
			
		||||
            this.items.comments.available = false;
 | 
			
		||||
        }
 | 
			
		||||
        if (!this.authController.hasPrivilege('tags:list')) {
 | 
			
		||||
            this.items.tags.available = false;
 | 
			
		||||
        }
 | 
			
		||||
        if (!this.authController.hasPrivilege('users:list')) {
 | 
			
		||||
            this.items.users.available = false;
 | 
			
		||||
        }
 | 
			
		||||
        if (this.authController.isLoggedIn()) {
 | 
			
		||||
            this.items.register.available = false;
 | 
			
		||||
            this.items.login.available = false;
 | 
			
		||||
        } else {
 | 
			
		||||
            this.items.account.available = false;
 | 
			
		||||
            this.items.logout.available = false;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    activate(itemName) {
 | 
			
		||||
        this.topNavigationView.activate(itemName);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
module.exports = TopNavigationController;
 | 
			
		||||
							
								
								
									
										38
									
								
								static/js/controllers/users_controller.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								static/js/controllers/users_controller.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,38 @@
 | 
			
		||||
'use strict';
 | 
			
		||||
 | 
			
		||||
class UsersController {
 | 
			
		||||
    constructor(topNavigationController, authController, registrationView) {
 | 
			
		||||
        this.topNavigationController = topNavigationController;
 | 
			
		||||
        this.authController = authController;
 | 
			
		||||
        this.registrationView = registrationView;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    listUsersRoute() {
 | 
			
		||||
        this.topNavigationController.activate('users');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    createUserRoute() {
 | 
			
		||||
        const self = this;
 | 
			
		||||
        this.topNavigationController.activate('register');
 | 
			
		||||
        this.registrationView.render({
 | 
			
		||||
            onRegistered: (user) => {
 | 
			
		||||
                alert(user);
 | 
			
		||||
                self.authController.login(user);
 | 
			
		||||
            }});
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    showUserRoute(user) {
 | 
			
		||||
        if (this.authController.isLoggedIn() &&
 | 
			
		||||
                user == this.authController.getCurrentUser().name) {
 | 
			
		||||
            this.topNavigationController.activate('account');
 | 
			
		||||
        } else {
 | 
			
		||||
            this.topNavigationController.activate('users');
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    editUserRoute(user) {
 | 
			
		||||
        this.topNavigationController.activate('users');
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
module.exports = UsersController;
 | 
			
		||||
							
								
								
									
										70
									
								
								static/js/main.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										70
									
								
								static/js/main.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,70 @@
 | 
			
		||||
'use strict';
 | 
			
		||||
 | 
			
		||||
// ------------------
 | 
			
		||||
// - import objects -
 | 
			
		||||
// ------------------
 | 
			
		||||
const page = require('page');
 | 
			
		||||
const handlebars = require('handlebars');
 | 
			
		||||
 | 
			
		||||
const RegistrationView = require('./views/registration_view.js');
 | 
			
		||||
const TopNavigationView = require('./views/top_navigation_view.js');
 | 
			
		||||
const TopNavigationController
 | 
			
		||||
    = require('./controllers/top_navigation_controller.js');
 | 
			
		||||
 | 
			
		||||
const HomeController = require('./controllers/home_controller.js');
 | 
			
		||||
const PostsController = require('./controllers/posts_controller.js');
 | 
			
		||||
const UsersController = require('./controllers/users_controller.js');
 | 
			
		||||
const HelpController = require('./controllers/help_controller.js');
 | 
			
		||||
const AuthController = require('./controllers/auth_controller.js');
 | 
			
		||||
const CommentsController = require('./controllers/comments_controller.js');
 | 
			
		||||
const HistoryController = require('./controllers/history_controller.js');
 | 
			
		||||
const TagsController = require('./controllers/tags_controller.js');
 | 
			
		||||
 | 
			
		||||
// -------------------
 | 
			
		||||
// - resolve objects -
 | 
			
		||||
// -------------------
 | 
			
		||||
const topNavigationView = new TopNavigationView(handlebars);
 | 
			
		||||
const registrationView = new RegistrationView(handlebars);
 | 
			
		||||
 | 
			
		||||
const authController = new AuthController(null);
 | 
			
		||||
const topNavigationController
 | 
			
		||||
    = new TopNavigationController(topNavigationView, authController);
 | 
			
		||||
// break cyclic dependency topNavigationView<->authController
 | 
			
		||||
authController.topNavigationController = topNavigationController;
 | 
			
		||||
 | 
			
		||||
const homeController = new HomeController(topNavigationController);
 | 
			
		||||
const postsController = new PostsController(topNavigationController);
 | 
			
		||||
const usersController = new UsersController(
 | 
			
		||||
    topNavigationController,
 | 
			
		||||
    authController,
 | 
			
		||||
    registrationView);
 | 
			
		||||
const helpController = new HelpController(topNavigationController);
 | 
			
		||||
const commentsController = new CommentsController(topNavigationController);
 | 
			
		||||
const historyController = new HistoryController(topNavigationController);
 | 
			
		||||
const tagsController = new TagsController(topNavigationController);
 | 
			
		||||
 | 
			
		||||
// -----------------
 | 
			
		||||
// - setup routing -
 | 
			
		||||
// -----------------
 | 
			
		||||
page('/', () => { homeController.indexRoute(); });
 | 
			
		||||
 | 
			
		||||
page('/upload', () => { postsController.uploadPostsRoute(); });
 | 
			
		||||
page('/posts', () => { postsController.listPostsRoute(); });
 | 
			
		||||
page('/post/:id', (id) => { postsController.showPostRoute(id); });
 | 
			
		||||
page('/post/:id/edit', (id) => { postsController.editPostRoute(id); });
 | 
			
		||||
 | 
			
		||||
page('/register', () => { usersController.createUserRoute(); });
 | 
			
		||||
page('/users', () => { usersController.listUsersRoute(); });
 | 
			
		||||
page('/user/:user', (user) => { usersController.showUserRoute(user); });
 | 
			
		||||
page('/user/:user/edit', (user) => { usersController.editUserRoute(user); });
 | 
			
		||||
 | 
			
		||||
page('/history', () => { historyController.showHistoryRoute(); });
 | 
			
		||||
page('/tags', () => { tagsController.listTagsRoute(); });
 | 
			
		||||
page('/comments', () => { commentsController.listCommentsRoute(); });
 | 
			
		||||
page('/login', () => { authController.loginRoute(); });
 | 
			
		||||
page('/logout', () => { authController.logoutRoute(); });
 | 
			
		||||
page('/help', () => { helpController.showHelpRoute(); });
 | 
			
		||||
 | 
			
		||||
page('*', () => { homeController.notFoundRoute(); });
 | 
			
		||||
 | 
			
		||||
page();
 | 
			
		||||
							
								
								
									
										37
									
								
								static/js/views/base_view.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								static/js/views/base_view.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,37 @@
 | 
			
		||||
'use strict';
 | 
			
		||||
 | 
			
		||||
// fix iterating over NodeList in Chrome and Opera
 | 
			
		||||
NodeList.prototype[Symbol.iterator] = Array.prototype[Symbol.iterator];
 | 
			
		||||
 | 
			
		||||
class BaseView {
 | 
			
		||||
    constructor(handlebars) {
 | 
			
		||||
        this.handlebars = handlebars;
 | 
			
		||||
        this.contentHolder = document.getElementById('content-holder');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    getTemplate(templatePath) {
 | 
			
		||||
        const templateElement = document.getElementById(templatePath);
 | 
			
		||||
        if (!templateElement) {
 | 
			
		||||
            console.log('Missing template: ' + templatePath);
 | 
			
		||||
            return null;
 | 
			
		||||
        }
 | 
			
		||||
        const templateText = templateElement.innerHTML;
 | 
			
		||||
        return this.handlebars.compile(templateText);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    decorateValidator(form) {
 | 
			
		||||
        // postpone showing form fields validity until user actually tries
 | 
			
		||||
        // to submit it (seeing red/green form w/o doing anything breaks POLA)
 | 
			
		||||
        const submitButton
 | 
			
		||||
            = document.querySelector('#content-holder .buttons input');
 | 
			
		||||
        submitButton.addEventListener('click', (e) => {
 | 
			
		||||
            form.classList.add('show-validation');
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    showView(html) {
 | 
			
		||||
        this.contentHolder.innerHTML = html;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
module.exports = BaseView;
 | 
			
		||||
							
								
								
									
										35
									
								
								static/js/views/registration_view.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								static/js/views/registration_view.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,35 @@
 | 
			
		||||
'use strict';
 | 
			
		||||
 | 
			
		||||
const config = require('../config.js');
 | 
			
		||||
const BaseView = require('./base_view.js');
 | 
			
		||||
 | 
			
		||||
class RegistrationView extends BaseView {
 | 
			
		||||
    constructor(handlebars) {
 | 
			
		||||
        super(handlebars);
 | 
			
		||||
        this.template = this.getTemplate('user-registration-template');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    render(settings) {
 | 
			
		||||
        this.showView(this.template());
 | 
			
		||||
        const form = document.querySelector('#content-holder form');
 | 
			
		||||
        this.decorateValidator(form);
 | 
			
		||||
 | 
			
		||||
        const userNameField = document.getElementById('user-name');
 | 
			
		||||
        const passwordField = document.getElementById('user-password');
 | 
			
		||||
        const emailField = document.getElementById('user-email');
 | 
			
		||||
        userNameField.setAttribute('pattern', config.service.userNameRegex);
 | 
			
		||||
        passwordField.setAttribute('pattern', config.service.passwordRegex);
 | 
			
		||||
 | 
			
		||||
        form.addEventListener('submit', (e) => {
 | 
			
		||||
            e.preventDefault();
 | 
			
		||||
            const user = {
 | 
			
		||||
                name: userNameField.value,
 | 
			
		||||
                password: passwordField.value,
 | 
			
		||||
                email: emailField.value,
 | 
			
		||||
            };
 | 
			
		||||
            settings.onRegistered(user);
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
module.exports = RegistrationView;
 | 
			
		||||
							
								
								
									
										30
									
								
								static/js/views/top_navigation_view.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								static/js/views/top_navigation_view.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,30 @@
 | 
			
		||||
'use strict';
 | 
			
		||||
 | 
			
		||||
const BaseView = require('./base_view.js');
 | 
			
		||||
 | 
			
		||||
class TopNavigationView extends BaseView {
 | 
			
		||||
    constructor(handlebars) {
 | 
			
		||||
        super(handlebars);
 | 
			
		||||
        this.template = this.getTemplate('top-nav-template');
 | 
			
		||||
        this.navHolder = document.getElementById('top-nav-holder');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    render(items) {
 | 
			
		||||
        this.navHolder.innerHTML = this.template({items: items});
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    activate(itemName) {
 | 
			
		||||
        const allItemsSelector = '#top-nav-holder [data-name]';
 | 
			
		||||
        const currentItemSelector =
 | 
			
		||||
            '#top-nav-holder [data-name="' + itemName + '"]';
 | 
			
		||||
        for (let item of document.querySelectorAll(allItemsSelector)) {
 | 
			
		||||
            item.className = '';
 | 
			
		||||
        }
 | 
			
		||||
        const currentItem = document.querySelectorAll(currentItemSelector);
 | 
			
		||||
        if (currentItem.length > 0) {
 | 
			
		||||
            currentItem[0].className = 'active';
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
module.exports = TopNavigationView;
 | 
			
		||||
							
								
								
									
										33
									
								
								static/prepare_config.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								static/prepare_config.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,33 @@
 | 
			
		||||
'use strict';
 | 
			
		||||
 | 
			
		||||
const fs = require('fs');
 | 
			
		||||
const ini = require('ini');
 | 
			
		||||
const merge = require('merge');
 | 
			
		||||
const camelcaseKeys = require('camelcase-keys');
 | 
			
		||||
 | 
			
		||||
function parseIniFile(path) {
 | 
			
		||||
    let result = ini.parse(fs.readFileSync(path, 'utf-8')
 | 
			
		||||
        .replace(/#.+$/gm, '')
 | 
			
		||||
        .replace(/\s+$/gm, ''));
 | 
			
		||||
    Object.keys(result).map((key, _) => {
 | 
			
		||||
        result[key] = camelcaseKeys(result[key]);
 | 
			
		||||
    });
 | 
			
		||||
    return result;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
let config = parseIniFile('./config.ini.dist');
 | 
			
		||||
 | 
			
		||||
try {
 | 
			
		||||
    const localConfig = parseIniFile('./config.ini');
 | 
			
		||||
    config = merge.recursive(config, localConfig);
 | 
			
		||||
} catch (e) {
 | 
			
		||||
    console.warn('Local config does not exist, ignoring');
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
delete config.basic.secret;
 | 
			
		||||
delete config.smtp;
 | 
			
		||||
delete config.database;
 | 
			
		||||
config.service.userRanks = config.service.userRanks.split(/,\s*/);
 | 
			
		||||
config.service.tagCategories = config.service.tagCategories.split(/,\s*/);
 | 
			
		||||
 | 
			
		||||
fs.writeFileSync('./static/js/.config.autogen.json', JSON.stringify(config));
 | 
			
		||||
							
								
								
									
										0
									
								
								szurubooru/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								szurubooru/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										40
									
								
								szurubooru/app.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								szurubooru/app.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,40 @@
 | 
			
		||||
import os
 | 
			
		||||
import falcon
 | 
			
		||||
import sqlalchemy
 | 
			
		||||
import sqlalchemy.orm
 | 
			
		||||
import szurubooru.rest.users
 | 
			
		||||
from szurubooru.config import Config
 | 
			
		||||
from szurubooru.middleware import Authenticator, JsonTranslator, RequireJson
 | 
			
		||||
from szurubooru.services import AuthService, UserService
 | 
			
		||||
 | 
			
		||||
def create_app():
 | 
			
		||||
    config = Config()
 | 
			
		||||
    root_dir = os.path.dirname(__file__)
 | 
			
		||||
    static_dir = os.path.join(root_dir, os.pardir, 'static')
 | 
			
		||||
 | 
			
		||||
    engine = sqlalchemy.create_engine(
 | 
			
		||||
        '{schema}://{user}:{password}@{host}:{port}/{name}'.format(
 | 
			
		||||
            schema=config['database']['schema'],
 | 
			
		||||
            user=config['database']['user'],
 | 
			
		||||
            password=config['database']['pass'],
 | 
			
		||||
            host=config['database']['host'],
 | 
			
		||||
            port=config['database']['port'],
 | 
			
		||||
            name=config['database']['name']))
 | 
			
		||||
    session = sqlalchemy.orm.sessionmaker(bind=engine)()
 | 
			
		||||
 | 
			
		||||
    user_service = UserService(session)
 | 
			
		||||
    auth_service = AuthService(config, user_service)
 | 
			
		||||
 | 
			
		||||
    user_list = szurubooru.rest.users.UserList(auth_service)
 | 
			
		||||
    user = szurubooru.rest.users.User(auth_service)
 | 
			
		||||
 | 
			
		||||
    app = falcon.API(middleware=[
 | 
			
		||||
        RequireJson(),
 | 
			
		||||
        JsonTranslator(),
 | 
			
		||||
        Authenticator(auth_service),
 | 
			
		||||
    ])
 | 
			
		||||
 | 
			
		||||
    app.add_route('/users/', user_list)
 | 
			
		||||
    app.add_route('/user/{user_id}', user)
 | 
			
		||||
 | 
			
		||||
    return app
 | 
			
		||||
							
								
								
									
										11
									
								
								szurubooru/config.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								szurubooru/config.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,11 @@
 | 
			
		||||
import os
 | 
			
		||||
import configobj
 | 
			
		||||
 | 
			
		||||
class Config(object):
 | 
			
		||||
    def __init__(self):
 | 
			
		||||
        self.config = configobj.ConfigObj('config.ini.dist')
 | 
			
		||||
        if os.path.exists('config.ini'):
 | 
			
		||||
            self.config.merge(configobj.ConfigObj('config.ini'))
 | 
			
		||||
 | 
			
		||||
    def __getitem__(self, key):
 | 
			
		||||
        return self.config[key]
 | 
			
		||||
							
								
								
									
										3
									
								
								szurubooru/middleware/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								szurubooru/middleware/__init__.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,3 @@
 | 
			
		||||
from szurubooru.middleware.authenticator import Authenticator
 | 
			
		||||
from szurubooru.middleware.json_translator import JsonTranslator
 | 
			
		||||
from szurubooru.middleware.require_json import RequireJson
 | 
			
		||||
							
								
								
									
										32
									
								
								szurubooru/middleware/authenticator.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								szurubooru/middleware/authenticator.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,32 @@
 | 
			
		||||
import base64
 | 
			
		||||
import falcon
 | 
			
		||||
 | 
			
		||||
class Authenticator(object):
 | 
			
		||||
    def __init__(self, auth_service):
 | 
			
		||||
        self._auth_service = auth_service
 | 
			
		||||
 | 
			
		||||
    def process_request(self, request, response):
 | 
			
		||||
        request.context['user'] = self._get_user(request)
 | 
			
		||||
 | 
			
		||||
    def _get_user(self, request):
 | 
			
		||||
        if not request.auth:
 | 
			
		||||
            return self._auth_service.authenticate(None, None)
 | 
			
		||||
 | 
			
		||||
        try:
 | 
			
		||||
            auth_type, user_and_password = request.auth.split(' ', 1)
 | 
			
		||||
 | 
			
		||||
            if auth_type.lower() != 'basic':
 | 
			
		||||
                raise falcon.HTTPBadRequest(
 | 
			
		||||
                    'Invalid authentication type',
 | 
			
		||||
                    'Only basic authorization is supported.')
 | 
			
		||||
 | 
			
		||||
            username, password = base64.decodestring(
 | 
			
		||||
                user_and_password.encode('ascii')).decode('utf8').split(':')
 | 
			
		||||
 | 
			
		||||
            return self._auth_service.authenticate(username, password)
 | 
			
		||||
        except ValueError as err:
 | 
			
		||||
            msg = 'Basic authentication header value not properly formed. ' \
 | 
			
		||||
                + 'Supplied header {0}. Got error: {1}'
 | 
			
		||||
            raise falcon.HTTPBadRequest(
 | 
			
		||||
                'Malformed authentication request',
 | 
			
		||||
                msg.format(request.auth, str(err)))
 | 
			
		||||
							
								
								
									
										27
									
								
								szurubooru/middleware/json_translator.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								szurubooru/middleware/json_translator.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,27 @@
 | 
			
		||||
import json
 | 
			
		||||
import falcon
 | 
			
		||||
 | 
			
		||||
class JsonTranslator(object):
 | 
			
		||||
    def process_request(self, request, response):
 | 
			
		||||
        if request.content_length in (None, 0):
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        body = request.stream.read()
 | 
			
		||||
        if not body:
 | 
			
		||||
            raise falcon.HTTPBadRequest(
 | 
			
		||||
                'Empty request body',
 | 
			
		||||
                'A valid JSON document is required.')
 | 
			
		||||
 | 
			
		||||
        try:
 | 
			
		||||
            request.context['doc'] = json.loads(body.decode('utf-8'))
 | 
			
		||||
        except (ValueError, UnicodeDecodeError):
 | 
			
		||||
            raise falcon.HTTPError(
 | 
			
		||||
                falcon.HTTP_401,
 | 
			
		||||
                'Malformed JSON',
 | 
			
		||||
                'Could not decode the request body. The '
 | 
			
		||||
                'JSON was incorrect or not encoded as UTF-8.')
 | 
			
		||||
 | 
			
		||||
    def process_response(self, request, response, resource):
 | 
			
		||||
        if 'result' not in request.context:
 | 
			
		||||
            return
 | 
			
		||||
        response.body = json.dumps(request.context['result'])
 | 
			
		||||
							
								
								
									
										7
									
								
								szurubooru/middleware/require_json.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								szurubooru/middleware/require_json.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,7 @@
 | 
			
		||||
import falcon
 | 
			
		||||
 | 
			
		||||
class RequireJson(object):
 | 
			
		||||
    def process_request(self, req, resp):
 | 
			
		||||
        if not req.client_accepts_json:
 | 
			
		||||
            raise falcon.HTTPNotAcceptable(
 | 
			
		||||
                'This API only supports responses encoded as JSON.')
 | 
			
		||||
							
								
								
									
										74
									
								
								szurubooru/migrations/env.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										74
									
								
								szurubooru/migrations/env.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,74 @@
 | 
			
		||||
import os
 | 
			
		||||
import sys
 | 
			
		||||
 | 
			
		||||
import alembic
 | 
			
		||||
import sqlalchemy
 | 
			
		||||
import logging.config
 | 
			
		||||
 | 
			
		||||
# make szurubooru module importable
 | 
			
		||||
dir_to_self = os.path.dirname(os.path.realpath(__file__))
 | 
			
		||||
sys.path.append(os.path.join(dir_to_self, *[os.pardir] * 2))
 | 
			
		||||
 | 
			
		||||
import szurubooru.model.base
 | 
			
		||||
import szurubooru.config
 | 
			
		||||
 | 
			
		||||
alembic_config = alembic.context.config
 | 
			
		||||
logging.config.fileConfig(alembic_config.config_file_name)
 | 
			
		||||
 | 
			
		||||
szuru_config = szurubooru.config.Config()
 | 
			
		||||
alembic_config.set_main_option(
 | 
			
		||||
    'sqlalchemy.url',
 | 
			
		||||
    '{schema}://{user}:{password}@{host}:{port}/{name}'.format(
 | 
			
		||||
        schema=szuru_config['database']['schema'],
 | 
			
		||||
        user=szuru_config['database']['user'],
 | 
			
		||||
        password=szuru_config['database']['pass'],
 | 
			
		||||
        host=szuru_config['database']['host'],
 | 
			
		||||
        port=szuru_config['database']['port'],
 | 
			
		||||
        name=szuru_config['database']['name']))
 | 
			
		||||
 | 
			
		||||
target_metadata = szurubooru.model.Base.metadata
 | 
			
		||||
 | 
			
		||||
def run_migrations_offline():
 | 
			
		||||
    '''
 | 
			
		||||
    Run migrations in 'offline' mode.
 | 
			
		||||
 | 
			
		||||
    This configures the context with just a URL
 | 
			
		||||
    and not an Engine, though an Engine is acceptable
 | 
			
		||||
    here as well.  By skipping the Engine creation
 | 
			
		||||
    we don't even need a DBAPI to be available.
 | 
			
		||||
 | 
			
		||||
    Calls to context.execute() here emit the given string to the
 | 
			
		||||
    script output.
 | 
			
		||||
    '''
 | 
			
		||||
    url = alembic_config.get_main_option('sqlalchemy.url')
 | 
			
		||||
    alembic.context.configure(
 | 
			
		||||
        url=url, target_metadata=target_metadata, literal_binds=True)
 | 
			
		||||
 | 
			
		||||
    with alembic.context.begin_transaction():
 | 
			
		||||
        alembic.context.run_migrations()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def run_migrations_online():
 | 
			
		||||
    '''
 | 
			
		||||
    Run migrations in 'online' mode.
 | 
			
		||||
 | 
			
		||||
    In this scenario we need to create an Engine
 | 
			
		||||
    and associate a connection with the context.
 | 
			
		||||
    '''
 | 
			
		||||
    connectable = sqlalchemy.engine_from_config(
 | 
			
		||||
        alembic_config.get_section(alembic_config.config_ini_section),
 | 
			
		||||
        prefix='sqlalchemy.',
 | 
			
		||||
        poolclass=sqlalchemy.pool.NullPool)
 | 
			
		||||
 | 
			
		||||
    with connectable.connect() as connection:
 | 
			
		||||
        alembic.context.configure(
 | 
			
		||||
            connection=connection,
 | 
			
		||||
            target_metadata=target_metadata)
 | 
			
		||||
 | 
			
		||||
        with alembic.context.begin_transaction():
 | 
			
		||||
            alembic.context.run_migrations()
 | 
			
		||||
 | 
			
		||||
if alembic.context.is_offline_mode():
 | 
			
		||||
    run_migrations_offline()
 | 
			
		||||
else:
 | 
			
		||||
    run_migrations_online()
 | 
			
		||||
							
								
								
									
										21
									
								
								szurubooru/migrations/script.py.mako
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								szurubooru/migrations/script.py.mako
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,21 @@
 | 
			
		||||
'''
 | 
			
		||||
${message}
 | 
			
		||||
 | 
			
		||||
Revision ID: ${up_revision}
 | 
			
		||||
Created at: ${create_date}
 | 
			
		||||
'''
 | 
			
		||||
 | 
			
		||||
import sqlalchemy as sa
 | 
			
		||||
from alembic import op
 | 
			
		||||
${imports if imports else ""}
 | 
			
		||||
 | 
			
		||||
revision = ${repr(up_revision)}
 | 
			
		||||
down_revision = ${repr(down_revision)}
 | 
			
		||||
branch_labels = ${repr(branch_labels)}
 | 
			
		||||
depends_on = ${repr(depends_on)}
 | 
			
		||||
 | 
			
		||||
def upgrade():
 | 
			
		||||
    ${upgrades if upgrades else "pass"}
 | 
			
		||||
 | 
			
		||||
def downgrade():
 | 
			
		||||
    ${downgrades if downgrades else "pass"}
 | 
			
		||||
@ -0,0 +1,31 @@
 | 
			
		||||
'''
 | 
			
		||||
Create user table
 | 
			
		||||
 | 
			
		||||
Revision ID: e5c1216a8503
 | 
			
		||||
Created at: 2016-03-20 15:53:25.030415
 | 
			
		||||
'''
 | 
			
		||||
 | 
			
		||||
import sqlalchemy as sa
 | 
			
		||||
from alembic import op
 | 
			
		||||
 | 
			
		||||
revision = 'e5c1216a8503'
 | 
			
		||||
down_revision = None
 | 
			
		||||
branch_labels = None
 | 
			
		||||
depends_on = None
 | 
			
		||||
 | 
			
		||||
def upgrade():
 | 
			
		||||
    op.create_table(
 | 
			
		||||
        'user',
 | 
			
		||||
        sa.Column('id', sa.Integer(), nullable=False),
 | 
			
		||||
        sa.Column('name', sa.String(length=50), nullable=False),
 | 
			
		||||
        sa.Column('password_hash', sa.String(length=64), nullable=False),
 | 
			
		||||
        sa.Column('pasword_salt', sa.String(length=32), nullable=True),
 | 
			
		||||
        sa.Column('email', sa.String(length=200), nullable=True),
 | 
			
		||||
        sa.Column('access_rank', sa.Integer(), nullable=False),
 | 
			
		||||
        sa.Column('creation_time', sa.DateTime(), nullable=False),
 | 
			
		||||
        sa.Column('last_login_time', sa.DateTime(), nullable=False),
 | 
			
		||||
        sa.Column('avatar_style', sa.Integer(), nullable=False),
 | 
			
		||||
        sa.PrimaryKeyConstraint('id'))
 | 
			
		||||
 | 
			
		||||
def downgrade():
 | 
			
		||||
    op.drop_table('user')
 | 
			
		||||
							
								
								
									
										2
									
								
								szurubooru/model/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								szurubooru/model/__init__.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,2 @@
 | 
			
		||||
from szurubooru.model.base import Base
 | 
			
		||||
from szurubooru.model.user import User
 | 
			
		||||
							
								
								
									
										2
									
								
								szurubooru/model/base.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								szurubooru/model/base.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,2 @@
 | 
			
		||||
from sqlalchemy.ext.declarative import declarative_base
 | 
			
		||||
Base = declarative_base()  # pylint: disable=C0103
 | 
			
		||||
							
								
								
									
										18
									
								
								szurubooru/model/user.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								szurubooru/model/user.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,18 @@
 | 
			
		||||
import sqlalchemy as sa
 | 
			
		||||
from szurubooru.model.base import Base
 | 
			
		||||
 | 
			
		||||
class User(Base):
 | 
			
		||||
    __tablename__ = 'user'
 | 
			
		||||
 | 
			
		||||
    user_id = sa.Column('id', sa.Integer, primary_key=True)
 | 
			
		||||
    name = sa.Column('name', sa.String(50), nullable=False)
 | 
			
		||||
    password_hash = sa.Column('password_hash', sa.String(64), nullable=False)
 | 
			
		||||
    password_salt = sa.Column('pasword_salt', sa.String(32))
 | 
			
		||||
    email = sa.Column('email', sa.String(200), nullable=True)
 | 
			
		||||
    access_rank = sa.Column('access_rank', sa.Integer, nullable=False)
 | 
			
		||||
    creation_time = sa.Column('creation_time', sa.DateTime, nullable=False)
 | 
			
		||||
    last_login_time = sa.Column('last_login_time', sa.DateTime, nullable=False)
 | 
			
		||||
    avatar_style = sa.Column('avatar_style', sa.Integer, nullable=False)
 | 
			
		||||
 | 
			
		||||
    def has_password(self, password):
 | 
			
		||||
        return self.password == password
 | 
			
		||||
							
								
								
									
										0
									
								
								szurubooru/rest/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								szurubooru/rest/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										23
									
								
								szurubooru/rest/users.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								szurubooru/rest/users.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,23 @@
 | 
			
		||||
class UserList(object):
 | 
			
		||||
    def __init__(self, auth_service):
 | 
			
		||||
        self._auth_service = auth_service
 | 
			
		||||
 | 
			
		||||
    def on_get(self, request, response):
 | 
			
		||||
        self._auth_service.verify_privilege(request.context['user'], 'users:list')
 | 
			
		||||
        request.context['reuslt'] = {'message': 'Searching for users'}
 | 
			
		||||
 | 
			
		||||
    def on_post(self, request, response):
 | 
			
		||||
        self._auth_service.verify_privilege(request.context['user'], 'users:create')
 | 
			
		||||
        request.context['result'] = {'message': 'Creating user'}
 | 
			
		||||
 | 
			
		||||
class User(object):
 | 
			
		||||
    def __init__(self, auth_service):
 | 
			
		||||
        self._auth_service = auth_service
 | 
			
		||||
 | 
			
		||||
    def on_get(self, request, response, user_id):
 | 
			
		||||
        self._auth_service.verify_privilege(request.context['user'], 'users:view')
 | 
			
		||||
        request.context['result'] = {'message': 'Getting user ' + user_id}
 | 
			
		||||
 | 
			
		||||
    def on_put(self, request, response, user_id):
 | 
			
		||||
        self._auth_service.verify_privilege(request.context['user'], 'users:edit')
 | 
			
		||||
        request.context['result'] = {'message': 'Updating user ' + user_id}
 | 
			
		||||
							
								
								
									
										2
									
								
								szurubooru/services/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								szurubooru/services/__init__.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,2 @@
 | 
			
		||||
from szurubooru.services.auth_service import AuthService
 | 
			
		||||
from szurubooru.services.user_service import UserService
 | 
			
		||||
							
								
								
									
										39
									
								
								szurubooru/services/auth_service.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								szurubooru/services/auth_service.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,39 @@
 | 
			
		||||
import falcon
 | 
			
		||||
from szurubooru.model.user import User
 | 
			
		||||
 | 
			
		||||
class AuthService(object):
 | 
			
		||||
    def __init__(self, config, user_service):
 | 
			
		||||
        self._config = config
 | 
			
		||||
        self._user_service = user_service
 | 
			
		||||
 | 
			
		||||
    def authenticate(self, username, password):
 | 
			
		||||
        if not username:
 | 
			
		||||
            return self._create_anonymous_user()
 | 
			
		||||
        user = self._user_service.get_by_name(username)
 | 
			
		||||
        if not user:
 | 
			
		||||
            raise falcon.HTTPForbidden(
 | 
			
		||||
                'Authentication failed', 'No such user.')
 | 
			
		||||
        if not user.has_password(password):
 | 
			
		||||
            raise falcon.HTTPForbidden(
 | 
			
		||||
                'Authentication failed', 'Invalid password.')
 | 
			
		||||
        return user
 | 
			
		||||
 | 
			
		||||
    def verify_privilege(self, user, privilege_name):
 | 
			
		||||
        all_ranks = ['anonymous'] \
 | 
			
		||||
            + self._config['service']['user_ranks'] \
 | 
			
		||||
            + ['admin', 'nobody']
 | 
			
		||||
 | 
			
		||||
        assert privilege_name in self._config['privileges']
 | 
			
		||||
        assert user.rank in all_ranks
 | 
			
		||||
        minimal_rank = self._config['privileges'][privilege_name]
 | 
			
		||||
        good_ranks = all_ranks[all_ranks.index(minimal_rank):]
 | 
			
		||||
        if user.rank not in good_ranks:
 | 
			
		||||
            raise falcon.HTTPForbidden(
 | 
			
		||||
                'Authentication failed', 'Insufficient privileges to do this.')
 | 
			
		||||
 | 
			
		||||
    def _create_anonymous_user(self):
 | 
			
		||||
        user = User()
 | 
			
		||||
        user.name = None
 | 
			
		||||
        user.rank = 'anonymous'
 | 
			
		||||
        user.password = None
 | 
			
		||||
        return user
 | 
			
		||||
							
								
								
									
										8
									
								
								szurubooru/services/user_service.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								szurubooru/services/user_service.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,8 @@
 | 
			
		||||
from szurubooru.model.user import User
 | 
			
		||||
 | 
			
		||||
class UserService(object):
 | 
			
		||||
    def __init__(self, session):
 | 
			
		||||
        self._session = session
 | 
			
		||||
 | 
			
		||||
    def get_by_name(self, name):
 | 
			
		||||
        self._session.query(User).filter_by(name=name).first()
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user