From 483c32cfbfd3debadbec447621302dd7d694f849 Mon Sep 17 00:00:00 2001 From: ReAnzu Date: Sun, 25 Feb 2018 04:44:02 -0600 Subject: [PATCH 01/33] User Token Authentication * Users are only authenticated against their password on login, and to retrieve a token. * Passwords are wiped from the app and cookies after login and token retrieval * Tokens are revoked at the end of the session/logout * If the user chooses the "remember me" option, the token is stored in the cookie * A user interface to revoke tokens will be added * Tokens correctly delete themselves on logout * API documentation updated for the new user-token endpoints * Added a Manage tokens tab to the user panel * Added bullet point about the token authentication for the API * Added tests for new endpoints and tests against authentication middleware --- .gitignore | 3 + API.md | 160 +++++++++++++++++- README.md | 1 + client/css/user-view.styl | 20 ++- client/html/user.tpl | 3 + client/html/user_tokens.tpl | 29 ++++ client/js/api.js | 85 +++++++++- client/js/controllers/user_controller.js | 78 ++++++++- client/js/models/user_token.js | 76 +++++++++ client/js/views/user_tokens_view.js | 86 ++++++++++ client/js/views/user_view.js | 12 +- config.yaml.dist | 9 + server/szurubooru/api/__init__.py | 1 + server/szurubooru/api/user_token_api.py | 60 +++++++ server/szurubooru/func/auth.py | 9 + server/szurubooru/func/user_tokens.py | 86 ++++++++++ server/szurubooru/func/users.py | 18 +- server/szurubooru/middleware/authenticator.py | 30 +++- .../a39c7f98a7fa_add_user_token_table.py | 35 ++++ server/szurubooru/model/__init__.py | 4 +- server/szurubooru/model/user.py | 19 +++ server/szurubooru/rest/__init__.py | 1 + .../tests/api/test_user_token_creating.py | 29 ++++ .../tests/api/test_user_token_deleting.py | 29 ++++ .../tests/api/test_user_token_retrieving.py | 31 ++++ .../tests/api/test_user_token_updating.py | 41 +++++ server/szurubooru/tests/conftest.py | 19 ++- server/szurubooru/tests/func/test_auth.py | 10 ++ .../szurubooru/tests/func/test_user_tokens.py | 72 ++++++++ .../szurubooru/tests/middleware/__init__.py | 0 .../tests/middleware/test_authenticator.py | 48 ++++++ .../szurubooru/tests/model/test_user_token.py | 14 ++ 32 files changed, 1086 insertions(+), 32 deletions(-) create mode 100644 client/html/user_tokens.tpl create mode 100644 client/js/models/user_token.js create mode 100644 client/js/views/user_tokens_view.js create mode 100644 server/szurubooru/api/user_token_api.py create mode 100644 server/szurubooru/func/user_tokens.py create mode 100644 server/szurubooru/migrations/versions/a39c7f98a7fa_add_user_token_table.py create mode 100644 server/szurubooru/tests/api/test_user_token_creating.py create mode 100644 server/szurubooru/tests/api/test_user_token_deleting.py create mode 100644 server/szurubooru/tests/api/test_user_token_retrieving.py create mode 100644 server/szurubooru/tests/api/test_user_token_updating.py create mode 100644 server/szurubooru/tests/func/test_user_tokens.py create mode 100644 server/szurubooru/tests/middleware/__init__.py create mode 100644 server/szurubooru/tests/middleware/test_authenticator.py create mode 100644 server/szurubooru/tests/model/test_user_token.py diff --git a/.gitignore b/.gitignore index a683cefa..30cd78cf 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,6 @@ config.yaml */*_modules/ .coverage .cache +__pycache__ +.idea/ +*.iml \ No newline at end of file diff --git a/API.md b/API.md index 62e0eb22..ae2bb6b3 100644 --- a/API.md +++ b/API.md @@ -7,6 +7,7 @@ 1. [General rules](#general-rules) - [Authentication](#authentication) + - [User token authentication](#user-token-authentication) - [Basic requests](#basic-requests) - [File uploads](#file-uploads) - [Error handling](#error-handling) @@ -56,6 +57,11 @@ - [Updating user](#updating-user) - [Getting user](#getting-user) - [Deleting user](#deleting-user) + - User Tokens + - [Listing tokens](#listing-tokens) + - [Creating token](#creating-token) + - [Updating token](#updating-token) + - [Deleting token](#deleting-token) - Password reset - [Password reset - step 1: mail request](#password-reset---step-2-confirmation) - [Password reset - step 2: confirmation](#password-reset---step-2-confirmation) @@ -70,6 +76,7 @@ - [User](#user) - [Micro user](#micro-user) + - [User token](#user-token) - [Tag category](#tag-category) - [Tag](#tag) - [Micro tag](#micro-tag) @@ -91,16 +98,35 @@ ## Authentication Authentication is achieved by means of [basic HTTP -auth](https://en.wikipedia.org/wiki/Basic_access_authentication). For this -reason, it is recommended to connect through HTTPS. There are no sessions, so -every privileged request must be authenticated. Available privileges depend on -the user's rank. The way how rank translates to privileges is defined in the -server's configuration. +auth](https://en.wikipedia.org/wiki/Basic_access_authentication) or through the +use of [user token authentication](#user-token-authentication). For this reason, +it is recommended to connect through HTTPS. There are no sessions, so every +privileged request must be authenticated. Available privileges depend on the +user's rank. The way how rank translates to privileges is defined in the server's +configuration. It is recommended to add `?bump-login` GET parameter to the first request in a client "session" (where the definition of a session is up to the client), so that the user's last login time is kept up to date. +## User token authentication + +User token authentication works similarly to [basic HTTP +auth](https://en.wikipedia.org/wiki/Basic_access_authentication). Because it +operates similarly to ***basic HTTP auth*** it is still recommended to connect +through HTTPS. The authorization header uses the type of Token and the username +and token are encoded as Base64 and sent as the second parameter. + +Example header for user1:token-is-more-secure +``` +Authorization: Token dXNlcjE6dG9rZW4taXMtbW9yZS1zZWN1cmU= +``` + +The benefit of token authentication is that beyond the initial login to acquire +the first token, there is no need to transmit the user password in plaintext via +basic auth. Additionally tokens can be revoked at anytime allowing a cleaner +interface for isolating clients from user credentials. + ## Basic requests Every request must use `Content-Type: application/json` and `Accept: @@ -1469,6 +1495,104 @@ data. Deletes existing user. +## Listing tokens +- **Request** + + `GET /user-tokens/` + +- **Output** + + An [unpaged search result resource](#unpaged-search-result), for which + `` is a [user token resource](#user-token). + +- **Errors** + + - privileges are too low + +- **Description** + + Searches for users tokens for the currently logged in user. + +## Creating token +- **Request** + + `POST /user-token` + +- **Input** + + ```json5 + {} + ``` + +- **Output** + + A [user token resource](#user-token). + +- **Errors** + + - privileges are too low + +- **Description** + + Creates a new user token that can be used for authentication of api + endpoints instead of a password. + +## Updating user +- **Request** + + `PUT /user-token/` + +- **Input** + + ```json5 + { + "version": , + "enabled": , // optional + } + ``` + +- **Output** + + A [user token resource](#user-token). + +- **Errors** + + - the version is outdated + - the user token does not exist + - privileges are too low + +- **Description** + + Updates an existing user token using specified parameters. All fields + except the [`version`](#versioning) are optional - update concerns only + provided fields. + +## Deleting token +- **Request** + + `DELETE /user-token/` + +- **Input** + + ```json5 + {} + ``` + +- **Output** + + ```json5 + {} + ``` + +- **Errors** + + - the token does not exist + - privileges are too low + +- **Description** + + Deletes existing user token. + ## Password reset - step 1: mail request - **Request** @@ -1701,6 +1825,32 @@ A single user. A [user resource](#user) stripped down to `name` and `avatarUrl` fields. +## User token +**Description** + +A single user token. + +**Structure** + +```json5 +{ + "user": , + "token": , + "enabled": , + "version": , + "creationTime": , + "lastEditTime": , +} +``` + +**Field meaning** +- ``: micro user. See [micro user](#micro-user). +- ``: the token that can be used to authenticate the user. +- ``: whether the token is still valid for authentication. +- ``: resource version. See [versioning](#versioning). +- ``: time the user token was created , formatted as per RFC 3339. +- ``: time the user token was edited, formatted as per RFC 3339. + ## Tag category **Description** diff --git a/README.md b/README.md index ceb91093..3b5c4e44 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ scrubbing](http://sjp.pwn.pl/sjp/;2527372). It is pronounced as *shoorubooru*. - Post comments - Post notes / annotations, including arbitrary polygons - Rich JSON REST API ([see documentation](https://github.com/rr-/szurubooru/blob/master/API.md)) +- Token based authentication for clients - Rich search system - Rich privilege system - Autocomplete in search and while editing tags diff --git a/client/css/user-view.styl b/client/css/user-view.styl index 12cba75e..46f3044a 100644 --- a/client/css/user-view.styl +++ b/client/css/user-view.styl @@ -1,6 +1,6 @@ #user width: 100% - max-width: 35em + max-width: 45em nav.text-nav margin-bottom: 1.5em @@ -37,6 +37,24 @@ height: 1px clear: both + #user-tokens + .token-flex-container + width: 100% + display: flex; + flex-direction column; + padding-bottom: 0.5em; + + .token-flex-row + display: flex; + flex-direction: row; + justify-content: space-between; + padding-top: 0.25em; + padding-bottom: 0.25em; + border-bottom: black solid 1px; + + form + width: auto; + #user-delete form width: 100% diff --git a/client/html/user.tpl b/client/html/user.tpl index 28e34e67..cbf4f18f 100644 --- a/client/html/user.tpl +++ b/client/html/user.tpl @@ -6,6 +6,9 @@ --><% if (ctx.canEditAnything) { %>
  • '>Account settings
  • <% } %><% if (ctx.canListTokens) { %>
  • '>Manage tokens
  • <% } %><% if (ctx.canDelete) { %>
  • '>Account deletion
  • <% } %> diff --git a/client/html/user_tokens.tpl b/client/html/user_tokens.tpl index 85996f0f..8a37aca2 100644 --- a/client/html/user_tokens.tpl +++ b/client/html/user_tokens.tpl @@ -9,6 +9,7 @@
    Note:
    Created:
    Expires:
    +
    Used:
    <%= token.token %>
    @@ -19,6 +20,7 @@ <% } else { %>
    No expiration
    <% } %> +
    <%= ctx.makeRelativeTime(token.lastUsageTime) %>
    diff --git a/client/js/models/user_token.js b/client/js/models/user_token.js index 613eb4b4..fcb374f4 100644 --- a/client/js/models/user_token.js +++ b/client/js/models/user_token.js @@ -17,6 +17,8 @@ class UserToken extends events.EventTarget { get version() { return this._version; } get expirationTime() { return this._expirationTime; } get creationTime() { return this._creationTime; } + get lastEditTime() { return this._lastEditTime; } + get lastUsageTime() { return this._lastUsageTime; } static fromResponse(response) { if (typeof response.results !== 'undefined') { @@ -73,12 +75,14 @@ class UserToken extends events.EventTarget { _updateFromResponse(response) { const map = { - _token: response.token, - _note: response.note, - _enabled: response.enabled, - _expirationTime: response.expirationTime, - _version: response.version, - _creationTime: response.creationTime, + _token: response.token, + _note: response.note, + _enabled: response.enabled, + _expirationTime: response.expirationTime, + _version: response.version, + _creationTime: response.creationTime, + _lastEditTime: response.lastEditTime, + _lastUsageTime: response.lastUsageTime, }; Object.assign(this, map); diff --git a/server/szurubooru/func/user_tokens.py b/server/szurubooru/func/user_tokens.py index e9d926c5..8cd93dab 100644 --- a/server/szurubooru/func/user_tokens.py +++ b/server/szurubooru/func/user_tokens.py @@ -35,6 +35,7 @@ class UserTokenSerializer(serialization.BaseSerializer): 'expirationTime': self.serialize_expiration_time, 'creationTime': self.serialize_creation_time, 'lastEditTime': self.serialize_last_edit_time, + 'lastUsageTime': self.serialize_last_usage_time, 'version': self.serialize_version, } @@ -47,6 +48,9 @@ class UserTokenSerializer(serialization.BaseSerializer): def serialize_last_edit_time(self) -> Any: return self.user_token.last_edit_time + def serialize_last_usage_time(self) -> Any: + return self.user_token.last_usage_time + def serialize_token(self) -> Any: return self.user_token.token @@ -98,6 +102,7 @@ def create_user_token(user: model.User, enabled: bool) -> model.UserToken: user_token.token = auth.generate_authorization_token() user_token.enabled = enabled user_token.creation_time = datetime.utcnow() + user_token.last_usage_time = datetime.utcnow() return user_token @@ -107,6 +112,7 @@ def update_user_token_enabled( if enabled is None: raise InvalidEnabledError('Enabled cannot be empty.') user_token.enabled = enabled + update_user_token_edit_time(user_token) def update_user_token_edit_time(user_token: model.UserToken) -> None: @@ -124,6 +130,7 @@ def update_user_token_expiration_time( raise InvalidExpirationError( 'Expiration cannot happen in the past') user_token.expiration_time = expiration_time + update_user_token_edit_time(user_token) except ValueError: raise InvalidExpirationError( 'Expiration is in an invalid format {}'.format( @@ -136,3 +143,9 @@ def update_user_token_note(user_token: model.UserToken, note: str) -> None: if util.value_exceeds_column_size(note, model.UserToken.note): raise InvalidNoteError('Note is too long.') user_token.note = note + update_user_token_edit_time(user_token) + + +def bump_usage_time(user_token: model.UserToken) -> None: + assert user_token + user_token.last_usage_time = datetime.utcnow() diff --git a/server/szurubooru/middleware/authenticator.py b/server/szurubooru/middleware/authenticator.py index cf8555e5..b185dad6 100644 --- a/server/szurubooru/middleware/authenticator.py +++ b/server/szurubooru/middleware/authenticator.py @@ -1,5 +1,5 @@ import base64 -from typing import Optional +from typing import Optional, Tuple from szurubooru import db, model, errors, rest from szurubooru.func import auth, users, user_tokens from szurubooru.rest.errors import HttpBadRequest @@ -13,29 +13,32 @@ def _authenticate_basic_auth(username: str, password: str) -> model.User: return user -def _authenticate_token(username: str, token: str) -> model.User: +def _authenticate_token( + username: str, token: str) -> Tuple[model.User, model.UserToken]: ''' Try to authenticate user. Throw AuthError for invalid users. ''' user = users.get_user_by_name(username) user_token = user_tokens.get_by_user_and_token(user, token) if not auth.is_valid_token(user_token): raise errors.AuthError('Invalid token.') - return user + return user, user_token -def _get_user(ctx: rest.Context) -> Optional[model.User]: +def _get_user(ctx: rest.Context, bump_login: bool) -> Optional[model.User]: if not ctx.has_header('Authorization'): return None + auth_token = None + try: auth_type, credentials = ctx.get_header('Authorization').split(' ', 1) if auth_type.lower() == 'basic': username, password = base64.decodebytes( credentials.encode('ascii')).decode('utf8').split(':', 1) - return _authenticate_basic_auth(username, password) + auth_user = _authenticate_basic_auth(username, password) elif auth_type.lower() == 'token': username, token = base64.decodebytes( credentials.encode('ascii')).decode('utf8').split(':', 1) - return _authenticate_token(username, token) + auth_user, auth_token = _authenticate_token(username, token) else: raise HttpBadRequest( 'ValidationError', @@ -48,15 +51,21 @@ def _get_user(ctx: rest.Context) -> Optional[model.User]: 'ValidationError', msg.format(ctx.get_header('Authorization'), str(err))) + if bump_login and auth_user.user_id: + users.bump_user_login_time(auth_user) + if auth_token is not None: + user_tokens.bump_usage_time(auth_token) + ctx.session.commit() + + return auth_user + def process_request(ctx: rest.Context) -> None: ''' Bind the user to request. Update last login time if needed. ''' - auth_user = _get_user(ctx) + bump_login = ctx.get_param_as_bool('bump-login', default=False) + auth_user = _get_user(ctx, bump_login) if auth_user: ctx.user = auth_user - if ctx.get_param_as_bool('bump-login', default=False) and ctx.user.user_id: - users.bump_user_login_time(ctx.user) - ctx.session.commit() @rest.middleware.pre_hook diff --git a/server/szurubooru/migrations/versions/a39c7f98a7fa_add_user_token_table.py b/server/szurubooru/migrations/versions/a39c7f98a7fa_add_user_token_table.py index 53f7ff5b..6978e829 100644 --- a/server/szurubooru/migrations/versions/a39c7f98a7fa_add_user_token_table.py +++ b/server/szurubooru/migrations/versions/a39c7f98a7fa_add_user_token_table.py @@ -25,6 +25,7 @@ def upgrade(): sa.Column('expiration_time', sa.DateTime(), nullable=True), sa.Column('creation_time', sa.DateTime(), nullable=False), sa.Column('last_edit_time', sa.DateTime(), nullable=True), + sa.Column('last_usage_time', sa.DateTime(), nullable=True), sa.Column('version', sa.Integer(), nullable=False), sa.ForeignKeyConstraint( ['user_id'], diff --git a/server/szurubooru/model/user.py b/server/szurubooru/model/user.py index d5397189..2d599e85 100644 --- a/server/szurubooru/model/user.py +++ b/server/szurubooru/model/user.py @@ -104,6 +104,7 @@ class UserToken(Base): expiration_time = sa.Column('expiration_time', sa.DateTime, nullable=True) creation_time = sa.Column('creation_time', sa.DateTime, nullable=False) last_edit_time = sa.Column('last_edit_time', sa.DateTime) + last_usage_time = sa.Column('last_usage_time', sa.DateTime) version = sa.Column('version', sa.Integer, default=1, nullable=False) user = sa.orm.relationship('User') diff --git a/server/szurubooru/tests/func/test_user_tokens.py b/server/szurubooru/tests/func/test_user_tokens.py index 7e101ebc..8c3577c8 100644 --- a/server/szurubooru/tests/func/test_user_tokens.py +++ b/server/szurubooru/tests/func/test_user_tokens.py @@ -20,6 +20,7 @@ def test_serialize_user_token(user_token_factory): 'enabled': True, 'expirationTime': None, 'lastEditTime': None, + 'lastUsageTime': None, 'note': None, 'token': 'dummy', 'user': { @@ -71,6 +72,7 @@ def test_update_user_token_enabled(user_token_factory): user_token = user_token_factory() user_tokens.update_user_token_enabled(user_token, False) assert user_token.enabled is False + assert user_token.last_edit_time is not None def test_update_user_token_edit_time(user_token_factory): @@ -85,6 +87,7 @@ def test_update_user_token_note(user_token_factory): assert user_token.note is None user_tokens.update_user_token_note(user_token, ' Test Note ') assert user_token.note == 'Test Note' + assert user_token.last_edit_time is not None def test_update_user_token_note_input_too_long(user_token_factory): @@ -107,6 +110,7 @@ def test_update_user_token_expiration_time(user_token_factory): user_tokens.update_user_token_expiration_time( user_token, expiration_time_str) assert user_token.expiration_time.isoformat() == expiration_time_str + assert user_token.last_edit_time is not None def test_update_user_token_expiration_time_in_past(user_token_factory): @@ -142,3 +146,10 @@ def test_update_user_token_expiration_time_invalid_format( % expiration_time_str): user_tokens.update_user_token_expiration_time( user_token, expiration_time_str) + + +def test_bump_usage_time(user_token_factory, fake_datetime): + user_token = user_token_factory() + with fake_datetime('1997-01-01'): + user_tokens.bump_usage_time(user_token) + assert user_token.last_usage_time == datetime(1997, 1, 1) diff --git a/server/szurubooru/tests/middleware/test_authenticator.py b/server/szurubooru/tests/middleware/test_authenticator.py index 11b824d3..36e74adf 100644 --- a/server/szurubooru/tests/middleware/test_authenticator.py +++ b/server/szurubooru/tests/middleware/test_authenticator.py @@ -1,5 +1,6 @@ from unittest.mock import patch import pytest +from szurubooru import db from szurubooru.func import auth, users, user_tokens from szurubooru.middleware import authenticator from szurubooru.rest import errors @@ -11,6 +12,48 @@ def test_process_request_no_header(context_factory): assert ctx.user.name is None +def test_process_request_bump_login(context_factory, user_factory): + user = user_factory() + db.session.add(user) + db.session.flush() + ctx = context_factory( + headers={ + 'Authorization': "Basic dGVzdFVzZXI6dGVzdFRva2Vu" + }, + params={ + 'bump-login': 'true' + }) + with patch('szurubooru.func.auth.is_valid_password'), \ + patch('szurubooru.func.users.get_user_by_name'): + users.get_user_by_name.return_value = user + auth.is_valid_password.return_value = True + authenticator.process_request(ctx) + assert user.last_login_time is not None + + +def test_process_request_bump_login_with_token( + context_factory, user_token_factory): + user_token = user_token_factory() + db.session.add(user_token) + db.session.flush() + ctx = context_factory( + headers={ + 'Authorization': "Token dGVzdFVzZXI6dGVzdFRva2Vu" + }, + params={ + 'bump-login': 'true' + }) + with patch('szurubooru.func.auth.is_valid_token'), \ + patch('szurubooru.func.users.get_user_by_name'), \ + patch('szurubooru.func.user_tokens.get_by_user_and_token'): + users.get_user_by_name.return_value = user_token.user + user_tokens.get_by_user_and_token.return_value = user_token + auth.is_valid_token.return_value = True + authenticator.process_request(ctx) + assert user_token.user.last_login_time is not None + assert user_token.last_usage_time is not None + + def test_process_request_basic_auth_valid(context_factory, user_factory): user = user_factory() ctx = context_factory( @@ -36,7 +79,7 @@ def test_process_request_token_auth_valid(context_factory, user_token_factory): patch('szurubooru.func.user_tokens.get_by_user_and_token'): users.get_user_by_name.return_value = user_token.user user_tokens.get_by_user_and_token.return_value = user_token - auth.is_valid_password.return_value = True + auth.is_valid_token.return_value = True authenticator.process_request(ctx) assert ctx.user == user_token.user From a88ace403fcc8ada5100a0e0e0ec1c293fc5197d Mon Sep 17 00:00:00 2001 From: ReAnzu Date: Sat, 10 Mar 2018 19:02:09 -0600 Subject: [PATCH 18/33] Page formatting/style updates --- client/css/core-forms.styl | 32 +++++++++++++++ client/css/user-view.styl | 17 +++----- client/html/user_tokens.tpl | 60 ++++++++++++++++------------- client/js/util/views.js | 6 +++ client/js/views/user_tokens_view.js | 4 +- 5 files changed, 80 insertions(+), 39 deletions(-) diff --git a/client/css/core-forms.styl b/client/css/core-forms.styl index f083ffbe..bed63e3b 100644 --- a/client/css/core-forms.styl +++ b/client/css/core-forms.styl @@ -137,6 +137,38 @@ input[type=checkbox]:focus + .checkbox:before +/* + * Date and time inputs + */ + +input[type=date], +input[type=time] + vertical-align: top + font-family: 'Droid Sans', sans-serif + font-size: 100% + padding: 0.2em 0.3em + box-sizing: border-box + border: 2px solid $input-enabled-border-color + background: $input-enabled-background-color + color: $input-enabled-text-color + box-shadow: none /* :-moz-submit-invalid on FF */ + transition: border-color 0.1s linear, background-color 0.1s linear + + &:disabled + border: 2px solid $input-disabled-border-color + background: $input-disabled-background-color + color: $input-disabled-text-color + + &:focus + border-color: $main-color + + &[readonly] + border: 2px solid $input-disabled-border-color + background: $input-disabled-background-color + color: $input-disabled-text-color + + + /* * Regular inputs */ diff --git a/client/css/user-view.styl b/client/css/user-view.styl index 64a01b52..3cdd29cb 100644 --- a/client/css/user-view.styl +++ b/client/css/user-view.styl @@ -42,12 +42,6 @@ $token-border-color = $active-tab-background-color #user-tokens - .flex-centered - width: 100%; - display: flex; - flex-direction: row; - justify-content: space-around; - .token-flex-container width: 100% display: flex; @@ -63,14 +57,14 @@ $token-border-color = $active-tab-background-color justify-content: space-between; padding: 0.2em; + .no-wrap + white-space: nowrap; + .token-input min-height: 2em; line-height: 2em; text-align: center; - form - width: auto; - .token-flex-column display: flex; flex-direction: column; @@ -81,7 +75,8 @@ $token-border-color = $active-tab-background-color hr border-top: 3px solid $token-border-color + form + width: 100%; + #user-delete form width: 100% - - diff --git a/client/html/user_tokens.tpl b/client/html/user_tokens.tpl index 8a37aca2..8032dc1a 100644 --- a/client/html/user_tokens.tpl +++ b/client/html/user_tokens.tpl @@ -1,31 +1,35 @@
    <% if (ctx.tokens.length > 0) { %> -
    +
    <% _.each(ctx.tokens, function(token, index) { %> -
    -
    -
    Token:
    -
    Note:
    -
    Created:
    -
    Expires:
    -
    Used:
    +
    +
    +
    Token:
    +
    Note:
    +
    Created:
    +
    Expires:
    +
    Last used:
    -
    -
    <%= token.token %>
    -
    <%= token.note %>
    -
    <%= ctx.makeRelativeTime(token.creationTime) %>
    - <% if (token.expirationTime) { %> -
    <%= ctx.makeRelativeTime(token.expirationTime) %>
    +
    +
    <%= token.token %>
    + <% if (token.note !== null) { %> +
    <%= token.note %>
    <% } else { %> -
    No expiration
    +
     
    <% } %> -
    <%= ctx.makeRelativeTime(token.lastUsageTime) %>
    +
    <%= ctx.makeRelativeTime(token.creationTime) %>
    + <% if (token.expirationTime) { %> +
    <%= ctx.makeRelativeTime(token.expirationTime) %>
    + <% } else { %> +
    No expiration
    + <% } %> +
    <%= ctx.makeRelativeTime(token.lastUsageTime) %>
    -
    -
    -
    +
    +
    +
    <% if (token.isCurrentAuthToken) { %> @@ -45,14 +49,18 @@

    No Registered Tokens

    <% } %> -
      -
    • - - +
        +
      • + <%= ctx.makeTextInput({ + text: 'Note', + id: 'note', + }) %>
      • -
      • - - +
      • + <%= ctx.makeDateInput({ + text: 'Expires', + id: 'expirationTime', + }) %>
      diff --git a/client/js/util/views.js b/client/js/util/views.js index b0b7ccec..9f238b1e 100644 --- a/client/js/util/views.js +++ b/client/js/util/views.js @@ -168,6 +168,11 @@ function makeNumericInput(options) { return makeInput(options); } +function makeDateInput(options) { + options.type = 'date'; + return makeInput(options) +} + function getPostUrl(id, parameters) { return uri.formatClientLink( 'post', id, @@ -392,6 +397,7 @@ function getTemplate(templatePath) { makePasswordInput: makePasswordInput, makeEmailInput: makeEmailInput, makeColorInput: makeColorInput, + makeDateInput: makeDateInput, makePostLink: makePostLink, makeTagLink: makeTagLink, makeUserLink: makeUserLink, diff --git a/client/js/views/user_tokens_view.js b/client/js/views/user_tokens_view.js index bb81acc2..3433ea74 100644 --- a/client/js/views/user_tokens_view.js +++ b/client/js/views/user_tokens_view.js @@ -94,11 +94,11 @@ class UserTokenView extends events.EventTarget { } get _userTokenNoteInputNode() { - return this._formNode.querySelector('[name=note]'); + return this._formNode.querySelector('.note input'); } get _userTokenExpirationTimeInputNode() { - return this._formNode.querySelector('[name=expirationTime]'); + return this._formNode.querySelector('.expirationTime input'); } } From 50efa71e0cc61f56e5530ec8707406099b7559a4 Mon Sep 17 00:00:00 2001 From: ReAnzu Date: Sat, 10 Mar 2018 19:46:06 -0600 Subject: [PATCH 19/33] Link to allow changing the note on the interface. --- client/html/user_tokens.tpl | 26 +++++++++------- client/js/controllers/user_controller.js | 18 +++++++++++ client/js/models/user_token.js | 23 ++++++++++++++ client/js/views/user_tokens_view.js | 39 ++++++++++++++++++++++-- client/js/views/user_view.js | 1 + 5 files changed, 93 insertions(+), 14 deletions(-) diff --git a/client/html/user_tokens.tpl b/client/html/user_tokens.tpl index 8032dc1a..73db7a17 100644 --- a/client/html/user_tokens.tpl +++ b/client/html/user_tokens.tpl @@ -13,17 +13,22 @@
      <%= token.token %>
      - <% if (token.note !== null) { %> -
      <%= token.note %>
      - <% } else { %> -
       
      - <% } %> +
      + <% if (token.note !== null) { %> + <%= token.note %> + <% } else { %> + No note + <% } %> + (change) +
      <%= ctx.makeRelativeTime(token.creationTime) %>
      - <% if (token.expirationTime) { %> -
      <%= ctx.makeRelativeTime(token.expirationTime) %>
      - <% } else { %> -
      No expiration
      - <% } %> +
      + <% if (token.expirationTime) { %> + <%= ctx.makeRelativeTime(token.expirationTime) %> + <% } else { %> + No expiration + <% } %> +
      <%= ctx.makeRelativeTime(token.lastUsageTime) %>
    @@ -31,7 +36,6 @@
    - <% if (token.isCurrentAuthToken) { %> diff --git a/client/js/controllers/user_controller.js b/client/js/controllers/user_controller.js index 55987641..d042e41f 100644 --- a/client/js/controllers/user_controller.js +++ b/client/js/controllers/user_controller.js @@ -93,6 +93,7 @@ class UserController { this._view.addEventListener('delete', e => this._evtDelete(e)); this._view.addEventListener('create-token', e => this._evtCreateToken(e)); this._view.addEventListener('delete-token', e => this._evtDeleteToken(e)); + this._view.addEventListener('update-token', e => this._evtUpdateToken(e)); for (let message of this._successMessages) { this.showSuccess(message); @@ -232,6 +233,23 @@ class UserController { }); } } + + _evtUpdateToken(e) { + this._view.clearMessages(); + this._view.disableForm(); + + if (e.detail.note !== undefined) { + e.detail.userToken.note = e.detail.note; + } + + e.detail.userToken.save(e.detail.user.name).then(response => { + const ctx = router.show(uri.formatClientLink('user', e.detail.user.name, 'list-tokens')); + ctx.controller.showSuccess('Token ' + response.token + ' updated.'); + }, error => { + this._view.showError(error.message); + this._view.enableForm(); + }); + } } module.exports = router => { diff --git a/client/js/models/user_token.js b/client/js/models/user_token.js index fcb374f4..6e70a94b 100644 --- a/client/js/models/user_token.js +++ b/client/js/models/user_token.js @@ -20,6 +20,8 @@ class UserToken extends events.EventTarget { get lastEditTime() { return this._lastEditTime; } get lastUsageTime() { return this._lastUsageTime; } + set note(value) { this._note = value; } + static fromResponse(response) { if (typeof response.results !== 'undefined') { let tokenList = []; @@ -59,6 +61,27 @@ class UserToken extends events.EventTarget { }); } + save(userName) { + const detail = {version: this._version}; + + if (this._note !== this._orig._note) { + detail.note = this._note; + } + + return api.put( + uri.formatApiLink('user-token', userName, this._orig._token), + detail) + .then(response => { + this._updateFromResponse(response); + this.dispatchEvent(new CustomEvent('change', { + detail: { + userToken: this, + }, + })); + return Promise.resolve(this); + }); + } + delete(userName) { return api.delete( uri.formatApiLink('user-token', userName, this._orig._token), diff --git a/client/js/views/user_tokens_view.js b/client/js/views/user_tokens_view.js index 3433ea74..b652973a 100644 --- a/client/js/views/user_tokens_view.js +++ b/client/js/views/user_tokens_view.js @@ -2,7 +2,6 @@ const events = require('../events.js'); const views = require('../util/views.js'); -const api = require('../api.js'); const template = views.getTemplate('user-tokens'); @@ -19,19 +18,26 @@ class UserTokenView extends events.EventTarget { this._formNode.addEventListener('submit', e => this._evtSubmit(e)); - this._decorateTokenForms() + this._decorateTokenForms(); + this._decorateTokenNoteChangeLinks(); } _decorateTokenForms() { this._tokenFormNodes = []; for (let i = 0; i < this._tokens.length; i++) { let formNode = this._hostNode.querySelector('.token[data-token-id=\"' + i + '\"]'); - views.decorateValidator(formNode); formNode.addEventListener('submit', e => this._evtDelete(e)); this._tokenFormNodes.push(formNode); } } + _decorateTokenNoteChangeLinks() { + for (let i = 0; i < this._tokens.length; i++) { + let linkNode = this._hostNode.querySelector('.token-change-note[data-token-id=\"' + i + '\"]'); + linkNode.addEventListener('click', e => this._evtChangeNoteClick(e)); + } + } + clearMessages() { views.clearMessages(this._hostNode); } @@ -89,6 +95,33 @@ class UserTokenView extends events.EventTarget { })); } + _evtChangeNoteClick(e) { + e.preventDefault(); + const userToken = this._tokens[parseInt(e.target.getAttribute('data-token-id'))]; + const text = window.prompt( + 'Please enter the new name:', userToken.note !== null ? userToken.note : undefined); + if (!text) { + return; + } + this.dispatchEvent(new CustomEvent('update', { + detail: { + user: this._user, + userToken: userToken, + note: text ? text : undefined, + }, + })); + // const notesObj = JSON.parse(text); + // this._post.notes.clear(); + // for (let noteObj of notesObj) { + // let note = new Note(); + // for (let pointObj of noteObj.polygon) { + // note.polygon.add(new Point(pointObj[0], pointObj[1])); + // } + // note.text = noteObj.text; + // this._post.notes.add(note); + // } + } + get _formNode() { return this._hostNode.querySelector('#create-token-form'); } diff --git a/client/js/views/user_view.js b/client/js/views/user_view.js index 449327c3..75fd154d 100644 --- a/client/js/views/user_view.js +++ b/client/js/views/user_view.js @@ -55,6 +55,7 @@ class UserView extends events.EventTarget { this._view = new UserTokensView(ctx); events.proxyEvent(this._view, this, 'delete', 'delete-token'); events.proxyEvent(this._view, this, 'submit', 'create-token'); + events.proxyEvent(this._view, this, 'update', 'update-token'); } } else if (ctx.section == 'delete') { if (!this._ctx.canDelete) { From 12b751f0663d6e114e19505ae2f15b0f27c21a59 Mon Sep 17 00:00:00 2001 From: ReAnzu Date: Sat, 10 Mar 2018 19:53:55 -0600 Subject: [PATCH 20/33] server/user_token: empty notes after strip will be saved as nulls --- server/szurubooru/func/user_tokens.py | 1 + 1 file changed, 1 insertion(+) diff --git a/server/szurubooru/func/user_tokens.py b/server/szurubooru/func/user_tokens.py index 8cd93dab..29d6cbab 100644 --- a/server/szurubooru/func/user_tokens.py +++ b/server/szurubooru/func/user_tokens.py @@ -140,6 +140,7 @@ def update_user_token_expiration_time( def update_user_token_note(user_token: model.UserToken, note: str) -> None: assert user_token note = note.strip() if note is not None else '' + note = None if len(note) == 0 else note if util.value_exceeds_column_size(note, model.UserToken.note): raise InvalidNoteError('Note is too long.') user_token.note = note From 141abf15c914bf2abdc550a06f3df55ea7552a17 Mon Sep 17 00:00:00 2001 From: rr- Date: Sun, 11 Mar 2018 01:24:55 +0100 Subject: [PATCH 21/33] docs: update typos and formatting --- API.md | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/API.md b/API.md index 4098a83a..50d6bb27 100644 --- a/API.md +++ b/API.md @@ -98,12 +98,12 @@ ## Authentication Authentication is achieved by means of [basic HTTP -auth](https://en.wikipedia.org/wiki/Basic_access_authentication) or through the -use of [user token authentication](#user-token-authentication). For this reason, -it is recommended to connect through HTTPS. There are no sessions, so every -privileged request must be authenticated. Available privileges depend on the -user's rank. The way how rank translates to privileges is defined in the server's -configuration. +auth](https://en.wikipedia.org/wiki/Basic_access_authentication) or through the +use of [user token authentication](#user-token-authentication). For this +reason, it is recommended to connect through HTTPS. There are no sessions, so +every privileged request must be authenticated. Available privileges depend on +the user's rank. The way how rank translates to privileges is defined in the +server's configuration. It is recommended to add `?bump-login` GET parameter to the first request in a client "session" (where the definition of a session is up to the client), so @@ -111,11 +111,11 @@ that the user's last login time is kept up to date. ## User token authentication -User token authentication works similarly to [basic HTTP +User token authentication works similarly to [basic HTTP auth](https://en.wikipedia.org/wiki/Basic_access_authentication). Because it operates similarly to ***basic HTTP auth*** it is still recommended to connect -through HTTPS. The authorization header uses the type of Token and the username -and token are encoded as Base64 and sent as the second parameter. +through HTTPS. The authorization header uses the type of `Token` and the +username and token are encoded as Base64 and sent as the second parameter. Example header for user1:token-is-more-secure ``` @@ -123,9 +123,9 @@ Authorization: Token dXNlcjE6dG9rZW4taXMtbW9yZS1zZWN1cmU= ``` The benefit of token authentication is that beyond the initial login to acquire -the first token, there is no need to transmit the user password in plaintext via -basic auth. Additionally tokens can be revoked at anytime allowing a cleaner -interface for isolating clients from user credentials. +the first token, there is no need to transmit the user password in plaintext +via basic auth. Additionally tokens can be revoked at anytime allowing a +cleaner interface for isolating clients from user credentials. ## Basic requests @@ -1511,7 +1511,7 @@ data. - **Description** - Searches for users tokens for the currently logged in user. + Searches for user tokens for the given user. ## Creating a user token - **Request** @@ -1538,9 +1538,9 @@ data. - **Description** - Creates a new user token that can be used for authentication of api + Creates a new user token that can be used for authentication of API endpoints instead of a password. - + ## Updating a user token - **Request** @@ -1569,8 +1569,8 @@ data. - **Description** - Updates an existing user token using specified parameters. All fields - except the [`version`](#versioning) are optional - update concerns only + Updates an existing user token using specified parameters. All fields + except the [`version`](#versioning) are optional - update concerns only provided fields. ## Deleting a user token @@ -1861,7 +1861,7 @@ A single user token. - ``: whether the token is still valid for authentication. - ``: time when the token expires. It must include the timezone as per RFC3339. - ``: resource version. See [versioning](#versioning). -- ``: time the user token was created , formatted as per RFC 3339. +- ``: time the user token was created, formatted as per RFC 3339. - ``: time the user token was edited, formatted as per RFC 3339. - ``: the last time this token was used during a login involving `?bump-login`, formatted as per RFC 3339. From 70e81bdce9599c7df869a3d690c75651ea79d751 Mon Sep 17 00:00:00 2001 From: ReAnzu Date: Sun, 11 Mar 2018 09:34:57 -0500 Subject: [PATCH 22/33] server/user_token: enabled cannot be None, no need for exception --- server/szurubooru/func/user_tokens.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/server/szurubooru/func/user_tokens.py b/server/szurubooru/func/user_tokens.py index 29d6cbab..9745a52b 100644 --- a/server/szurubooru/func/user_tokens.py +++ b/server/szurubooru/func/user_tokens.py @@ -6,10 +6,6 @@ from szurubooru import db, model, rest, errors from szurubooru.func import auth, serialization, users, util -class InvalidEnabledError(errors.ValidationError): - pass - - class InvalidExpirationError(errors.ValidationError): pass @@ -109,8 +105,6 @@ def create_user_token(user: model.User, enabled: bool) -> model.UserToken: def update_user_token_enabled( user_token: model.UserToken, enabled: bool) -> None: assert user_token - if enabled is None: - raise InvalidEnabledError('Enabled cannot be empty.') user_token.enabled = enabled update_user_token_edit_time(user_token) From 2f857f00b2b4ca3e1df5895e62d5e26d032b4bb4 Mon Sep 17 00:00:00 2001 From: ReAnzu Date: Wed, 21 Mar 2018 20:52:48 -0500 Subject: [PATCH 23/33] server/api.md: Fix broken API docs --- API.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/API.md b/API.md index 50d6bb27..a5560db0 100644 --- a/API.md +++ b/API.md @@ -58,10 +58,10 @@ - [Getting user](#getting-user) - [Deleting user](#deleting-user) - User Tokens - - [Listing tokens](#listing-tokens) - - [Creating token](#creating-token) - - [Updating token](#updating-token) - - [Deleting token](#deleting-token) + - [Listing user tokens](#listing-user-tokens) + - [Creating user token](#creating-user-token) + - [Updating user token](#updating-user-token) + - [Deleting user token](#deleting-user-token) - Password reset - [Password reset - step 1: mail request](#password-reset---step-2-confirmation) - [Password reset - step 2: confirmation](#password-reset---step-2-confirmation) @@ -1513,7 +1513,7 @@ data. Searches for user tokens for the given user. -## Creating a user token +## Creating user token - **Request** `POST /user-token/` @@ -1541,7 +1541,7 @@ data. Creates a new user token that can be used for authentication of API endpoints instead of a password. -## Updating a user token +## Updating user token - **Request** `PUT /user-token//` @@ -1573,7 +1573,7 @@ data. except the [`version`](#versioning) are optional - update concerns only provided fields. -## Deleting a user token +## Deleting user token - **Request** `DELETE /user-token//` From 2cfd635954cbde624d6428515f44d727a0a77d32 Mon Sep 17 00:00:00 2001 From: rr- Date: Thu, 22 Mar 2018 09:06:53 +0100 Subject: [PATCH 24/33] remove dead code --- client/js/views/user_tokens_view.js | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/client/js/views/user_tokens_view.js b/client/js/views/user_tokens_view.js index b652973a..0010d5d5 100644 --- a/client/js/views/user_tokens_view.js +++ b/client/js/views/user_tokens_view.js @@ -110,16 +110,6 @@ class UserTokenView extends events.EventTarget { note: text ? text : undefined, }, })); - // const notesObj = JSON.parse(text); - // this._post.notes.clear(); - // for (let noteObj of notesObj) { - // let note = new Note(); - // for (let pointObj of noteObj.polygon) { - // note.polygon.add(new Point(pointObj[0], pointObj[1])); - // } - // note.text = noteObj.text; - // this._post.notes.add(note); - // } } get _formNode() { From 5f835e9d5590f0beecf12a27adf53e3452d1c9c8 Mon Sep 17 00:00:00 2001 From: rr- Date: Thu, 22 Mar 2018 09:08:30 +0100 Subject: [PATCH 25/33] wrap to 80 characters --- client/js/views/user_tokens_view.js | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/client/js/views/user_tokens_view.js b/client/js/views/user_tokens_view.js index 0010d5d5..f6c84800 100644 --- a/client/js/views/user_tokens_view.js +++ b/client/js/views/user_tokens_view.js @@ -25,7 +25,8 @@ class UserTokenView extends events.EventTarget { _decorateTokenForms() { this._tokenFormNodes = []; for (let i = 0; i < this._tokens.length; i++) { - let formNode = this._hostNode.querySelector('.token[data-token-id=\"' + i + '\"]'); + let formNode = this._hostNode.querySelector( + '.token[data-token-id=\"' + i + '\"]'); formNode.addEventListener('submit', e => this._evtDelete(e)); this._tokenFormNodes.push(formNode); } @@ -33,8 +34,10 @@ class UserTokenView extends events.EventTarget { _decorateTokenNoteChangeLinks() { for (let i = 0; i < this._tokens.length; i++) { - let linkNode = this._hostNode.querySelector('.token-change-note[data-token-id=\"' + i + '\"]'); - linkNode.addEventListener('click', e => this._evtChangeNoteClick(e)); + let linkNode = this._hostNode.querySelector( + '.token-change-note[data-token-id=\"' + i + '\"]'); + linkNode.addEventListener( + 'click', e => this._evtChangeNoteClick(e)); } } @@ -66,7 +69,8 @@ class UserTokenView extends events.EventTarget { _evtDelete(e) { e.preventDefault(); - const userToken = this._tokens[parseInt(e.target.getAttribute('data-token-id'))]; + const userToken = this._tokens[parseInt( + e.target.getAttribute('data-token-id'))]; this.dispatchEvent(new CustomEvent('delete', { detail: { user: this._user, @@ -87,19 +91,21 @@ class UserTokenView extends events.EventTarget { expirationTime: (this._userTokenExpirationTimeInputNode - && this._userTokenExpirationTimeInputNode.value.length > 0) ? - new Date(this._userTokenExpirationTimeInputNode.value).toISOString() : + && this._userTokenExpirationTimeInputNode.value) ? + new Date(this._userTokenExpirationTimeInputNode.value) + .toISOString() : undefined, - }, })); } _evtChangeNoteClick(e) { e.preventDefault(); - const userToken = this._tokens[parseInt(e.target.getAttribute('data-token-id'))]; + const userToken = this._tokens[ + parseInt(e.target.getAttribute('data-token-id'))]; const text = window.prompt( - 'Please enter the new name:', userToken.note !== null ? userToken.note : undefined); + 'Please enter the new name:', + userToken.note !== null ? userToken.note : undefined); if (!text) { return; } From e678dcb872e5d89e7a7f793c0dab65dff1cd309a Mon Sep 17 00:00:00 2001 From: rr- Date: Thu, 22 Mar 2018 09:09:57 +0100 Subject: [PATCH 26/33] move date to polyfill --- client/js/api.js | 1 - client/js/util/date.js | 7 ------- client/js/util/polyfill.js | 7 +++++++ 3 files changed, 7 insertions(+), 8 deletions(-) delete mode 100644 client/js/util/date.js diff --git a/client/js/api.js b/client/js/api.js index 3d4787be..3623045b 100644 --- a/client/js/api.js +++ b/client/js/api.js @@ -1,6 +1,5 @@ 'use strict'; -require('./util/date.js'); const cookies = require('js-cookie'); const request = require('superagent'); const config = require('./config.js'); diff --git a/client/js/util/date.js b/client/js/util/date.js deleted file mode 100644 index 7a40b163..00000000 --- a/client/js/util/date.js +++ /dev/null @@ -1,7 +0,0 @@ -'use strict'; - -Date.prototype.addDays = function(days) { - let dat = new Date(this.valueOf()); - dat.setDate(dat.getDate() + days); - return dat; -}; diff --git a/client/js/util/polyfill.js b/client/js/util/polyfill.js index 91186b2a..71ee9724 100644 --- a/client/js/util/polyfill.js +++ b/client/js/util/polyfill.js @@ -59,3 +59,10 @@ Number.prototype.between = function(a, b, inclusive) { // non standard Promise.prototype.abort = () => {}; + +// non standard +Date.prototype.addDays = function(days) { + let dat = new Date(this.valueOf()); + dat.setDate(dat.getDate() + days); + return dat; +}; From a0ea1dc64a443db391fe34aa0bb2d1c5308bb431 Mon Sep 17 00:00:00 2001 From: rr- Date: Thu, 22 Mar 2018 09:15:46 +0100 Subject: [PATCH 27/33] formatting --- .../a39c7f98a7fa_add_user_token_table.py | 30 +++++++++---------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/server/szurubooru/migrations/versions/a39c7f98a7fa_add_user_token_table.py b/server/szurubooru/migrations/versions/a39c7f98a7fa_add_user_token_table.py index 6978e829..899eaa70 100644 --- a/server/szurubooru/migrations/versions/a39c7f98a7fa_add_user_token_table.py +++ b/server/szurubooru/migrations/versions/a39c7f98a7fa_add_user_token_table.py @@ -16,22 +16,20 @@ depends_on = None def upgrade(): - op.create_table('user_token', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('user_id', sa.Integer(), nullable=False), - sa.Column('token', sa.Unicode(length=36), nullable=False), - sa.Column('note', sa.Unicode(length=128), nullable=True), - sa.Column('enabled', sa.Boolean(), nullable=False), - sa.Column('expiration_time', sa.DateTime(), nullable=True), - sa.Column('creation_time', sa.DateTime(), nullable=False), - sa.Column('last_edit_time', sa.DateTime(), nullable=True), - sa.Column('last_usage_time', sa.DateTime(), nullable=True), - sa.Column('version', sa.Integer(), nullable=False), - sa.ForeignKeyConstraint( - ['user_id'], - ['user.id'], - ondelete='CASCADE'), - sa.PrimaryKeyConstraint('id')) + op.create_table( + 'user_token', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('user_id', sa.Integer(), nullable=False), + sa.Column('token', sa.Unicode(length=36), nullable=False), + sa.Column('note', sa.Unicode(length=128), nullable=True), + sa.Column('enabled', sa.Boolean(), nullable=False), + sa.Column('expiration_time', sa.DateTime(), nullable=True), + sa.Column('creation_time', sa.DateTime(), nullable=False), + sa.Column('last_edit_time', sa.DateTime(), nullable=True), + sa.Column('last_usage_time', sa.DateTime(), nullable=True), + sa.Column('version', sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(['user_id'], ['user.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id')) op.create_index( op.f('ix_user_token_user_id'), 'user_token', ['user_id'], unique=False) From 472f153960a3924a2a3384387a57f74c7586d411 Mon Sep 17 00:00:00 2001 From: rr- Date: Thu, 22 Mar 2018 09:17:26 +0100 Subject: [PATCH 28/33] formatting --- server/szurubooru/api/user_token_api.py | 2 +- .../szurubooru/tests/middleware/test_authenticator.py | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/server/szurubooru/api/user_token_api.py b/server/szurubooru/api/user_token_api.py index 7724a6c9..77398239 100644 --- a/server/szurubooru/api/user_token_api.py +++ b/server/szurubooru/api/user_token_api.py @@ -19,7 +19,7 @@ def get_user_tokens( auth.verify_privilege(ctx.user, 'user_tokens:list:%s' % infix) user_token_list = user_tokens.get_user_tokens(user) return { - "results": [_serialize(ctx, token) for token in user_token_list] + 'results': [_serialize(ctx, token) for token in user_token_list] } diff --git a/server/szurubooru/tests/middleware/test_authenticator.py b/server/szurubooru/tests/middleware/test_authenticator.py index 36e74adf..fc19d659 100644 --- a/server/szurubooru/tests/middleware/test_authenticator.py +++ b/server/szurubooru/tests/middleware/test_authenticator.py @@ -18,7 +18,7 @@ def test_process_request_bump_login(context_factory, user_factory): db.session.flush() ctx = context_factory( headers={ - 'Authorization': "Basic dGVzdFVzZXI6dGVzdFRva2Vu" + 'Authorization': 'Basic dGVzdFVzZXI6dGVzdFRva2Vu' }, params={ 'bump-login': 'true' @@ -38,7 +38,7 @@ def test_process_request_bump_login_with_token( db.session.flush() ctx = context_factory( headers={ - 'Authorization': "Token dGVzdFVzZXI6dGVzdFRva2Vu" + 'Authorization': 'Token dGVzdFVzZXI6dGVzdFRva2Vu' }, params={ 'bump-login': 'true' @@ -58,7 +58,7 @@ def test_process_request_basic_auth_valid(context_factory, user_factory): user = user_factory() ctx = context_factory( headers={ - 'Authorization': "Basic dGVzdFVzZXI6dGVzdFBhc3N3b3Jk" + 'Authorization': 'Basic dGVzdFVzZXI6dGVzdFBhc3N3b3Jk' }) with patch('szurubooru.func.auth.is_valid_password'), \ patch('szurubooru.func.users.get_user_by_name'): @@ -72,7 +72,7 @@ def test_process_request_token_auth_valid(context_factory, user_token_factory): user_token = user_token_factory() ctx = context_factory( headers={ - 'Authorization': "Token dGVzdFVzZXI6dGVzdFRva2Vu" + 'Authorization': 'Token dGVzdFVzZXI6dGVzdFRva2Vu' }) with patch('szurubooru.func.auth.is_valid_token'), \ patch('szurubooru.func.users.get_user_by_name'), \ @@ -87,7 +87,7 @@ def test_process_request_token_auth_valid(context_factory, user_token_factory): def test_process_request_bad_header(context_factory): ctx = context_factory( headers={ - 'Authorization': "Secret SuperSecretValue" + 'Authorization': 'Secret SuperSecretValue' }) with pytest.raises(errors.HttpBadRequest): authenticator.process_request(ctx) From 8f43c0db2dea162c2d4ad332620661ff98278c68 Mon Sep 17 00:00:00 2001 From: rr- Date: Thu, 22 Mar 2018 09:19:45 +0100 Subject: [PATCH 29/33] formatting --- server/szurubooru/tests/middleware/test_authenticator.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/server/szurubooru/tests/middleware/test_authenticator.py b/server/szurubooru/tests/middleware/test_authenticator.py index fc19d659..be21a931 100644 --- a/server/szurubooru/tests/middleware/test_authenticator.py +++ b/server/szurubooru/tests/middleware/test_authenticator.py @@ -24,7 +24,7 @@ def test_process_request_bump_login(context_factory, user_factory): 'bump-login': 'true' }) with patch('szurubooru.func.auth.is_valid_password'), \ - patch('szurubooru.func.users.get_user_by_name'): + patch('szurubooru.func.users.get_user_by_name'): users.get_user_by_name.return_value = user auth.is_valid_password.return_value = True authenticator.process_request(ctx) @@ -44,8 +44,8 @@ def test_process_request_bump_login_with_token( 'bump-login': 'true' }) with patch('szurubooru.func.auth.is_valid_token'), \ - patch('szurubooru.func.users.get_user_by_name'), \ - patch('szurubooru.func.user_tokens.get_by_user_and_token'): + patch('szurubooru.func.users.get_user_by_name'), \ + patch('szurubooru.func.user_tokens.get_by_user_and_token'): users.get_user_by_name.return_value = user_token.user user_tokens.get_by_user_and_token.return_value = user_token auth.is_valid_token.return_value = True From 0f8fc5efa8c1b929b0e535f9325f7f76959b3d4d Mon Sep 17 00:00:00 2001 From: rr- Date: Thu, 22 Mar 2018 09:27:31 +0100 Subject: [PATCH 30/33] imports --- server/szurubooru/func/user_tokens.py | 4 ++-- server/szurubooru/middleware/authenticator.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/server/szurubooru/func/user_tokens.py b/server/szurubooru/func/user_tokens.py index 9745a52b..c0f4badb 100644 --- a/server/szurubooru/func/user_tokens.py +++ b/server/szurubooru/func/user_tokens.py @@ -1,7 +1,7 @@ -import pytz from datetime import datetime -from pyrfc3339 import parser as rfc3339_parser from typing import Any, Optional, List, Dict, Callable +from pyrfc3339 import parser as rfc3339_parser +import pytz from szurubooru import db, model, rest, errors from szurubooru.func import auth, serialization, users, util diff --git a/server/szurubooru/middleware/authenticator.py b/server/szurubooru/middleware/authenticator.py index b185dad6..4340ec94 100644 --- a/server/szurubooru/middleware/authenticator.py +++ b/server/szurubooru/middleware/authenticator.py @@ -1,6 +1,6 @@ import base64 from typing import Optional, Tuple -from szurubooru import db, model, errors, rest +from szurubooru import model, errors, rest from szurubooru.func import auth, users, user_tokens from szurubooru.rest.errors import HttpBadRequest From a3be5135ffb7bcee18ada95deb84fc8578d70fad Mon Sep 17 00:00:00 2001 From: rr- Date: Thu, 22 Mar 2018 09:34:36 +0100 Subject: [PATCH 31/33] docs/api: whitespace --- API.md | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/API.md b/API.md index a5560db0..083edc80 100644 --- a/API.md +++ b/API.md @@ -1522,9 +1522,9 @@ data. ```json5 { - "enabled": , // optional - "note": , // optional - "expirationTime": , // optional + "enabled": , // optional + "note": , // optional + "expirationTime": , // optional } ``` @@ -1551,8 +1551,8 @@ data. ```json5 { "version": , - "enabled": , // optional - "note": , // optional + "enabled": , // optional + "note": , // optional "expirationTime": , // optional } ``` @@ -1582,7 +1582,7 @@ data. ```json5 { - "version": , + "version": , } ``` @@ -1842,15 +1842,15 @@ A single user token. ```json5 { - "user": , - "token": , - "note": , - "enabled": , - "expirationTime": , - "version": , - "creationTime": , - "lastEditTime": , - "lastUsageTime": , + "user": , + "token": , + "note": , + "enabled": , + "expirationTime": , + "version": , + "creationTime": , + "lastEditTime": , + "lastUsageTime": , } ``` From a297489d97244ee39132f487200f1b14f78773e2 Mon Sep 17 00:00:00 2001 From: rr- Date: Thu, 22 Mar 2018 09:35:24 +0100 Subject: [PATCH 32/33] docs/api: delete trailing commas --- API.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/API.md b/API.md index 083edc80..ecc2e217 100644 --- a/API.md +++ b/API.md @@ -1522,9 +1522,9 @@ data. ```json5 { - "enabled": , // optional - "note": , // optional - "expirationTime": , // optional + "enabled": , // optional + "note": , // optional + "expirationTime": // optional } ``` @@ -1551,9 +1551,9 @@ data. ```json5 { "version": , - "enabled": , // optional - "note": , // optional - "expirationTime": , // optional + "enabled": , // optional + "note": , // optional + "expirationTime": // optional } ``` @@ -1582,7 +1582,7 @@ data. ```json5 { - "version": , + "version": } ``` @@ -1850,7 +1850,7 @@ A single user token. "version": , "creationTime": , "lastEditTime": , - "lastUsageTime": , + "lastUsageTime": } ``` From 020d8b42bafa057b492cd4797b14efee1a9b7d20 Mon Sep 17 00:00:00 2001 From: rr- Date: Thu, 22 Mar 2018 09:37:31 +0100 Subject: [PATCH 33/33] docs/api: more ocd --- API.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/API.md b/API.md index ecc2e217..c23f0454 100644 --- a/API.md +++ b/API.md @@ -1859,7 +1859,7 @@ A single user token. - ``: the token that can be used to authenticate the user. - ``: a note that describes the token. - ``: whether the token is still valid for authentication. -- ``: time when the token expires. It must include the timezone as per RFC3339. +- ``: time when the token expires. It must include the timezone as per RFC 3339. - ``: resource version. See [versioning](#versioning). - ``: time the user token was created, formatted as per RFC 3339. - ``: time the user token was edited, formatted as per RFC 3339.