diff --git a/.gitignore b/.gitignore
index 378345b..e2548a3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,2 @@
-config.ini
+config.yaml
*/*_modules/
diff --git a/INSTALL.md b/INSTALL.md
index b96680f..04b5cc9 100644
--- a/INSTALL.md
+++ b/INSTALL.md
@@ -75,12 +75,12 @@ user@host:szuru/server$ source python_modules/bin/activate # enters the sandbox
1. Configure things:
```console
- user@host:szuru$ cp config.ini.dist config.ini
- user@host:szuru$ vim config.ini
+ user@host:szuru$ cp config.yaml.dist config.yaml
+ user@host:szuru$ vim config.yaml
```
- Pay extra attention to the `[database]` section, `[smtp]` section, API URL
- and base URL in `[basic]`.
+ Pay extra attention to API URL, base URL, the `database` section and the
+ `smtp` section.
2. Compile the frontend:
@@ -132,7 +132,7 @@ Below are described the methods to integrate the API into a web server:
`uwsgi`, but they'll need to write wrapper scripts themselves.
Note that the API URL in the virtual host configuration needs to be the same as
-the one in the `config.ini`, so that client knows how to access the backend!
+the one in the `config.yaml`, so that client knows how to access the backend!
#### Example
@@ -157,12 +157,11 @@ server {
}
```
-**`config.ini`**:
+**`config.yaml`**:
-```ini
-[basic]
-api_url = http://big.dude/api/
-base_url = http://big.dude/
+```yaml
+api_url: 'http://big.dude/api/'
+base_url: 'http://big.dude/'
```
Then the backend is started with `./server/host-waitress` from within
diff --git a/client/build.js b/client/build.js
index eb32e9b..70e63e5 100644
--- a/client/build.js
+++ b/client/build.js
@@ -5,40 +5,47 @@ 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 getVersion() {
return execSync('git describe --always --dirty --long --tags').toString();
}
function getConfig() {
- const ini = require('ini');
+ const yaml = require('js-yaml');
const merge = require('merge');
const camelcaseKeys = require('camelcase-keys');
- function parseIniFile(path) {
- let result = ini.parse(fs.readFileSync(path, 'utf-8')
- .replace(/#.+$/gm, '')
- .replace(/\s+$/gm, ''));
- Object.keys(result).map((key, _) => {
- result[key] = camelcaseKeys(result[key]);
- });
- return result;
+ function parseConfigFile(path) {
+ let result = yaml.load(fs.readFileSync(path, 'utf-8'));
+ return convertKeysToCamelCase(result);
}
- let config = parseIniFile('../config.ini.dist');
+ let config = parseConfigFile('../config.yaml.dist');
try {
- const localConfig = parseIniFile('../config.ini');
+ const localConfig = parseConfigFile('../config.yaml');
config = merge.recursive(config, localConfig);
} catch (e) {
console.warn('Local config does not exist, ignoring');
}
- delete config.basic.secret;
+ delete config.secret;
delete config.smtp;
delete config.database;
- config.service.userRanks = config.service.userRanks.split(/,\s*/);
- config.service.tagCategories = config.service.tagCategories.split(/,\s*/);
config.meta = {
version: getVersion(),
buildDate: new Date().toUTCString(),
@@ -63,7 +70,7 @@ function bundleHtml(config) {
.replace(/(<\/head>)/, templatesHtml + '$1')
.replace(
/(
)(.*)(<\/title>)/,
- util.format('$1%s$3', config.basic.name));
+ util.format('$1%s$3', config.name));
fs.writeFileSync(
'./public/index.htm',
diff --git a/client/js/api.js b/client/js/api.js
index a881b56..497525e 100644
--- a/client/js/api.js
+++ b/client/js/api.js
@@ -48,7 +48,7 @@ class Api {
continue;
}
const rankName = config.privileges[privilege];
- const rankIndex = config.service.userRanks.indexOf(rankName);
+ const rankIndex = config.ranks.indexOf(rankName);
if (minViableRank === null || rankIndex < minViableRank) {
minViableRank = rankIndex;
}
@@ -57,7 +57,7 @@ class Api {
console.error('Bad privilege name: ' + lookup);
}
let myRank = this.user !== null ?
- config.service.userRanks.indexOf(this.user.accessRank) :
+ config.ranks.indexOf(this.user.rank) :
0;
return myRank >= minViableRank;
}
@@ -91,7 +91,7 @@ class Api {
}
getFullUrl(url) {
- return (config.basic.apiUrl + '/' + url).replace(/([^:])\/+/g, '$1/');
+ return (config.apiUrl + '/' + url).replace(/([^:])\/+/g, '$1/');
}
}
diff --git a/client/js/views/help_view.js b/client/js/views/help_view.js
index 4f68a7d..eaf6c14 100644
--- a/client/js/views/help_view.js
+++ b/client/js/views/help_view.js
@@ -25,7 +25,7 @@ class HelpView extends BaseView {
}
const content = this.sectionTemplates[section]({
- 'name': config.basic.name,
+ 'name': config.name,
});
this.showView(this.template({'content': content}));
diff --git a/client/js/views/home_view.js b/client/js/views/home_view.js
index 1f1f0f4..9a8603d 100644
--- a/client/js/views/home_view.js
+++ b/client/js/views/home_view.js
@@ -11,7 +11,7 @@ class HomeView extends BaseView {
render(section) {
this.showView(this.template({
- name: config.basic.name,
+ name: config.name,
version: config.meta.version,
buildDate: config.meta.buildDate,
}));
diff --git a/client/js/views/login_view.js b/client/js/views/login_view.js
index 2f24053..3708e65 100644
--- a/client/js/views/login_view.js
+++ b/client/js/views/login_view.js
@@ -17,8 +17,8 @@ class LoginView extends BaseView {
const userNameField = document.getElementById('user-name');
const passwordField = document.getElementById('user-password');
const rememberUserField = document.getElementById('remember-user');
- userNameField.setAttribute('pattern', config.service.userNameRegex);
- passwordField.setAttribute('pattern', config.service.passwordRegex);
+ userNameField.setAttribute('pattern', config.userNameRegex);
+ passwordField.setAttribute('pattern', config.passwordRegex);
form.addEventListener('submit', e => {
e.preventDefault();
diff --git a/client/js/views/registration_view.js b/client/js/views/registration_view.js
index 7e1ee85..1a1583e 100644
--- a/client/js/views/registration_view.js
+++ b/client/js/views/registration_view.js
@@ -17,8 +17,8 @@ class RegistrationView extends BaseView {
const userNameField = document.getElementById('user-name');
const passwordField = document.getElementById('user-password');
const emailField = document.getElementById('user-email');
- userNameField.setAttribute('pattern', config.service.userNameRegex);
- passwordField.setAttribute('pattern', config.service.passwordRegex);
+ userNameField.setAttribute('pattern', config.userNameRegex);
+ passwordField.setAttribute('pattern', config.passwordRegex);
form.addEventListener('submit', e => {
e.preventDefault();
diff --git a/client/package.json b/client/package.json
index da59222..8f8fca2 100644
--- a/client/package.json
+++ b/client/package.json
@@ -7,13 +7,13 @@
},
"dependencies": {
"browserify": "^13.0.0",
- "camelcase-keys": "^2.1.0",
+ "camelcase": "^2.1.1",
"csso": "^1.8.0",
"glob": "^7.0.3",
"handlebars": "^4.0.5",
"html-minifier": "^1.3.1",
- "ini": "^1.3.4",
"js-cookie": "^2.1.0",
+ "js-yaml": "^3.5.5",
"merge": "^1.2.0",
"page": "^1.7.1",
"superagent": "^1.8.3",
diff --git a/config.ini.dist b/config.ini.dist
deleted file mode 100644
index ba3d4d4..0000000
--- a/config.ini.dist
+++ /dev/null
@@ -1,90 +0,0 @@
-# rather than editing this file, it is strongly suggested to create config.ini
-# and override only what you need.
-
-[basic]
-name = szurubooru
-debug = 0
-secret = change
-api_url = "http://api.example.com/" # where frontend connects to
-base_url = "http://example.com/" # used in absolute links (e.g. password reminder)
-
-[database]
-schema = postgres
-host = localhost
-port = 5432
-user = szuru
-pass = dog
-name = szuru
-
-[smtp]
-# used to send password reminders
-host = localhost
-port = 25
-user = bot
-pass = groovy123
-
-[service]
-user_ranks = anonymous, regular_user, power_user, mod, admin, nobody
-default_user_rank = regular_user
-users_per_page = 20
-posts_per_page = 40
-max_comment_length = 5000
-tag_categories = meta, artist, character, copyright, other unique
-
-# don't change these regexes, unless you want to annoy people. but if you do
-# customize them, make sure to update the instructions in the registration form
-# template as well.
-password_regex = "^.{5,}$"
-user_name_regex = "^[a-zA-Z0-9_-]{1,32}$"
-
-[privileges]
-users:create = anonymous
-users:list = regular_user
-users:view = regular_user
-users:edit:any:name = mod
-users:edit:any:pass = mod
-users:edit:any:email = mod
-users:edit:any:avatar = mod
-# note: promoting people to higher rank than one's own is always impossible
-users:edit:any:rank = mod
-users:edit:self:name = regular_user
-users:edit:self:pass = regular_user
-users:edit:self:email = regular_user
-users:edit:self:avatar = regular_user
-users:edit:self:rank = mod
-users:delete:any = admin
-users:delete:self = regular_user
-
-posts:create:anonymous = regular_user
-posts:create:identified = regular_user
-posts:list = anonymous
-posts:view = anonymous
-posts:edit:content = power_user
-posts:edit:flags = regular_user
-posts:edit:notes = regular_user
-posts:edit:relations = regular_user
-posts:edit:safety = power_user
-posts:edit:source = regular_user
-posts:edit:tags = regular_user
-posts:edit:thumbnail = power_user
-posts:feature = mod
-posts:delete = mod
-
-tags:create = regular_user
-tags:edit:name = power_user
-tags:edit:category = power_user
-tags:edit:implications = power_user
-tags:edit:suggestions = power_user
-tags:list = regular_user
-tags:masstag = power_user
-tags:merge = mod
-tags:delete = mod
-
-comments:create = regular_user
-comments:delete:any = mod
-comments:delete:own = regular_user
-comments:edit:any = mod
-comments:edit:own = regular_user
-comments:list = regular_user
-
-history:view = power_user
diff --git a/config.yaml.dist b/config.yaml.dist
new file mode 100644
index 0000000..8e844b9
--- /dev/null
+++ b/config.yaml.dist
@@ -0,0 +1,102 @@
+# rather than editing this file, it is strongly suggested to create config.ini
+# and override only what you need.
+
+name: szurubooru
+debug: 0
+secret: change
+api_url: # where frontend connects to, example: http://api.example.com/
+base_url: # used to form absolute links, example: http://example.com/
+
+database:
+ schema: postgres
+ host: # example: localhost
+ port: # example: 5432
+ user: # example: szuru
+ pass: # example: dog
+ name: # example: szuru
+
+# used to send password reminders
+smtp:
+ host: # example: localhost
+ port: # example: 25
+ user: # example: bot
+ pass: # example: groovy123
+
+limits:
+ users_per_page: 20
+ posts_per_page: 40
+ max_comment_length: 5000
+
+tag_categories:
+ - meta
+ - artist
+ - character
+ - copyright
+ - other unique
+
+# changing ranks after deployment may require manual tweaks to the database.
+ranks:
+ - anonymous
+ - regular_user
+ - power_user
+ - mod
+ - admin
+ - nobody
+default_rank: regular_user
+
+# don't change these, unless you want to annoy people. if you do customize
+# them though, make sure to update the instructions in the registration form
+# template as well.
+password_regex: '^.{5,}$'
+user_name_regex: '^[a-zA-Z0-9_-]{1,32}$'
+
+privileges:
+ 'users:create': anonymous
+ 'users:list': regular_user
+ 'users:view': regular_user
+ 'users:edit:any:name': mod
+ 'users:edit:any:pass': mod
+ 'users:edit:any:email': mod
+ 'users:edit:any:avatar': mod
+ 'users:edit:any:rank': mod
+ 'users:edit:self:name': regular_user
+ 'users:edit:self:pass': regular_user
+ 'users:edit:self:email': regular_user
+ 'users:edit:self:avatar': regular_user
+ 'users:edit:self:rank': mod # one can't promote themselves or anyone to upper rank than their own.
+ 'users:delete:any': admin
+ 'users:delete:self': regular_user
+
+ 'posts:create:anonymous': regular_user
+ 'posts:create:identified': regular_user
+ 'posts:list': anonymous
+ 'posts:view': anonymous
+ 'posts:edit:content': power_user
+ 'posts:edit:flags': regular_user
+ 'posts:edit:notes': regular_user
+ 'posts:edit:relations': regular_user
+ 'posts:edit:safety': power_user
+ 'posts:edit:source': regular_user
+ 'posts:edit:tags': regular_user
+ 'posts:edit:thumbnail': power_user
+ 'posts:feature': mod
+ 'posts:delete': mod
+
+ 'tags:create': regular_user
+ 'tags:edit:name': power_user
+ 'tags:edit:category': power_user
+ 'tags:edit:implications': power_user
+ 'tags:edit:suggestions': power_user
+ 'tags:list': regular_user
+ 'tags:masstag': power_user
+ 'tags:merge': mod
+ 'tags:delete': mod
+
+ 'comments:create': regular_user
+ 'comments:delete:any': mod
+ 'comments:delete:own': regular_user
+ 'comments:edit:any': mod
+ 'comments:edit:own': regular_user
+ 'comments:list': regular_user
+
+ 'history:view': power_user
diff --git a/server/requirements.txt b/server/requirements.txt
index 614cf9c..5c0d15a 100644
--- a/server/requirements.txt
+++ b/server/requirements.txt
@@ -1,5 +1,5 @@
alembic>=0.8.5
-configobj>=5.0.6
+pyyaml>=3.11
falcon>=0.3.0
psycopg2>=2.6.1
SQLAlchemy>=1.0.12
diff --git a/server/szurubooru/api/password_reset_api.py b/server/szurubooru/api/password_reset_api.py
index 45ef042..47c1f12 100644
--- a/server/szurubooru/api/password_reset_api.py
+++ b/server/szurubooru/api/password_reset_api.py
@@ -19,12 +19,12 @@ class PasswordResetApi(BaseApi):
'User %r hasn\'t supplied email. Cannot reset password.' % user_name)
token = auth.generate_authentication_token(user)
url = '%s/password-reset/%s:%s' % (
- config.config['basic']['base_url'].rstrip('/'), user.name, token)
+ config.config['base_url'].rstrip('/'), user.name, token)
mailer.send_mail(
- 'noreply@%s' % config.config['basic']['name'],
+ 'noreply@%s' % config.config['name'],
user.email,
- MAIL_SUBJECT.format(name=config.config['basic']['name']),
- MAIL_BODY.format(name=config.config['basic']['name'], url=url))
+ MAIL_SUBJECT.format(name=config.config['name']),
+ MAIL_BODY.format(name=config.config['name'], url=url))
return {}
def post(self, context, user_name):
diff --git a/server/szurubooru/config.py b/server/szurubooru/config.py
index ccb8b23..38637c3 100644
--- a/server/szurubooru/config.py
+++ b/server/szurubooru/config.py
@@ -1,13 +1,26 @@
import os
-import configobj
+import yaml
from szurubooru import errors
+def merge(left, right):
+ for key in right:
+ if key in left:
+ if isinstance(left[key], dict) and isinstance(right[key], dict):
+ merge(left[key], right[key])
+ elif left[key] != right[key]:
+ left[key] = right[key]
+ else:
+ left[key] = right[key]
+ return left
+
class Config(object):
- ''' INI config parser and container. '''
+ ''' Config parser and container. '''
def __init__(self):
- self.config = configobj.ConfigObj('../config.ini.dist')
- if os.path.exists('../config.ini'):
- self.config.merge(configobj.ConfigObj('../config.ini'))
+ with open('../config.yaml.dist') as handle:
+ self.config = yaml.load(handle.read())
+ if os.path.exists('../config.yaml'):
+ with open('../config.yaml') as handle:
+ self.config = merge(self.config, yaml.load(handle.read()))
self._validate()
def __getitem__(self, key):
@@ -15,22 +28,25 @@ class Config(object):
def _validate(self):
'''
- Check whether config.ini doesn't contain errors that might prove
+ Check whether config doesn't contain errors that might prove
lethal at runtime.
'''
- all_ranks = self['service']['user_ranks']
+ all_ranks = self['ranks']
for privilege, rank in self['privileges'].items():
if rank not in all_ranks:
raise errors.ConfigError(
- 'Rank %r for privilege %r is missing from user_ranks' % (
- rank, privilege))
+ 'Rank %r for privilege %r is missing' % (rank, privilege))
for rank in ['anonymous', 'admin', 'nobody']:
if rank not in all_ranks:
- raise errors.ConfigError(
- 'Fixed rank %r is missing from user_ranks' % rank)
- if self['service']['default_user_rank'] not in all_ranks:
+ raise errors.ConfigError('Protected rank %r is missing' % rank)
+ if self['default_rank'] not in all_ranks:
raise errors.ConfigError(
- 'Default rank %r is missing from user_ranks' % (
- self['service']['default_user_rank']))
+ 'Default rank %r is not on the list of known ranks' % (
+ self['default_rank']))
+
+ for key in ['schema', 'host', 'port', 'user', 'pass', 'name']:
+ if not self['database'][key]:
+ raise errors.ConfigError(
+ 'Database is not configured: %r is missing' % key)
config = Config() # pylint: disable=invalid-name
diff --git a/server/szurubooru/tests/api/test_password_reset_api.py b/server/szurubooru/tests/api/test_password_reset_api.py
index 1e7a43d..60f766d 100644
--- a/server/szurubooru/tests/api/test_password_reset_api.py
+++ b/server/szurubooru/tests/api/test_password_reset_api.py
@@ -9,11 +9,9 @@ class TestPasswordReset(DatabaseTestCase):
def setUp(self):
super().setUp()
config_mock = {
- 'basic': {
- 'secret': 'x',
- 'base_url': 'http://example.com/',
- 'name': 'Test instance',
- },
+ 'secret': 'x',
+ 'base_url': 'http://example.com/',
+ 'name': 'Test instance',
}
self.old_config = config.config
config.config = config_mock
diff --git a/server/szurubooru/tests/api/test_user_api.py b/server/szurubooru/tests/api/test_user_api.py
index b2b4b8c..9ea962a 100644
--- a/server/szurubooru/tests/api/test_user_api.py
+++ b/server/szurubooru/tests/api/test_user_api.py
@@ -13,9 +13,7 @@ class TestRetrievingUsers(DatabaseTestCase):
'users:view': 'regular_user',
'users:create': 'regular_user',
},
- 'service': {
- 'user_ranks': ['anonymous', 'regular_user', 'mod', 'admin'],
- },
+ 'ranks': ['anonymous', 'regular_user', 'mod', 'admin'],
}
self.old_config = config.config
config.config = config_mock
@@ -74,15 +72,11 @@ class TestCreatingUser(DatabaseTestCase):
def setUp(self):
super().setUp()
config_mock = {
- 'basic': {
- 'secret': '',
- },
- 'service': {
- 'user_name_regex': '.{3,}',
- 'password_regex': '.{3,}',
- 'user_ranks': ['anonymous', 'regular_user', 'mod', 'admin'],
- 'default_user_rank': 'regular_user',
- },
+ 'secret': '',
+ 'user_name_regex': '.{3,}',
+ 'password_regex': '.{3,}',
+ 'default_rank': 'regular_user',
+ 'ranks': ['anonymous', 'regular_user', 'mod', 'admin'],
'privileges': {
'users:create': 'anonymous',
},
@@ -146,14 +140,10 @@ class TestUpdatingUser(DatabaseTestCase):
def setUp(self):
super().setUp()
config_mock = {
- 'basic': {
- 'secret': '',
- },
- 'service': {
- 'user_name_regex': '.{3,}',
- 'password_regex': '.{3,}',
- 'user_ranks': ['anonymous', 'regular_user', 'mod', 'admin'],
- },
+ 'secret': '',
+ 'user_name_regex': '.{3,}',
+ 'password_regex': '.{3,}',
+ 'ranks': ['anonymous', 'regular_user', 'mod', 'admin'],
'privileges': {
'users:edit:self:name': 'regular_user',
'users:edit:self:pass': 'regular_user',
diff --git a/server/szurubooru/util/auth.py b/server/szurubooru/util/auth.py
index 2f65b39..bb7223f 100644
--- a/server/szurubooru/util/auth.py
+++ b/server/szurubooru/util/auth.py
@@ -6,7 +6,7 @@ from szurubooru import errors
def get_password_hash(salt, password):
''' Retrieve new-style password hash. '''
digest = hashlib.sha256()
- digest.update(config.config['basic']['secret'].encode('utf8'))
+ digest.update(config.config['secret'].encode('utf8'))
digest.update(salt.encode('utf8'))
digest.update(password.encode('utf8'))
return digest.hexdigest()
@@ -42,7 +42,7 @@ def verify_privilege(user, privilege_name):
'''
Throw an AuthError if the given user doesn't have given privilege.
'''
- all_ranks = config.config['service']['user_ranks']
+ all_ranks = config.config['ranks']
assert privilege_name in config.config['privileges']
assert user.rank in all_ranks
@@ -54,6 +54,6 @@ def verify_privilege(user, privilege_name):
def generate_authentication_token(user):
''' Generate nonguessable challenge (e.g. links in password reminder). '''
digest = hashlib.md5()
- digest.update(config.config['basic']['secret'].encode('utf8'))
+ digest.update(config.config['secret'].encode('utf8'))
digest.update(user.password_salt.encode('utf8'))
return digest.hexdigest()
diff --git a/server/szurubooru/util/users.py b/server/szurubooru/util/users.py
index 4e68ec6..f1fb654 100644
--- a/server/szurubooru/util/users.py
+++ b/server/szurubooru/util/users.py
@@ -10,7 +10,7 @@ def create_user(name, password, email):
update_name(user, name)
update_password(user, password)
update_email(user, email)
- user.rank = config.config['service']['default_user_rank']
+ user.rank = config.config['default_rank']
user.creation_time = datetime.now()
user.avatar_style = db.User.AVATAR_GRAVATAR
return user
@@ -18,7 +18,7 @@ def create_user(name, password, email):
def update_name(user, name):
''' Validate and update user's name. '''
name = name.strip()
- name_regex = config.config['service']['user_name_regex']
+ name_regex = config.config['user_name_regex']
if not re.match(name_regex, name):
raise errors.ValidationError(
'Name must satisfy regex %r.' % name_regex)
@@ -26,7 +26,7 @@ def update_name(user, name):
def update_password(user, password):
''' Validate and update user's password. '''
- password_regex = config.config['service']['password_regex']
+ password_regex = config.config['password_regex']
if not re.match(password_regex, password):
raise errors.ValidationError(
'Password must satisfy regex %r.' % password_regex)
@@ -43,10 +43,10 @@ def update_email(user, email):
def update_rank(user, rank, authenticated_user):
rank = rank.strip()
- available_ranks = config.config['service']['user_ranks']
+ available_ranks = config.config['ranks']
if not rank in available_ranks:
raise errors.ValidationError(
- 'Bad rank. Valid ranks: %r' % available_ranks)
+ 'Bad rank %r. Valid ranks: %r' % (rank, available_ranks))
if available_ranks.index(authenticated_user.rank) \
< available_ranks.index(rank):
raise errors.AuthError('Trying to set higher rank than your own')