diff --git a/.gitignore b/.gitignore index a683cefa..15ddae57 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ config.yaml */*_modules/ .coverage .cache +docker-compose.yml diff --git a/.travis.yml b/.travis.yml index 0e675329..9c249186 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,24 +9,30 @@ matrix: include: - language: python python: - - "3.5" + - "3.6" before_install: - sudo apt-get -y install software-properties-common - sudo add-apt-repository -y ppa:mc3man/trusty-media - sudo apt-get update - sudo apt-get -y --allow-unauthenticated install ffmpeg - - cp config.yaml.dist config.yaml - - sed -i -e 's/^database:$/database:\ postgres:\/\/szuru:dog@localhost:5432\/szuru_test/' config.yaml - sudo -i -u postgres createuser szuru -D -R -S - sudo -i -u postgres createdb szuru_test - sudo -i -u postgres psql -c "ALTER USER szuru PASSWORD 'dog';" - - sed -i -e 's/^api_url:/api_url:\ http:\/\/localhost\/api\//' config.yaml - - sed -i -e 's/^base_url:/base_url:\ http:\/\/localhost\//' config.yaml - - sed -i -e 's/^data_url:/data_url:\ http:\/\/localhost\/data\//' config.yaml - - sed -i -e 's/^data_dir:/data_dir:\ \/data\//' config.yaml - install: - cd server + - cp config.yaml.dist ../config.yaml.dist + - cp config.yaml.dist ../config.yaml + - sed -i -e 's/^#debug:/debug:/' ../config.yaml + - sed -i -e 's/^#show_sql:/show_sql:/' ../config.yaml + - sed -i -e 's/^#data_url:/data_url:/' ../config.yaml + - sed -i -e 's/^#data_dir:/data_dir:/' ../config.yaml + - sed -i -e 's/^#database:$/database:\ postgres:\/\/szuru:dog@localhost:5432\/szuru_test/' ../config.yaml + - sed -i -e 's/^#elasticsearch:/elasticsearch:/' ../config.yaml + - sed -i -e 's/^# / /' ../config.yaml + install: - pip install -r requirements.txt + - pip install -r dev-requirements.txt script: + - pycodestyle wait-for-es generate-thumb szurubooru/ + - ./wait-for-es - alembic upgrade head - py.test diff --git a/API.md b/API.md index b060e92c..c23f0454 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 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) @@ -70,8 +76,10 @@ - [User](#user) - [Micro user](#micro-user) + - [User token](#user-token) - [Tag category](#tag-category) - [Tag](#tag) + - [Micro tag](#micro-tag) - [Post](#post) - [Micro post](#micro-post) - [Note](#note) @@ -90,7 +98,8 @@ ## Authentication Authentication is achieved by means of [basic HTTP -auth](https://en.wikipedia.org/wiki/Basic_access_authentication). For this +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 @@ -100,6 +109,24 @@ 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: @@ -254,12 +281,6 @@ data. Lists all tag categories. Doesn't use paging. - **Note**: independently, the server exports current tag category list - snapshots to the data directory under `tags.json` name. Its purpose is to - reduce the trips frontend needs to make when doing autocompletion, and ease - caching. The data directory and its URL are controlled with `data_dir` and - `data_url` variables in server's configuration. - ## Creating tag category - **Request** @@ -404,7 +425,7 @@ data. ## Listing tags - **Request** - `GET /tags/?page=&pageSize=&query=` + `GET /tags/?offset=&limit=&query=` - **Output** @@ -419,33 +440,27 @@ data. Searches for tags. - **Note**: independently, the server exports current tag list snapshots to - the data directory under `tags.json` name. Its purpose is to reduce the - trips frontend needs to make when doing autocompletion, and ease caching. - The data directory and its URL are controlled with `data_dir` and - `data_url` variables in server's configuration. - **Anonymous tokens** Same as `name` token. **Named tokens** - | `` | Description | - | ------------------- | ------------------------------------- | - | `name` | having given name (accepts wildcards) | - | `category` | having given category | - | `creation-date` | created at given date | - | `creation-time` | alias of `creation-date` | - | `last-edit-date` | edited at given date | - | `last-edit-time` | alias of `last-edit-date` | - | `edit-date` | alias of `last-edit-date` | - | `edit-time` | alias of `last-edit-date` | - | `usages` | used in given number of posts | - | `usage-count` | alias of `usages` | - | `post-count` | alias of `usages` | - | `suggestion-count` | with given number of suggestions | - | `implication-count` | with given number of implications | + | `` | Description | + | ------------------- | ----------------------------------------- | + | `name` | having given name (accepts wildcards) | + | `category` | having given category (accepts wildcards) | + | `creation-date` | created at given date | + | `creation-time` | alias of `creation-date` | + | `last-edit-date` | edited at given date | + | `last-edit-time` | alias of `last-edit-date` | + | `edit-date` | alias of `last-edit-date` | + | `edit-time` | alias of `last-edit-date` | + | `usages` | used in given number of posts | + | `usage-count` | alias of `usages` | + | `post-count` | alias of `usages` | + | `suggestion-count` | with given number of suggestions | + | `implication-count` | with given number of implications | **Sort style tokens** @@ -675,7 +690,7 @@ data. ## Listing posts - **Request** - `GET /posts/?page=&pageSize=&query=` + `GET /posts/?offset=&limit=&query=` - **Output** @@ -696,47 +711,52 @@ data. **Named tokens** - | `` | Description | - | ------------------ | ---------------------------------------------------------- | - | `id` | having given post number | - | `tag` | having given tag | - | `score` | having given score | - | `uploader` | uploaded by given user | - | `upload` | alias of upload | - | `submit` | alias of upload | - | `comment` | commented by given user | - | `fav` | favorited by given user | - | `tag-count` | having given number of tags | - | `comment-count` | having given number of comments | - | `fav-count` | favorited by given number of users | - | `note-count` | having given number of annotations | - | `relation-count` | having given number of relations | - | `feature-count` | having been featured given number of times | - | `type` | given type of posts. `` can be either `image`, `animation` (or `animated` or `anim`), `flash` (or `swf`) or `video` (or `webm`). | - | `content-checksum` | having given SHA1 checksum | - | `file-size` | having given file size (in bytes) | - | `image-width` | having given image width (where applicable) | - | `image-height` | having given image height (where applicable) | - | `image-area` | having given number of pixels (image width * image height) | - | `width` | alias of `image-width` | - | `height` | alias of `image-height` | - | `area` | alias of `image-area` | - | `creation-date` | posted at given date | - | `creation-time` | alias of `creation-date` | - | `date` | alias of `creation-date` | - | `time` | alias of `creation-date` | - | `last-edit-date` | edited at given date | - | `last-edit-time` | alias of `last-edit-date` | - | `edit-date` | alias of `last-edit-date` | - | `edit-time` | alias of `last-edit-date` | - | `comment-date` | commented at given date | - | `comment-time` | alias of `comment-date` | - | `fav-date` | last favorited at given date | - | `fav-time` | alias of `fav-date` | - | `feature-date` | featured at given date | - | `feature-time` | alias of `feature-time` | - | `safety` | having given safety. `` can be either `safe`, `sketchy` (or `questionable`) or `unsafe`. | - | `rating` | alias of `safety` | + | `` | Description | + | -------------------- | ---------------------------------------------------------- | + | `id` | having given post number | + | `tag` | having given tag (accepts wildcards) | + | `score` | having given score | + | `uploader` | uploaded by given user (accepts wildcards) | + | `upload` | alias of upload | + | `submit` | alias of upload | + | `comment` | commented by given user (accepts wildcards) | + | `fav` | favorited by given user (accepts wildcards) | + | `tag-count` | having given number of tags | + | `comment-count` | having given number of comments | + | `fav-count` | favorited by given number of users | + | `note-count` | having given number of annotations | + | `note-text` | having given note text (accepts wildcards) | + | `relation-count` | having given number of relations | + | `feature-count` | having been featured given number of times | + | `type` | given type of posts. `` can be either `image`, `animation` (or `animated` or `anim`), `flash` (or `swf`) or `video` (or `webm`). | + | `content-checksum` | having given SHA1 checksum | + | `file-size` | having given file size (in bytes) | + | `image-width` | having given image width (where applicable) | + | `image-height` | having given image height (where applicable) | + | `image-area` | having given number of pixels (image width * image height) | + | `image-aspect-ratio` | having given aspect ratio (image width / image height) | + | `image-ar` | alias of `image-aspect-ratio` | + | `width` | alias of `image-width` | + | `height` | alias of `image-height` | + | `area` | alias of `image-area` | + | `ar` | alias of `image-aspect-ratio` | + | `aspect-ratio` | alias of `image-aspect-ratio` | + | `creation-date` | posted at given date | + | `creation-time` | alias of `creation-date` | + | `date` | alias of `creation-date` | + | `time` | alias of `creation-date` | + | `last-edit-date` | edited at given date | + | `last-edit-time` | alias of `last-edit-date` | + | `edit-date` | alias of `last-edit-date` | + | `edit-time` | alias of `last-edit-date` | + | `comment-date` | commented at given date | + | `comment-time` | alias of `comment-date` | + | `fav-date` | last favorited at given date | + | `fav-time` | alias of `fav-date` | + | `feature-date` | featured at given date | + | `feature-time` | alias of `feature-time` | + | `safety` | having given safety. `` can be either `safe`, `sketchy` (or `questionable`) or `unsafe`. | + | `rating` | alias of `safety` | **Sort style tokens** @@ -1097,7 +1117,7 @@ data. ## Listing comments - **Request** - `GET /comments/?page=&pageSize=&query=` + `GET /comments/?offset=&limit=&query=` - **Output** @@ -1286,7 +1306,7 @@ data. ## Listing users - **Request** - `GET /users/?page=&pageSize=&query=` + `GET /users/?offset=&limit=&query=` - **Output** @@ -1475,6 +1495,112 @@ data. Deletes existing user. +## Listing user 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 user tokens for the given user. + +## Creating user token +- **Request** + + `POST /user-token/` + +- **Input** + + ```json5 + { + "enabled": , // optional + "note": , // optional + "expirationTime": // optional + } + ``` + +- **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 token +- **Request** + + `PUT /user-token//` + +- **Input** + + ```json5 + { + "version": , + "enabled": , // optional + "note": , // optional + "expirationTime": // 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 user token +- **Request** + + `DELETE /user-token//` + +- **Input** + + ```json5 + { + "version": + } + ``` + +- **Output** + + ```json5 + {} + ``` + +- **Errors** + + - the token does not exist + - privileges are too low + +- **Description** + + Deletes existing user token. + ## Password reset - step 1: mail request - **Request** @@ -1534,7 +1660,7 @@ data. ## Listing snapshots - **Request** - `GET /snapshots/?page=&pageSize=&query=` + `GET /snapshots/?offset=&limit=&query=` - **Output** @@ -1555,14 +1681,14 @@ data. **Named tokens** - | `` | Description | - | ----------------- | --------------------------------------------- | - | `type` | involving given resource type | - | `id` | involving given resource id | - | `date` | created at given date | - | `time` | alias of `date` | - | `operation` | `modified`, `created`, `deleted` or `merged` | - | `user` | name of the user that created given snapshot | + | `` | Description | + | ----------------- | ---------------------------------------------------------------- | + | `type` | involving given resource type | + | `id` | involving given resource id | + | `date` | created at given date | + | `time` | alias of `date` | + | `operation` | `modified`, `created`, `deleted` or `merged` | + | `user` | name of the user that created given snapshot (accepts wildcards) | **Sort style tokens** @@ -1707,6 +1833,38 @@ 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": , + "note": , + "enabled": , + "expirationTime": , + "version": , + "creationTime": , + "lastEditTime": , + "lastUsageTime": +} +``` + +**Field meaning** +- ``: micro user. See [micro user](#micro-user). +- ``: 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 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. +- ``: the last time this token was used during a login involving `?bump-login`, formatted as per RFC 3339. + ## Tag category **Description** @@ -1761,16 +1919,23 @@ A single tag. Tags are used to let users search for posts. - ``: a list of tag names (aliases). Tagging a post with any name will automatically assign the first name from this list. - ``: the name of the category the given tag belongs to. -- ``: a list of implied tag names. Implied tags are automatically - appended by the web client on usage. -- ``: a list of suggested tag names. Suggested tags are shown to - the user by the web client on usage. +- ``: a list of implied tags, serialized as [micro + tag resource](#micro-tag). Implied tags are automatically appended by the web + client on usage. +- ``: a list of suggested tags, serialized as [micro + tag resource](#micro-tag). Suggested tags are shown to the user by the web + client on usage. - ``: time the tag was created, formatted as per RFC 3339. - ``: time the tag was edited, formatted as per RFC 3339. - ``: the number of posts the tag was used in. - ``: the tag description (instructions how to use, history etc.) The client should render is as Markdown. +## Micro tag +**Description** + +A [tag resource](#tag) stripped down to `names`, `category` and `usages` fields. + ## Post **Description** @@ -1809,12 +1974,12 @@ One file together with its metadata posted to the site. "lastFeatureTime": , "favoritedBy": , "hasCustomThumbnail": , - "mimeType": - "comments": { + "mimeType": , + "comments": [ , , - } + ] } ``` @@ -1851,7 +2016,8 @@ One file together with its metadata posted to the site. - ``: where the post thumbnail is located. - ``: various flags such as whether the post is looped, represented as array of plain strings. -- ``: list of tag names the post is tagged with. +- ``: list of tags the post is tagged with, serialized as [micro + tag resource](#micro-tag). - ``: a list of related posts, serialized as [micro post resources](#micro-post). Links to related posts are shown to the user by the web client. @@ -2161,9 +2327,9 @@ A result of search operation that involves paging. ```json5 { - "query": , // same as in input - "page": , // same as in input - "pageSize": , + "query": , // same as in input + "offset": , // same as in input + "limit": , "total": , "results": [ , @@ -2176,7 +2342,7 @@ A result of search operation that involves paging. **Field meaning** - ``: the query passed in the original request that contains standard [search query](#search). -- ``: the page number, passed in the original request. +- ``: the record starting offset, passed in the original request. - ``: number of records on one page. - ``: how many resources were found. To get the page count, divide this number by ``. @@ -2253,6 +2419,9 @@ Date/time values can be of following form: Some fields, such as user names, can take wildcards (`*`). +You can escape special characters such as `:` and `-` by prepending them with a +backslash: `\\`. + **Example** Searching for posts with following query: @@ -2261,3 +2430,8 @@ Searching for posts with following query: will show flash files tagged as sea, that were liked by seven people at most, uploaded by user Pirate. + +Searching for posts with `re:zero` will show an error message about unknown +named token. + +Searching for posts with `re\:zero` will show posts tagged with `re:zero`. diff --git a/INSTALL-OLD.md b/INSTALL-OLD.md new file mode 100644 index 00000000..f67796b2 --- /dev/null +++ b/INSTALL-OLD.md @@ -0,0 +1,212 @@ +**This installation guide is deprecated and might be out +of date! It is recommended that you deploy using +[Docker](https://github.com/rr-/szurubooru/blob/master/INSTALL.md) +instead.** + +This guide assumes Arch Linux. Although exact instructions for other +distributions are different, the steps stay roughly the same. + +### Installing hard dependencies + +```console +user@host:~$ sudo pacman -S postgresql +user@host:~$ sudo pacman -S python +user@host:~$ sudo pacman -S python-pip +user@host:~$ sudo pacman -S ffmpeg +user@host:~$ sudo pacman -S npm +user@host:~$ sudo pacman -S elasticsearch +user@host:~$ sudo pip install virtualenv +user@host:~$ python --version +Python 3.5.1 +``` + +The reason `ffmpeg` is used over, say, `ImageMagick` or even `PIL` is because of +Flash and video posts. + + + +### Setting up a database + +First, basic `postgres` configuration: + +```console +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: + +```console +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';" +``` + + + +### Setting up elasticsearch + +```console +user@host:~$ sudo systemctl start elasticsearch +user@host:~$ sudo systemctl enable elasticsearch +``` + +### Preparing environment + +Getting `szurubooru`: + +```console +user@host:~$ git clone https://github.com/rr-/szurubooru.git szuru +user@host:~$ cd szuru +``` + +Installing frontend dependencies: + +```console +user@host:szuru$ cd client +user@host:szuru/client$ npm install +``` + +`npm` sandboxes dependencies by default, i.e. installs them to +`./node_modules`. This is good, because it avoids polluting the system with the +project's dependencies. To make Python work the same way, we'll use +`virtualenv`. Installing backend dependencies with `virtualenv` looks like +this: + +```console +user@host:szuru/client$ cd ../server +user@host:szuru/server$ virtualenv python_modules # consistent with node_modules +user@host:szuru/server$ source python_modules/bin/activate # enters the sandbox +(python_modules) user@host:szuru/server$ pip install -r requirements.txt # installs the dependencies +``` + + + +### Preparing `szurubooru` for first run + +1. Compile the frontend: + + ```console + user@host:szuru$ cd client + user@host:szuru/client$ node build.js + ``` + + You can include the flags `--no-transpile` to disable the JavaScript + transpiler, which provides compatibility with older browsers, and + `--debug` to generate JS source mappings. + +2. Configure things: + + ```console + user@host:szuru/client$ cd .. + user@host:szuru$ mv server/config.yaml.dist . + user@host:szuru$ cp config.yaml.dist config.yaml + user@host:szuru$ vim config.yaml + ``` + + Pay extra attention to these fields: + + - data directory, + - data URL, + - database, + - the `smtp` section. + +3. Upgrade the database: + + ```console + user@host:szuru/client$ cd ../server + user@host:szuru/server$ source python_modules/bin/activate + (python_modules) user@host:szuru/server$ alembic upgrade head + ``` + + `alembic` should have been installed during installation of `szurubooru`'s + dependencies. + +4. Run the tests: + + ```console + (python_modules) user@host:szuru/server$ pytest + ``` + +It is recommended to rebuild the frontend after each change to configuration. + + + +### 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. + +The static files are located in the `client/public/data` directory and are +meant to be exposed directly to the end users. + +The API should be exposed using WSGI server such as `waitress`, `gunicorn` or +similar. Other configurations might be possible but I didn't pursue them. + +API calls are made to the relative URL `/api/`. Your HTTP server should be +configured to proxy this URL format to the WSGI server. Some users may prefer +to use a dedicated reverse proxy for this, to incorporate additional features +such as load balancing and SSL. + +Note that the API URL in the virtual host configuration needs to be the same as +the one in the `config.yaml`, so that client knows how to access the backend! + +#### Example + +In this example: + +- The booru is accessed from `http://example.com/` +- The API is accessed from `http://example.com/api` +- The API server listens locally on port 6666, and is proxied by nginx +- The static files are served from `/srv/www/booru/client/public/data` + +**nginx configuration**: + +```nginx +server { + listen 80; + server_name example.com; + + location ~ ^/api$ { + return 302 /api/; + } + location ~ ^/api/(.*)$ { + if ($request_uri ~* "/api/(.*)") { # preserve PATH_INFO as-is + proxy_pass http://127.0.0.1:6666/$1; + } + } + location / { + root /srv/www/booru/client/public; + try_files $uri /index.htm; + } +} +``` + +**`config.yaml`**: + +```yaml +data_url: 'http://example.com/data/' +data_dir: '/srv/www/booru/client/public/data' +``` + +To run the server using `waitress`: + +```console +user@host:szuru/server$ source python_modules/bin/activate +(python_modules) user@host:szuru/server$ pip install waitress +(python_modules) user@host:szuru/server$ waitress-serve --port 6666 szurubooru.facade:app +``` + +or `gunicorn`: + +```console +user@host:szuru/server$ source python_modules/bin/activate +(python_modules) user@host:szuru/server$ pip install gunicorn +(python_modules) user@host:szuru/server$ gunicorn szurubooru.facade:app -b 127.0.0.1:6666 +``` diff --git a/INSTALL.md b/INSTALL.md index c4ccdcc3..b67dd25f 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -1,187 +1,64 @@ -This guide assumes Arch Linux. Although exact instructions for other -distributions are different, the steps stay roughly the same. +This assumes that you have Docker and Docker Compose already installed. -### Installing hard dependencies +### Prepare things -```console -user@host:~$ sudo pacman -S postgresql -user@host:~$ sudo pacman -S python -user@host:~$ sudo pacman -S python-pip -user@host:~$ sudo pacman -S ffmpeg -user@host:~$ sudo pacman -S npm -user@host:~$ sudo pacman -S elasticsearch -user@host:~$ sudo pip install virtualenv -user@host:~$ python --version -Python 3.5.1 -``` - -The reason `ffmpeg` is used over, say, `ImageMagick` or even `PIL` is because of -Flash and video posts. - - - -### Setting up a database - -First, basic `postgres` configuration: - -```console -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: - -```console -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';" -``` - - - -### Setting up elasticsearch - -```console -user@host:~$ sudo systemctl start elasticsearch -user@host:~$ sudo systemctl enable elasticsearch -``` - -### Preparing environment - -Getting `szurubooru`: - -```console -user@host:~$ git clone https://github.com/rr-/szurubooru.git szuru -user@host:~$ cd szuru -``` - -Installing frontend dependencies: - -```console -user@host:szuru$ cd client -user@host:szuru/client$ npm install -``` - -`npm` sandboxes dependencies by default, i.e. installs them to -`./node_modules`. This is good, because it avoids polluting the system with the -project's dependencies. To make Python work the same way, we'll use -`virtualenv`. Installing backend dependencies with `virtualenv` looks like -this: - -```console -user@host:szuru/client$ cd ../server -user@host:szuru/server$ virtualenv python_modules # consistent with node_modules -user@host:szuru/server$ source python_modules/bin/activate # enters the sandbox -(python_modules) user@host:szuru/server$ pip install -r requirements.txt # installs the dependencies -``` - - - -### Preparing `szurubooru` for first run - -1. Configure things: +1. Getting `szurubooru`: ```console - user@host:szuru$ cp config.yaml.dist config.yaml - user@host:szuru$ vim config.yaml + user@host:~$ git clone https://github.com/rr-/szurubooru.git szuru + user@host:~$ cd szuru + ``` +2. Configure the application: + + ```console + user@host:szuru$ cp server/config.yaml.dist config.yaml + user@host:szuru$ edit config.yaml ``` Pay extra attention to these fields: - - base URL, - - API URL, - - data directory, - - data URL, - - database, + - secret - the `smtp` section. -2. Compile the frontend: + You can omit lines when you want to use the defaults of that field. + +3. Configure Docker Compose: ```console - user@host:szuru$ cd client - user@host:szuru/client$ npm run build + user@host:szuru$ cp docker-compose.yml.example docker-compose.yml + user@host:szuru$ edit docker-compose.yml ``` -3. Upgrade the database: + Read the comments to guide you. For production use, it is *important* + that you configure the volumes appropriately to avoid data loss. + +### Running the Application + +1. Configurations for ElasticSearch: + + You may need to raise the `vm.max_map_count` + parameter to at least `262144` in order for the + ElasticSearch container to function. Instructions + on how to do so are provided + [here](https://www.elastic.co/guide/en/elasticsearch/reference/current/docker.html#docker-cli-run-prod-mode). + +2. Build or update the containers: ```console - user@host:szuru/client$ cd ../server - user@host:szuru/server$ source python_modules/bin/activate - (python_modules) user@host:szuru/server$ alembic upgrade head + user@host:szuru$ docker-compose pull + user@host:szuru$ docker-compose build --pull ``` - `alembic` should have been installed during installation of `szurubooru`'s - dependencies. + This will build both the frontend and backend containers, and may take + some time. -4. Run the tests: +3. Start and stop the the application ```console - (python_modules) user@host:szuru/server$ ./test + # To start: + user@host:szuru$ docker-compose up -d + # To monitor (CTRL+C to exit): + user@host:szuru$ docker-compose logs -f + # To stop + user@host:szuru$ docker-compose down ``` - -It is recommended to rebuild the frontend after each change to configuration. - - - -### 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 (from within `virtualenv`) install `waitress` - with `pip install waitress` and then start `szurubooru` with `./host-waitress` - from within the `server/` directory (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 `client/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.yaml`, so that client knows how to access the backend! - -#### Example - -**nginx configuration** - wiring API `http://great.dude/api/` to -`localhost:6666` to avoid fiddling with CORS: - -```nginx -server { - listen 80; - server_name great.dude; - merge_slashes off; # to support post tags such as /// - - location ~ ^/api$ { - return 302 /api/; - } - location ~ ^/api/(.*)$ { - proxy_pass http://127.0.0.1:6666/$1$is_args$args; - } - location / { - root /home/rr-/src/maintained/szurubooru/client/public; - try_files $uri /index.htm; - } -} -``` - -**`config.yaml`**: - -```yaml -api_url: 'http://big.dude/api/' -base_url: 'http://big.dude/' -data_url: 'http://big.dude/data/' -data_dir: '/home/rr-/src/maintained/szurubooru/client/public/data' -``` - -Then the backend is started with `host-waitress` from within `virtualenv` and -`./server/` directory. diff --git a/README.md b/README.md index 92b694ea..4f9c0f74 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,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 @@ -33,6 +34,7 @@ scrubbing](http://sjp.pwn.pl/sjp/;2527372). It is pronounced as *shoorubooru*. - FFmpeg - node.js +It is recommended that you use Docker for deployment. [See installation instructions.](https://github.com/rr-/szurubooru/blob/master/INSTALL.md) ## Screenshots diff --git a/client/.babelrc b/client/.babelrc index 9d8d5165..684fff67 100644 --- a/client/.babelrc +++ b/client/.babelrc @@ -1 +1 @@ -{ "presets": ["es2015"] } +{ "presets": ["env"] } diff --git a/client/.dockerignore b/client/.dockerignore new file mode 100644 index 00000000..1313ed68 --- /dev/null +++ b/client/.dockerignore @@ -0,0 +1,6 @@ +node_modules/* +package-lock.json + +Dockerfile +.dockerignore +**/.gitignore diff --git a/client/Dockerfile b/client/Dockerfile new file mode 100644 index 00000000..49e0ab63 --- /dev/null +++ b/client/Dockerfile @@ -0,0 +1,31 @@ +FROM node:9 as builder +WORKDIR /opt/app + +COPY package.json ./ +RUN npm install + +COPY . ./ + +ARG BUILD_INFO="docker-latest" +ARG CLIENT_BUILD_ARGS="" +RUN node build.js ${CLIENT_BUILD_ARGS} + +RUN find public/ -type f -size +5k -print0 | xargs -0 -- gzip -6 -k + + +FROM nginx:alpine +WORKDIR /var/www + +RUN \ + # Create init file + echo "#!/bin/sh" >> /init && \ + echo 'sed -i "s|__BACKEND__|${BACKEND_HOST}|" /etc/nginx/nginx.conf' \ + >> /init && \ + echo 'exec nginx -g "daemon off;"' >> /init && \ + chmod a+x /init + +CMD ["/init"] +VOLUME ["/data"] + +COPY nginx.conf.docker /etc/nginx/nginx.conf +COPY --from=builder /opt/app/public/ . diff --git a/client/build.js b/client/build.js index a803acba..e5513842 100644 --- a/client/build.js +++ b/client/build.js @@ -5,20 +5,6 @@ const glob = require('glob'); const path = require('path'); const util = require('util'); const execSync = require('child_process').execSync; -const camelcase = require('camelcase'); - -function convertKeysToCamelCase(input) { - let result = {}; - Object.keys(input).map((key, _) => { - const value = input[key]; - if (value !== null && value.constructor == Object) { - result[camelcase(key)] = convertKeysToCamelCase(value); - } else { - result[camelcase(key)] = value; - } - }); - return result; -} function readTextFile(path) { return fs.readFileSync(path, 'utf-8'); @@ -29,37 +15,27 @@ function writeFile(path, content) { } function getVersion() { - return execSync('git describe --always --dirty --long --tags') - .toString() - .trim(); + let build_info = process.env.BUILD_INFO; + if (build_info) { + return build_info.trim(); + } else { + try { + build_info = execSync('git describe --always --dirty --long --tags') + .toString(); + } catch (e) { + console.warn('Cannot find build version'); + return 'unknown'; + } + return build_info.trim(); + } } function getConfig() { - const yaml = require('js-yaml'); - const merge = require('merge'); - const camelcaseKeys = require('camelcase-keys'); - - function parseConfigFile(path) { - let result = yaml.load(readTextFile(path, 'utf-8')); - return convertKeysToCamelCase(result); - } - - let config = parseConfigFile('../config.yaml.dist'); - - try { - const localConfig = parseConfigFile('../config.yaml'); - config = merge.recursive(config, localConfig); - } catch (e) { - console.warn('Local config does not exist, ignoring'); - } - - config.canSendMails = !!config.smtp.host; - delete config.secret; - delete config.smtp; - delete config.database; - config.meta = { - version: getVersion(), - buildDate: new Date().toUTCString(), + let config = { + meta: { + version: getVersion(), + buildDate: new Date().toUTCString() + } }; return config; @@ -70,11 +46,11 @@ function copyFile(source, target) { } function minifyJs(path) { - return require('uglify-js').minify(path, {compress: {unused: false}}).code; + return require('terser').minify(fs.readFileSync(path, 'utf-8'), {compress: {unused: false}}).code; } function minifyCss(css) { - return require('csso').minify(css); + return require('csso').minify(css).css; } function minifyHtml(html) { @@ -85,15 +61,11 @@ function minifyHtml(html) { }).trim(); } -function bundleHtml(config) { +function bundleHtml() { const underscore = require('underscore'); const babelify = require('babelify'); const baseHtml = readTextFile('./html/index.htm', 'utf-8'); - const finalHtml = baseHtml - .replace( - /()(.*)(<\/title>)/, - util.format('$1%s$3', config.name)); - writeFile('./public/index.htm', minifyHtml(finalHtml)); + writeFile('./public/index.htm', minifyHtml(baseHtml)); glob('./html/**/*.tpl', {}, (er, files) => { let compiledTemplateJs = '\'use strict\'\n'; @@ -143,7 +115,7 @@ function bundleCss() { }); } -function bundleJs(config) { +function bundleJs() { const browserify = require('browserify'); const external = [ 'underscore', @@ -170,7 +142,7 @@ function bundleJs(config) { for (let lib of external) { b.require(lib); } - if (config.transpile) { + if (!process.argv.includes('--no-transpile')) { b.add(require.resolve('babel-polyfill')); } writeJsBundle( @@ -179,15 +151,15 @@ function bundleJs(config) { if (!process.argv.includes('--no-app-js')) { let outputFile = fs.createWriteStream('./public/js/app.min.js'); - let b = browserify({debug: config.debug}); - if (config.transpile) { + let b = browserify({debug: process.argv.includes('--debug')}); + if (!process.argv.includes('--no-transpile')) { b = b.transform('babelify'); } writeJsBundle( b.external(external).add(files), './public/js/app.min.js', 'Bundled app JS', - !config.debug); + !process.argv.includes('--debug')); } }); } @@ -217,11 +189,11 @@ const config = getConfig(); bundleConfig(config); bundleBinaryAssets(); if (!process.argv.includes('--no-html')) { - bundleHtml(config); + bundleHtml(); } if (!process.argv.includes('--no-css')) { bundleCss(); } if (!process.argv.includes('--no-js')) { - bundleJs(config); + bundleJs(); } diff --git a/client/css/colors.styl b/client/css/colors.styl index ed68e410..3be05fe3 100644 --- a/client/css/colors.styl +++ b/client/css/colors.styl @@ -55,3 +55,5 @@ $hovered-first-note-point-color = red $safety-safe = #88D488 $safety-sketchy = #F3D75F $safety-unsafe = #F3985F +$scrollbar-thumb-color = $main-color +$scrollbar-bg-color = $input-enabled-background-color diff --git a/client/css/comment-control.styl b/client/css/comment-control.styl index 6b730172..7e98171f 100644 --- a/client/css/comment-control.styl +++ b/client/css/comment-control.styl @@ -3,7 +3,6 @@ $comment-header-background-color = $top-navigation-color $comment-border-color = #DDD .comment-container - margin: 0 0 1em 0 padding: 0 0 0 60px .avatar @@ -124,7 +123,7 @@ $comment-border-color = #DDD font-family: 'MS PGothic', 'MS Pゴシック', 'IPAMonaPGothic', 'Trebuchet MS', Verdana, Futura, Arial, Helvetica, sans-serif background: #fbfbfb color: #111 - font-size: 12pt + font-size: 1em line-height: 1 margin: 0 padding: 4px diff --git a/client/css/comment-list-control.styl b/client/css/comment-list-control.styl index 63459d64..fe847245 100644 --- a/client/css/comment-list-control.styl +++ b/client/css/comment-list-control.styl @@ -2,3 +2,8 @@ list-style-type: none margin: 0 padding: 0 + + >li + margin-bottom: 1em + &:last-child + margin-bottom: 0 diff --git a/client/css/comment-list-view.styl b/client/css/comment-list-view.styl index 697b372b..bd50beb8 100644 --- a/client/css/comment-list-view.styl +++ b/client/css/comment-list-view.styl @@ -1,15 +1,24 @@ +@import colors +$comment-border-color = $top-navigation-color + .global-comment-list text-align: left &>ul list-style-type: none - margin: 1em 0 + margin: 1em 0 0 padding: 0 + &>li + margin-top: 2em + padding-top: 2em + border-top: 3px solid $comment-border-color + &:first-child + margin-top: 0 + padding-top: 0 + border-top: none + @media (max-width: 700px) - &>li - margin-bottom: 5em - padding: 1vw .post-thumbnail margin-bottom: 1em .thumbnail @@ -19,7 +28,6 @@ @media (min-width: 700px) &>li padding-left: 13em - margin-bottom: 2em .post-thumbnail float: left margin: 0 0 1em -13em diff --git a/client/css/core-forms.styl b/client/css/core-forms.styl index 8323d16e..bed63e3b 100644 --- a/client/css/core-forms.styl +++ b/client/css/core-forms.styl @@ -17,6 +17,8 @@ form .input li:first-child padding-top: 0 margin-top: 0 + +form:not(.horizontal) .hint margin-top: 0.2em margin-bottom: 0 @@ -29,13 +31,22 @@ form.horizontal margin-bottom: 1em .input, .buttons, ul display: inline-block - vertical-align: middle + vertical-align: top margin: 0 padding: 0 input - vertical-align: middle + vertical-align: top .buttons margin-right: 0.5em + @media (max-width: 1000px) + display: block + .input, .buttons, ul + display: block + margin-top: 0.5em + &:first-child + margin-top: 0 + .buttons + margin-right: 0 @@ -126,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 */ @@ -170,13 +213,25 @@ input:disabled cursor: not-allowed label.color + white-space: nowrap position: relative + display: flex input[type=text] + margin-right: 0.25em + width: auto + .preview + display: inline-block text-align: center - pointer-events: none - input[type=color] - position: absolute - opacity: 0 + padding: 0 0.5em + border: 2px solid black + &:after + content: 'A' + .background-preview + border-right: 0 + color: transparent + .text-preview + border-left: 0 + form.show-validation .input input:invalid @@ -199,10 +254,13 @@ input[type=submit] cursor: pointer font-size: 100% padding: 0.2em 0.7em + border-radius: 0 border: 2px solid $button-enabled-background-color background: $button-enabled-background-color color: $button-enabled-text-color outline: 0 /* something on Chrome */ + -moz-appearance: none + -webkit-appearance: none &:disabled cursor: default @@ -231,25 +289,26 @@ input::-moz-focus-inner * File dropper */ .file-dropper-holder - display: flex - flex-wrap: wrap .file-dropper display: block - width: 100% background: $window-color border: 3px dashed #eee padding: 0.3em 0.5em line-height: 140% text-align: center cursor: pointer + overflow: hidden word-wrap: break-word - input + .url-holder + display: flex margin-top: 0.5em - width: auto - flex: 1 - button - margin-top: 0.5em - width: 8em + input, button + min-width: 0 /* firefox being sassy */ + width: auto !important /* don't inherit anything weird */ + input + flex: 1 + button + margin-left: 0.5em input[type=file]:disabled+.file-dropper cursor: default diff --git a/client/css/core-general.styl b/client/css/core-general.styl index 9209bc30..75e82241 100644 --- a/client/css/core-general.styl +++ b/client/css/core-general.styl @@ -21,19 +21,28 @@ body margin: 0 color: $text-color font-family: 'Open Sans', sans-serif - font-size: 12pt - line-height: 18pt + font-size: 1em + line-height: 1.4 @media (max-width: 800px) - font-size: 10pt - line-height: 15pt + font-size: 0.875em @media (max-width: 1200px) - font-size: 11pt - line-height: 16.5pt + font-size: 0.95em h1, h2, h3 font-weight: normal margin-bottom: 1em +h1 + font-size: 2em + +h2 + font-size: 1.5em + +p, +ol, +ul + margin: 1em 0 + th font-weight: normal @@ -61,8 +70,10 @@ form .fa-question-circle-o vertical-align: middle #content-holder - padding: 1.5vw + padding: 1.5em text-align: center + @media (max-width: 1000px) + padding: 1em >.content-wrapper box-sizing: border-box /* make max-width: 100% on this element include padding */ text-align: left @@ -70,9 +81,26 @@ form .fa-question-circle-o margin: 0 auto >*:first-child, form h1 margin-top: 0 + nav.buttons + ul + display: block + max-width: 100% + white-space: nowrap + overflow-x: auto + &::-webkit-scrollbar + height: 6px + background-color: $scrollbar-bg-color + &::-webkit-scrollbar-thumb + background-color: $scrollbar-thumb-color >.content-wrapper:not(.transparent) background: $top-navigation-color - padding: 2vw + padding: 1.8em + @media (max-width: 1000px) + padding: 1.5em + .content, + .content .subcontent + >*:last-child + margin-bottom: 0 hr border: 0 @@ -125,6 +153,39 @@ nav li display: inline-block float: left + a + padding: 0 1.5em + #mobile-navigation-toggle + display: none + width: 100% + padding: 0 1em + line-height: 2.3em + font-family: inherit + border: none + background: none + color: $active-tab-text-color + .site-name + display: block + float: left + max-width: 50vw + overflow: hidden + text-overflow: ellipsis + .toggle-icon + display: block + float: right + @media (max-width: 1000px) + text-align: left + li + display: none + float: none + a + display: block + padding: 0 1em + #mobile-navigation-toggle + display: block + &.opened + li + display: block ul li[data-name=account], ul li[data-name=register], ul li[data-name=login], @@ -141,6 +202,8 @@ nav margin-right: 0.6em margin-left: calc(0.6em - 1.2em) float: left + @media (max-width: 1000px) + display: none a .access-key text-decoration: underline @@ -194,6 +257,14 @@ a .access-key margin-top: 0 !important margin-bottom: 0 !important +.table-wrap + overflow-x: auto + &::-webkit-scrollbar + height: 6px + background-color: $scrollbar-bg-color + &::-webkit-scrollbar-thumb + background-color: $scrollbar-thumb-color + /* hack to prevent text from being copied */ [data-pseudo-content]:before { content: attr(data-pseudo-content) diff --git a/client/css/expander-control.styl b/client/css/expander-control.styl index 7f2e0b7b..620ec250 100644 --- a/client/css/expander-control.styl +++ b/client/css/expander-control.styl @@ -16,7 +16,7 @@ color: mix($text-color, $inactive-link-color) font-size: 120% i - font-size: 12pt + font-size: 1em color: $inactive-link-color float: right line-height: 2em diff --git a/client/css/help-view.styl b/client/css/help-view.styl index d90e0c57..0c98d59a 100644 --- a/client/css/help-view.styl +++ b/client/css/help-view.styl @@ -16,6 +16,10 @@ font-size: 1.6em &:first-child margin-top: 0 + @media (max-width: 1000px) + margin-top: 1.5em + &:first-child + margin-top: 0 nav ul margin: 0 auto diff --git a/client/css/home-view.styl b/client/css/home-view.styl index e17e4bb5..729fac0c 100644 --- a/client/css/home-view.styl +++ b/client/css/home-view.styl @@ -6,13 +6,16 @@ margin-bottom: 1em h1 line-height: initial - font-size: 30pt + font-size: 2.5em margin: 0 - .message - margin-bottom: 2em + .messages + text-align: center + .message + margin: 0 auto 2em auto form + display: inline-block width: auto vertical-align: middle margin: 0 0 2em 0 @@ -31,6 +34,8 @@ display: flex align-items: center justify-content: center + &:empty + margin-bottom: 0 nav a @@ -50,6 +55,8 @@ li display: inline white-space: nowrap + @media (max-width: 800px) + display: block .sep word-spacing: 1.1em background-repeat: no-repeat diff --git a/client/css/pager.styl b/client/css/pager.styl index df35f3e8..3976404c 100644 --- a/client/css/pager.styl +++ b/client/css/pager.styl @@ -8,7 +8,7 @@ .page position: relative .page-header - margin: 0.5em 0.5em 0.5em 0 + margin: 0.5em 0 position: relative &:before display: block diff --git a/client/css/password-reset.styl b/client/css/password-reset.styl new file mode 100644 index 00000000..47e32f3f --- /dev/null +++ b/client/css/password-reset.styl @@ -0,0 +1,2 @@ +#password-reset + max-width: 30em diff --git a/client/css/post-list-view.styl b/client/css/post-list-view.styl index 4e797e0c..03806b88 100644 --- a/client/css/post-list-view.styl +++ b/client/css/post-list-view.styl @@ -54,33 +54,66 @@ .icon:not(:first-of-type) margin-left: 1em - .masstag + .edit-overlay position: absolute top: 0.5em left: 0.5em - display: inline-block - padding: 0.5em - box-sizing: border-box - border: 0 - &:after + + .tag-flipper display: inline-block - width: 1em - height: 1em + padding: 0.5em + box-sizing: border-box + border: 0 + &:after + display: inline-block + width: 1em + height: 1em + text-align: center + line-height: 1em + font-size: 1.6em + &.tagged + background: rgba(0, 230, 0, 0.7) + &:after + color: white + content: '-' + &:not(.tagged) + background: rgba(255, 0, 0, 0.7) + &:after + color: white + content: '+' + &[data-disabled] + background: rgba(200, 200, 200, 0.7) + + .safety-flipper a + display: inline-block + margin: 0.1em + box-sizing: border-box + border: 0 + display: inline-block + width: 1.2em + height: 1.2em text-align: center line-height: 1em - font-size: 20pt - &.tagged - background: rgba(0, 230, 0, 0.7) - &:after - color: white - content: '-' - &:not(.tagged) - background: rgba(255, 0, 0, 0.7) - &:after - color: white - content: '+' - &[data-disabled] - background: rgba(200, 200, 200, 0.7) + font-size: 1.6em + border: 3px solid + &.safety-safe + background-color: darken($safety-safe, 5%) + border-color: @background-color + &:not(.active) + background-color: alpha(@background-color, 0.3) + &.safety-sketchy + background-color: $safety-sketchy + border-color: @background-color + &:not(.active) + background-color: alpha(@background-color, 0.3) + &.safety-unsafe + background-color: $safety-unsafe + border-color: @background-color + &:not(.active) + background-color: alpha(@background-color, 0.3) + &[data-disabled] + background: rgba(200, 200, 200, 0.7) + .thumbnail background-position: 50% 30% @@ -112,29 +145,59 @@ margin-bottom: 0.75em * vertical-align: top + @media (max-width: 1000px) + display: block input margin-bottom: 0.25em margin-right: 0.25em input[name=search-text] width: 25em - input[name=masstag] - width: 12em - .masstag-hint, .open-masstag - margin-right: 1em + @media (max-width: 1000px) + display: block + width: 100% + margin-bottom: 0.5em .append + vertical-align: middle font-size: 0.95em color: $inactive-link-color - .masstag - &:not(.active) + .bulk-edit + &:not(.opened) + .close + display: none + &.opened + .open + display: none + &.hidden + display: none + .bulk-edit-tags + &.opened + .hint + @media (max-width: 1000px) + display: block + margin-bottom: 0.5em + &:not(.opened) [type=text], - .start-tagging, - .stop-tagging + .start display: none - .masstag-hint - display: none - &.active - .open-masstag + .hint display: none + input[name=tag] + width: 12em + @media (max-width: 1000px) + display: block + width: 100% + margin-bottom: 0.5em + .append + &.open, + &.hint + @media (max-width: 1000px) + margin-left: 0 + .hint + margin-right: 1em + .bulk-edit-safety + .append + @media (max-width: 1000px) + margin-left: 0 .safety margin-right: 0.25em diff --git a/client/css/post-main-view.styl b/client/css/post-main-view.styl index 689ec73c..9e596417 100644 --- a/client/css/post-main-view.styl +++ b/client/css/post-main-view.styl @@ -33,6 +33,8 @@ i font-size: 140% text-align: center + @media (max-width: 800px) + margin-top: 2em >.content width: 100% @@ -50,6 +52,7 @@ order: 2 min-width: 100% max-width: 0 + margin-right: 0 >.content order: 1 @@ -130,10 +133,18 @@ display: inline-block .management - li + ul + list-style-type: none margin: 0 + padding: 0 + li + margin: 0 + padding: 0 - label + form + width: auto + + label:not(.file-dropper) margin-bottom: 0.3em display: block diff --git a/client/css/post-upload.styl b/client/css/post-upload.styl index 4da53a77..147df6bc 100644 --- a/client/css/post-upload.styl +++ b/client/css/post-upload.styl @@ -22,6 +22,8 @@ $cancel-button-color = tomato .file-dropper font-size: 150% padding: 2em + small + font-size: 60% input[type=submit] margin-top: 1em diff --git a/client/css/snapshots-list-view.styl b/client/css/snapshots-list-view.styl index a858a07d..b059a64a 100644 --- a/client/css/snapshots-list-view.styl +++ b/client/css/snapshots-list-view.styl @@ -8,11 +8,16 @@ $snapshot-merged-background-color = #FEC ul margin: 0 auto + padding: 0 width: 100% max-width: 35em list-style-type: none li + margin-bottom: 1em + &:last-child + margin-bottom: 0 + .time float: right @@ -39,6 +44,3 @@ $snapshot-merged-background-color = #FEC background: $snapshot-merged-background-color &+.details background: lighten($snapshot-merged-background-color, 50%) - - div.details - margin-bottom: 2em diff --git a/client/css/tag-categories-view.styl b/client/css/tag-categories-view.styl index 31c58380..b8a91802 100644 --- a/client/css/tag-categories-view.styl +++ b/client/css/tag-categories-view.styl @@ -2,7 +2,7 @@ .content-wrapper.tag-categories width: 100% - max-width: 40em + max-width: 45em table border-spacing: 0 width: 100% @@ -11,11 +11,18 @@ td, th padding: .4em &.color - text-align: center + input[type=text] + width: 8em &.usages text-align: center &.remove, &.set-default white-space: pre + th + white-space: nowrap + &:first-child + padding-left: 0 + &:last-child + padding-right: 0 tfoot display: none form diff --git a/client/css/tag-list-view.styl b/client/css/tag-list-view.styl index 8ae7bcb8..4fd167f5 100644 --- a/client/css/tag-list-view.styl +++ b/client/css/tag-list-view.styl @@ -11,6 +11,7 @@ th, td padding: 0.1em 0.5em th + white-space: nowrap background: $top-navigation-color .names width: 28% @@ -46,7 +47,10 @@ form width: auto input[name=search-text] - max-width: 15em + width: 25em + @media (max-width: 1000px) + width: 100% .append + vertical-align: middle font-size: 0.95em color: $inactive-link-color diff --git a/client/css/user-list-view.styl b/client/css/user-list-view.styl index 3d49d941..1fba50b5 100644 --- a/client/css/user-list-view.styl +++ b/client/css/user-list-view.styl @@ -33,7 +33,10 @@ form width: auto input[name=search-text] - max-width: 15em + width: 25em + @media (max-width: 1000px) + width: 100% .append + vertical-align: middle font-size: 0.95em color: $inactive-link-color diff --git a/client/css/user-view.styl b/client/css/user-view.styl index 12cba75e..3cdd29cb 100644 --- a/client/css/user-view.styl +++ b/client/css/user-view.styl @@ -1,3 +1,6 @@ +@import colors +$token-border-color = $active-tab-background-color + #user width: 100% max-width: 35em @@ -37,7 +40,43 @@ height: 1px clear: both + #user-tokens + + .token-flex-container + width: 100% + display: flex; + flex-direction column; + padding-bottom: 0.5em; + + .full-width + width: 100% + + .token-flex-row + display: flex; + flex-direction: row; + justify-content: space-between; + padding: 0.2em; + + .no-wrap + white-space: nowrap; + + .token-input + min-height: 2em; + line-height: 2em; + text-align: center; + + .token-flex-column + display: flex; + flex-direction: column; + + .token-flex-labels + padding-right: 0.5em + + hr + border-top: 3px solid $token-border-color + + form + width: 100%; + #user-delete form width: 100% - - diff --git a/client/html/comment.tpl b/client/html/comment.tpl index 04469c9a..6bea7045 100644 --- a/client/html/comment.tpl +++ b/client/html/comment.tpl @@ -1,7 +1,7 @@ <div class='comment-container'> <div class='avatar'> <% if (ctx.user && ctx.user.name && ctx.canViewUsers) { %> - <a href='/user/<%- encodeURIComponent(ctx.user.name) %>'> + <a href='<%- ctx.formatClientLink('user', ctx.user.name) %>'> <% } %> <%= ctx.makeThumbnail(ctx.user ? ctx.user.avatarUrl : null) %> @@ -23,7 +23,7 @@ <nav class='readonly'><% %><strong><span class='nickname'><% %><% if (ctx.user && ctx.user.name && ctx.canViewUsers) { %><% - %><a href='/user/<%- encodeURIComponent(ctx.user.name) %>'><% + %><a href='<%- ctx.formatClientLink('user', ctx.user.name) %>'><% %><% } %><% %><%- ctx.user ? ctx.user.name : 'Deleted user' %><% diff --git a/client/html/comments_page.tpl b/client/html/comments_page.tpl index 4f956d42..27d7011d 100644 --- a/client/html/comments_page.tpl +++ b/client/html/comments_page.tpl @@ -1,10 +1,10 @@ <div class='global-comment-list'> <ul><!-- - --><% for (let post of ctx.results) { %><!-- + --><% for (let post of ctx.response.results) { %><!-- --><li><!-- --><div class='post-thumbnail'><!-- --><% if (ctx.canViewPosts) { %><!-- - --><a href='/post/<%- encodeURIComponent(post.id) %>'><!-- + --><a href='<%- ctx.formatClientLink('post', post.id) %>'><!-- --><% } %><!-- --><%= ctx.makeThumbnail(post.thumbnailUrl) %><!-- --><% if (ctx.canViewPosts) { %><!-- diff --git a/client/html/endless_pager.tpl b/client/html/endless_pager.tpl index 6870f9a5..4812f96d 100644 --- a/client/html/endless_pager.tpl +++ b/client/html/endless_pager.tpl @@ -1,5 +1,7 @@ <div class='pager'> <div class='page-header-holder'></div> <div class='messages'></div> + <div class='page-guard top'></div> <div class='pages-holder'></div> + <div class='page-guard bottom'></div> </div> diff --git a/client/html/file_dropper.tpl b/client/html/file_dropper.tpl index 9c662010..3123cd01 100644 --- a/client/html/file_dropper.tpl +++ b/client/html/file_dropper.tpl @@ -8,9 +8,19 @@ <% } %> <br/> Or just click on this box. + <% if (ctx.extraText) { %> + <br/> + <small><%= ctx.extraText %></small> + <% } %> </label> <% if (ctx.allowUrls) { %> - <input type='text' name='url' placeholder='Alternatively, paste an URL here.'/> - <button>Add URL</button> + <div class='url-holder'> + <input type='text' name='url' placeholder='<%- ctx.urlPlaceholder %>'/> + <% if (ctx.lock) { %> + <button>Confirm</button> + <% } else { %> + <button>Add URL</button> + <% } %> + </div> <% } %> </div> diff --git a/client/html/help.tpl b/client/html/help.tpl index 5e80725e..995ab0bd 100644 --- a/client/html/help.tpl +++ b/client/html/help.tpl @@ -1,11 +1,11 @@ <div class='content-wrapper' id='help'> <nav class='buttons primary'><!-- --><ul><!-- - --><li data-name='about'><a href='/help/about'>About</a></li><!-- - --><li data-name='keyboard'><a href='/help/keyboard'>Keyboard</a></li><!-- - --><li data-name='search'><a href='/help/search'>Search syntax</a></li><!-- - --><li data-name='comments'><a href='/help/comments'>Comments</a></li><!-- - --><li data-name='tos'><a href='/help/tos'>Terms of service</a></li><!-- + --><li data-name='about'><a href='<%- ctx.formatClientLink('help', 'about') %>'>About</a></li><!-- + --><li data-name='keyboard'><a href='<%- ctx.formatClientLink('help', 'keyboard') %>'>Keyboard</a></li><!-- + --><li data-name='search'><a href='<%- ctx.formatClientLink('help', 'search') %>'>Search syntax</a></li><!-- + --><li data-name='comments'><a href='<%- ctx.formatClientLink('help', 'comments') %>'>Comments</a></li><!-- + --><li data-name='tos'><a href='<%- ctx.formatClientLink('help', 'tos') %>'>Terms of service</a></li><!-- --></ul><!-- --></nav> diff --git a/client/html/help_keyboard.tpl b/client/html/help_keyboard.tpl index 5b12324b..f200ce02 100644 --- a/client/html/help_keyboard.tpl +++ b/client/html/help_keyboard.tpl @@ -33,10 +33,15 @@ shortcuts:</p> <td><kbd>P</kbd></td> <td>Focus first post in post list</td> </tr> + + <tr> + <td><kbd>Delete</kbd></td> + <td>Delete post (while in edit mode)</td> + </tr> </tbody> </table> -<p>Additionally, each item in top navigation can be accessed using feature -called “access keys”. Pressing underlined letter while holding -Shfit or Alt+Shift (depending on your browser) will go to the desired page -(most browsers) or focus the link (IE).</p> +<p>Additionally, each item in the top navigation can be accessed using a +feature called “access keys”. Pressing the underlined letter while +holding Shift or Alt+Shift (depending on your browser) will go to the desired +page (most browsers) or focus the link (IE).</p> diff --git a/client/html/help_search.tpl b/client/html/help_search.tpl index 8c22d73e..70737893 100644 --- a/client/html/help_search.tpl +++ b/client/html/help_search.tpl @@ -1,9 +1,9 @@ <nav class='buttons secondary'><!-- --><ul><!-- - --><li data-name='default'><a href='/help/search'>General</a></li><!-- - --><li data-name='posts'><a href='/help/search/posts'>Posts</a></li><!-- - --><li data-name='users'><a href='/help/search/users'>Users</a></li><!-- - --><li data-name='tags'><a href='/help/search/tags'>Tags</a></li><!-- + --><li data-name='default'><a href='<%- ctx.formatClientLink('help', 'search') %>'>General</a></li><!-- + --><li data-name='posts'><a href='<%- ctx.formatClientLink('help', 'search', 'posts') %>'>Posts</a></li><!-- + --><li data-name='users'><a href='<%- ctx.formatClientLink('help', 'search', 'users') %>'>Users</a></li><!-- + --><li data-name='tags'><a href='<%- ctx.formatClientLink('help', 'search', 'tags') %>'>Tags</a></li><!-- --></ul><!-- --></nav> diff --git a/client/html/help_search_general.tpl b/client/html/help_search_general.tpl index a7b05d83..6a5cfd58 100644 --- a/client/html/help_search_general.tpl +++ b/client/html/help_search_general.tpl @@ -80,6 +80,9 @@ take following form:</p> <code>,desc</code> to control the sort direction, which can be also controlled by negating the whole token.</p> +<p>You can escape special characters such as <code>:</code> and <code>-</code> +by prepending them with a backslash: <code>\\</code>.</p> + <h1>Example</h1> <p>Searching for posts with following query:</p> @@ -89,3 +92,8 @@ by negating the whole token.</p> <p>will show flash files tagged as sea, that were liked by seven people at most, uploaded by user Pirate.</p> +<p>Searching for posts with <code>re:zero</code> will show an error message +about unknown named token.</p> + +<p>Searching for posts with <code>re\:zero</code> will show posts tagged with +<code>re:zero</code>.</p> diff --git a/client/html/help_search_posts.tpl b/client/html/help_search_posts.tpl index 074819f9..a9e1a8e0 100644 --- a/client/html/help_search_posts.tpl +++ b/client/html/help_search_posts.tpl @@ -12,7 +12,7 @@ </tr> <tr> <td><code>tag</code></td> - <td>having given tag</td> + <td>having given tag (accepts wildcards)</td> </tr> <tr> <td><code>score</code></td> @@ -20,7 +20,7 @@ </tr> <tr> <td><code>uploader</code></td> - <td>uploaded by given user</td> + <td>uploaded by given use (accepts wildcards)r</td> </tr> <tr> <td><code>upload</code></td> @@ -32,11 +32,11 @@ </tr> <tr> <td><code>comment</code></td> - <td>commented by given user</td> + <td>commented by given user (accepts wildcards)</td> </tr> <tr> <td><code>fav</code></td> - <td>favorited by given user</td> + <td>favorited by given user (accepts wildcards)</td> </tr> <tr> <td><code>tag-count</code></td> @@ -54,6 +54,10 @@ <td><code>note-count</code></td> <td>having given number of annotations</td> </tr> + <tr> + <td><code>note-text</code></td> + <td>having given note text (accepts wildcards)</td> + </tr> <tr> <td><code>relation-count</code></td> <td>having given number of relations</td> @@ -86,6 +90,14 @@ <td><code>image-area</code></td> <td>having given number of pixels (image width * image height)</td> </tr> + <tr> + <td><code>image-aspect-ratio</code></td> + <td>having given aspect ratio (image width / image height)</td> + </tr> + <tr> + <td><code>image-ar</code></td> + <td>alias of <code>image-aspect-ratio</code></td> + </tr> <tr> <td><code>width</code></td> <td>alias of <code>image-width</code></td> @@ -98,6 +110,14 @@ <td><code>area</code></td> <td>alias of <code>image-area</code></td> </tr> + <tr> + <td><code>aspect-ratio</code></td> + <td>alias of <code>image-aspect-ratio</code></td> + </tr> + <tr> + <td><code>ar</code></td> + <td>alias of <code>image-aspect-ratio</code></td> + </tr> <tr> <td><code>creation-date</code></td> <td>posted at given date</td> diff --git a/client/html/help_search_tags.tpl b/client/html/help_search_tags.tpl index 38697341..b6fbeccd 100644 --- a/client/html/help_search_tags.tpl +++ b/client/html/help_search_tags.tpl @@ -12,7 +12,7 @@ </tr> <tr> <td><code>category</code></td> - <td>having given category</td> + <td>having given category (accepts wildcards)</td> </tr> <tr> <td><code>creation-date</code></td> diff --git a/client/html/home.tpl b/client/html/home.tpl index f1d9b1c1..1e51b12b 100644 --- a/client/html/home.tpl +++ b/client/html/home.tpl @@ -8,7 +8,7 @@ <%= ctx.makeTextInput({name: 'search-text', placeholder: 'enter some tags'}) %> <input type='submit' value='Search'/> <span class=sep>or</span> - <a href='/posts'>browse all posts</a> + <a href='<%- ctx.formatClientLink('posts') %>'>browse all posts</a> </form> <% } %> <div class='post-info-container'></div> diff --git a/client/html/home_footer.tpl b/client/html/home_footer.tpl index 9ab9cd0c..8f9cb10a 100644 --- a/client/html/home_footer.tpl +++ b/client/html/home_footer.tpl @@ -2,6 +2,6 @@ <li><%- ctx.postCount %> posts</li><span class='sep'> </span><li><%= ctx.makeFileSize(ctx.diskUsage) %></li><span class='sep'> </span><li>Build <a class='version' href='https://github.com/rr-/szurubooru/commits/master'><%- ctx.version %></a> from <%= ctx.makeRelativeTime(ctx.buildDate) %></li><span class='sep'> - </span><% if (ctx.canListSnapshots) { %><li><a href='/history'>History</a></li><span class='sep'> + </span><% if (ctx.canListSnapshots) { %><li><a href='<%- ctx.formatClientLink('history') %>'>History</a></li><span class='sep'> </span><% } %> </ul> diff --git a/client/html/index.htm b/client/html/index.htm index f20ad461..5df8e6dd 100644 --- a/client/html/index.htm +++ b/client/html/index.htm @@ -3,7 +3,7 @@ <head> <meta charset='utf-8'/> <meta name='viewport' content='width=device-width, initial-scale=1, maximum-scale=1'> - <title><!-- configured in the config file --> + Loading... diff --git a/client/html/login.tpl b/client/html/login.tpl index cc2a6805..186a5489 100644 --- a/client/html/login.tpl +++ b/client/html/login.tpl @@ -30,9 +30,7 @@
- <% if (ctx.canSendMails) { %> - Forgot the password? - <% } %> + '>Forgot the password?
diff --git a/client/html/manual_pager_nav.tpl b/client/html/manual_pager_nav.tpl index d47df141..35908503 100644 --- a/client/html/manual_pager_nav.tpl +++ b/client/html/manual_pager_nav.tpl @@ -1,17 +1,17 @@