mirror of
https://github.com/arcan1s/ffxivbis.git
synced 2025-04-25 01:37:17 +00:00
auth support
This commit is contained in:
parent
39be8cebf1
commit
7de6a5fcc9
36
README.md
36
README.md
@ -71,6 +71,34 @@ Service which allows to manage savage loot distribution easy.
|
|||||||
|
|
||||||
* `is_tome`: is item tome gear or not, bool, required.
|
* `is_tome`: is item tome gear or not, bool, required.
|
||||||
* `piece`: item name, string, required.
|
* `piece`: item name, string, required.
|
||||||
|
|
||||||
|
|
||||||
|
### Users API
|
||||||
|
|
||||||
|
* `DELETE /api/v1/login/{username}`
|
||||||
|
|
||||||
|
Delete user with specified username. Parameters:
|
||||||
|
|
||||||
|
* `username`: username to remove, required.
|
||||||
|
|
||||||
|
* `POST /api/v1/login`
|
||||||
|
|
||||||
|
Login with credentials. Parameters:
|
||||||
|
|
||||||
|
* `username`: username to login, string, required.
|
||||||
|
* `password`: password to login, string, required.
|
||||||
|
|
||||||
|
* `PUT /api/v1/login`
|
||||||
|
|
||||||
|
Create new user. Parameters:
|
||||||
|
|
||||||
|
* `username`: username to login, string, required.
|
||||||
|
* `password`: password to login, string,
|
||||||
|
* `permission`: user permission, one of `get`, `post`, optional, default `get`.
|
||||||
|
|
||||||
|
* `POST /api/v1/logout`
|
||||||
|
|
||||||
|
Logout.
|
||||||
|
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
@ -93,6 +121,14 @@ Service which allows to manage savage loot distribution easy.
|
|||||||
* `xivapi_key`: xivapi developer key, string, optional.
|
* `xivapi_key`: xivapi developer key, string, optional.
|
||||||
* `xivapi_url`: xivapi base url, string, required.
|
* `xivapi_url`: xivapi base url, string, required.
|
||||||
|
|
||||||
|
* `auth` section
|
||||||
|
|
||||||
|
Authentication settings.
|
||||||
|
|
||||||
|
* `enabled`: whether authentication enabled or not, boolean, required.
|
||||||
|
* `root_username`: username of administrator, string, required.
|
||||||
|
* `root_password`: md5 hashed password of administrator, string, required.
|
||||||
|
|
||||||
* `sqlite` section
|
* `sqlite` section
|
||||||
|
|
||||||
Database settings for `sqlite` provider.
|
Database settings for `sqlite` provider.
|
||||||
|
17
migrations/20190910_01_tgBmx-users-table.py
Normal file
17
migrations/20190910_01_tgBmx-users-table.py
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
'''
|
||||||
|
users table
|
||||||
|
'''
|
||||||
|
|
||||||
|
from yoyo import step
|
||||||
|
|
||||||
|
__depends__ = {}
|
||||||
|
|
||||||
|
steps = [
|
||||||
|
step('''create table users (
|
||||||
|
user_id integer primary key,
|
||||||
|
username text not null,
|
||||||
|
password text not null,
|
||||||
|
permission text not null
|
||||||
|
)'''),
|
||||||
|
step('''create unique index users_username_idx on users(username)''')
|
||||||
|
]
|
@ -7,3 +7,4 @@ priority = is_required loot_count_bis loot_count_total loot_count loot_priority
|
|||||||
[web]
|
[web]
|
||||||
host = 0.0.0.0
|
host = 0.0.0.0
|
||||||
port = 8000
|
port = 8000
|
||||||
|
templates = templates
|
||||||
|
4
package/ini/ffxivbis.ini.d/auth.ini
Normal file
4
package/ini/ffxivbis.ini.d/auth.ini
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
[auth]
|
||||||
|
enabled = yes
|
||||||
|
root_username = admin
|
||||||
|
root_password = $1$R3j4sym6$HtvrKOJ66f7w3.9Zc3U6h1
|
4
setup.py
4
setup.py
@ -25,9 +25,11 @@ setup(
|
|||||||
packages=find_packages(exclude=['contrib', 'docs', 'tests']),
|
packages=find_packages(exclude=['contrib', 'docs', 'tests']),
|
||||||
|
|
||||||
install_requires=[
|
install_requires=[
|
||||||
'aiohttp_jinja2',
|
|
||||||
'aiohttp',
|
'aiohttp',
|
||||||
|
'aiohttp_jinja2',
|
||||||
|
'aiohttp_security',
|
||||||
'Jinja2',
|
'Jinja2',
|
||||||
|
'passlib',
|
||||||
'requests',
|
'requests',
|
||||||
'yoyo_migrations'
|
'yoyo_migrations'
|
||||||
],
|
],
|
||||||
|
51
src/service/api/auth.py
Normal file
51
src/service/api/auth.py
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2019 Evgeniy Alekseev.
|
||||||
|
#
|
||||||
|
# This file is part of ffxivbis
|
||||||
|
# (see https://github.com/arcan1s/ffxivbis).
|
||||||
|
#
|
||||||
|
# License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
|
||||||
|
#
|
||||||
|
from aiohttp.web import middleware, Request, Response
|
||||||
|
from aiohttp_security import AbstractAuthorizationPolicy, check_permission
|
||||||
|
from typing import Callable, Optional
|
||||||
|
|
||||||
|
from service.core.database import Database
|
||||||
|
|
||||||
|
|
||||||
|
class AuthorizationPolicy(AbstractAuthorizationPolicy):
|
||||||
|
|
||||||
|
def __init__(self, database: Database) -> None:
|
||||||
|
self.database = database
|
||||||
|
|
||||||
|
async def authorized_userid(self, identity: str) -> Optional[str]:
|
||||||
|
user = self.database.get_user(identity)
|
||||||
|
return identity if user is not None else None
|
||||||
|
|
||||||
|
async def permits(self, identity: str, permission: str, context: str = None) -> bool:
|
||||||
|
user = self.database.get_user(identity)
|
||||||
|
if user is None:
|
||||||
|
return False
|
||||||
|
if user.username != identity:
|
||||||
|
return False
|
||||||
|
if user.permission == 'admin':
|
||||||
|
return True
|
||||||
|
return permission == 'get' or user.permission == permission
|
||||||
|
|
||||||
|
|
||||||
|
def authorize_factory() -> Callable:
|
||||||
|
allowed_paths = {'/', '/favicon.ico', '/api/v1/login', '/api/v1/logout'}
|
||||||
|
|
||||||
|
@middleware
|
||||||
|
async def authorize(request: Request, handler: Callable) -> Response:
|
||||||
|
if request.path.startswith('/admin'):
|
||||||
|
permission = 'admin'
|
||||||
|
else:
|
||||||
|
permission = 'get' if request.method in ('GET', 'HEAD') else 'post'
|
||||||
|
if request.path not in allowed_paths:
|
||||||
|
await check_permission(request, permission)
|
||||||
|
|
||||||
|
return await handler(request)
|
||||||
|
|
||||||
|
return authorize
|
||||||
|
|
@ -8,19 +8,27 @@
|
|||||||
#
|
#
|
||||||
from aiohttp.web import Application
|
from aiohttp.web import Application
|
||||||
|
|
||||||
from service.api.views.api.bis import BiSView
|
from .views.api.bis import BiSView
|
||||||
from service.api.views.api.loot import LootView
|
from .views.api.login import LoginView
|
||||||
from service.api.views.api.player import PlayerView
|
from .views.api.logout import LogoutView
|
||||||
from service.api.views.html.bis import BiSHtmlView
|
from .views.api.loot import LootView
|
||||||
from service.api.views.html.index import IndexHtmlView
|
from .views.api.player import PlayerView
|
||||||
from service.api.views.html.loot import LootHtmlView
|
from .views.html.bis import BiSHtmlView
|
||||||
from service.api.views.html.loot_suggest import LootSuggestHtmlView
|
from .views.html.index import IndexHtmlView
|
||||||
from service.api.views.html.player import PlayerHtmlView
|
from .views.html.loot import LootHtmlView
|
||||||
from service.api.views.html.static import StaticHtmlView
|
from .views.html.loot_suggest import LootSuggestHtmlView
|
||||||
|
from .views.html.player import PlayerHtmlView
|
||||||
|
from .views.html.static import StaticHtmlView
|
||||||
|
from .views.html.users import UsersHtmlView
|
||||||
|
|
||||||
|
|
||||||
def setup_routes(app: Application) -> None:
|
def setup_routes(app: Application) -> None:
|
||||||
# api routes
|
# api routes
|
||||||
|
app.router.add_delete('/api/v1/login/{username}', LoginView)
|
||||||
|
app.router.add_post('/api/v1/login', LoginView)
|
||||||
|
app.router.add_post('/api/v1/logout', LogoutView)
|
||||||
|
app.router.add_put('/api/v1/login', LoginView)
|
||||||
|
|
||||||
app.router.add_get('/api/v1/party', PlayerView)
|
app.router.add_get('/api/v1/party', PlayerView)
|
||||||
app.router.add_post('/api/v1/party', PlayerView)
|
app.router.add_post('/api/v1/party', PlayerView)
|
||||||
|
|
||||||
@ -47,3 +55,8 @@ def setup_routes(app: Application) -> None:
|
|||||||
|
|
||||||
app.router.add_get('/suggest', LootSuggestHtmlView)
|
app.router.add_get('/suggest', LootSuggestHtmlView)
|
||||||
app.router.add_post('/suggest', LootSuggestHtmlView)
|
app.router.add_post('/suggest', LootSuggestHtmlView)
|
||||||
|
|
||||||
|
app.router.add_get('/admin/users', UsersHtmlView)
|
||||||
|
app.router.add_post('/admin/users', UsersHtmlView)
|
||||||
|
|
||||||
|
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
#
|
#
|
||||||
import json
|
import json
|
||||||
|
|
||||||
from aiohttp.web import Response
|
from aiohttp.web import HTTPException, Response
|
||||||
from typing import Any, Mapping, List
|
from typing import Any, Mapping, List
|
||||||
|
|
||||||
from .json import HttpEncoder
|
from .json import HttpEncoder
|
||||||
@ -23,6 +23,8 @@ def make_json(response: Any, args: Mapping[str, Any], code: int = 200) -> str:
|
|||||||
|
|
||||||
|
|
||||||
def wrap_exception(exception: Exception, args: Mapping[str, Any], code: int = 500) -> Response:
|
def wrap_exception(exception: Exception, args: Mapping[str, Any], code: int = 500) -> Response:
|
||||||
|
if isinstance(exception, HTTPException):
|
||||||
|
raise exception # reraise return
|
||||||
return wrap_json({'message': repr(exception)}, args, code)
|
return wrap_json({'message': repr(exception)}, args, code)
|
||||||
|
|
||||||
|
|
||||||
|
62
src/service/api/views/api/login.py
Normal file
62
src/service/api/views/api/login.py
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2019 Evgeniy Alekseev.
|
||||||
|
#
|
||||||
|
# This file is part of ffxivbis
|
||||||
|
# (see https://github.com/arcan1s/ffxivbis).
|
||||||
|
#
|
||||||
|
# License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
|
||||||
|
#
|
||||||
|
from aiohttp.web import Response
|
||||||
|
|
||||||
|
from service.api.utils import wrap_exception, wrap_invalid_param, wrap_json
|
||||||
|
from service.api.views.common.login_base import LoginBaseView
|
||||||
|
|
||||||
|
|
||||||
|
class LoginView(LoginBaseView):
|
||||||
|
|
||||||
|
async def delete(self) -> Response:
|
||||||
|
username = self.request.match_info['username']
|
||||||
|
|
||||||
|
try:
|
||||||
|
await self.remove_user(username)
|
||||||
|
except Exception as e:
|
||||||
|
self.request.app.logger.exception('cannot remove user')
|
||||||
|
return wrap_exception(e, {'username': username})
|
||||||
|
|
||||||
|
return wrap_json({}, {'username': username})
|
||||||
|
|
||||||
|
async def post(self) -> Response:
|
||||||
|
try:
|
||||||
|
data = await self.request.json()
|
||||||
|
except Exception:
|
||||||
|
data = dict(await self.request.post())
|
||||||
|
|
||||||
|
required = ['username', 'password']
|
||||||
|
if any(param not in data for param in required):
|
||||||
|
return wrap_invalid_param(required, data)
|
||||||
|
|
||||||
|
try:
|
||||||
|
await self.login(data['username'], data['password'])
|
||||||
|
except Exception as e:
|
||||||
|
self.request.app.logger.exception('cannot login user')
|
||||||
|
return wrap_exception(e, data)
|
||||||
|
|
||||||
|
return wrap_json({}, data)
|
||||||
|
|
||||||
|
async def put(self) -> Response:
|
||||||
|
try:
|
||||||
|
data = await self.request.json()
|
||||||
|
except Exception:
|
||||||
|
data = dict(await self.request.post())
|
||||||
|
|
||||||
|
required = ['username', 'password']
|
||||||
|
if any(param not in data for param in required):
|
||||||
|
return wrap_invalid_param(required, data)
|
||||||
|
|
||||||
|
try:
|
||||||
|
await self.create_user(data['username'], data['password'], data.get('permission', 'get'))
|
||||||
|
except Exception as e:
|
||||||
|
self.request.app.logger.exception('cannot login user')
|
||||||
|
return wrap_exception(e, data)
|
||||||
|
|
||||||
|
return wrap_json({}, data)
|
21
src/service/api/views/api/logout.py
Normal file
21
src/service/api/views/api/logout.py
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
# This file is part of ffxivbis
|
||||||
|
# (see https://github.com/arcan1s/ffxivbis).
|
||||||
|
#
|
||||||
|
# License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
|
||||||
|
#
|
||||||
|
from aiohttp.web import Response
|
||||||
|
|
||||||
|
from service.api.utils import wrap_exception, wrap_json
|
||||||
|
from service.api.views.common.login_base import LoginBaseView
|
||||||
|
|
||||||
|
|
||||||
|
class LogoutView(LoginBaseView):
|
||||||
|
|
||||||
|
async def post(self) -> Response:
|
||||||
|
try:
|
||||||
|
await self.logout()
|
||||||
|
except Exception as e:
|
||||||
|
self.request.app.logger.exception('cannot logout user')
|
||||||
|
return wrap_exception(e, {})
|
||||||
|
|
||||||
|
return wrap_json({}, {})
|
43
src/service/api/views/common/login_base.py
Normal file
43
src/service/api/views/common/login_base.py
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2019 Evgeniy Alekseev.
|
||||||
|
#
|
||||||
|
# This file is part of ffxivbis
|
||||||
|
# (see https://github.com/arcan1s/ffxivbis).
|
||||||
|
#
|
||||||
|
# License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
|
||||||
|
#
|
||||||
|
from aiohttp.web import HTTPFound, HTTPUnauthorized, View
|
||||||
|
from aiohttp_security import check_authorized, forget, remember
|
||||||
|
from passlib.hash import md5_crypt
|
||||||
|
|
||||||
|
from service.models.user import User
|
||||||
|
|
||||||
|
|
||||||
|
class LoginBaseView(View):
|
||||||
|
|
||||||
|
async def check_credentials(self, username: str, password: str) -> bool:
|
||||||
|
user = self.request.app['database'].get_user(username)
|
||||||
|
if user is None:
|
||||||
|
return False
|
||||||
|
return md5_crypt.verify(password, user.password)
|
||||||
|
|
||||||
|
async def create_user(self, username: str, password: str, permission: str) -> None:
|
||||||
|
self.request.app['database'].insert_user(User(username, password, permission), False)
|
||||||
|
|
||||||
|
async def login(self, username: str, password: str) -> None:
|
||||||
|
if await self.check_credentials(username, password):
|
||||||
|
response = HTTPFound('/')
|
||||||
|
await remember(self.request, response, username)
|
||||||
|
raise response
|
||||||
|
|
||||||
|
raise HTTPUnauthorized()
|
||||||
|
|
||||||
|
async def logout(self) -> None:
|
||||||
|
await check_authorized(self.request)
|
||||||
|
response = HTTPFound('/')
|
||||||
|
await forget(self.request, response)
|
||||||
|
|
||||||
|
raise response
|
||||||
|
|
||||||
|
async def remove_user(self, username: str) -> None:
|
||||||
|
self.request.app['database'].delete_user(username)
|
@ -8,6 +8,7 @@
|
|||||||
#
|
#
|
||||||
from aiohttp.web import View
|
from aiohttp.web import View
|
||||||
from aiohttp_jinja2 import template
|
from aiohttp_jinja2 import template
|
||||||
|
from aiohttp_security import authorized_userid
|
||||||
from typing import Any, Dict
|
from typing import Any, Dict
|
||||||
|
|
||||||
|
|
||||||
@ -15,4 +16,8 @@ class IndexHtmlView(View):
|
|||||||
|
|
||||||
@template('index.jinja2')
|
@template('index.jinja2')
|
||||||
async def get(self) -> Dict[str, Any]:
|
async def get(self) -> Dict[str, Any]:
|
||||||
return {}
|
username = await authorized_userid(self.request)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'logged': username
|
||||||
|
}
|
||||||
|
@ -39,9 +39,9 @@ class LootSuggestHtmlView(LootBaseView, PlayerBaseView):
|
|||||||
return wrap_invalid_param(required, data)
|
return wrap_invalid_param(required, data)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
piece = Piece.get({'piece': data.get('piece'), 'is_tome': data.get('is_tome', False)})
|
piece = Piece.get({'piece': data.get('piece'), 'is_tome': data.get('is_tome', True)})
|
||||||
players = self.loot_put(piece)
|
players = self.loot_put(piece)
|
||||||
item_values = {'piece': piece.name, 'is_tome': piece.is_tome}
|
item_values = {'piece': piece.name, 'is_tome': getattr(piece, 'is_tome', True)}
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.request.app.logger.exception('could not manage loot')
|
self.request.app.logger.exception('could not manage loot')
|
||||||
|
62
src/service/api/views/html/users.py
Normal file
62
src/service/api/views/html/users.py
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2019 Evgeniy Alekseev.
|
||||||
|
#
|
||||||
|
# This file is part of ffxivbis
|
||||||
|
# (see https://github.com/arcan1s/ffxivbis).
|
||||||
|
#
|
||||||
|
# License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
|
||||||
|
#
|
||||||
|
from aiohttp.web import HTTPFound, Response
|
||||||
|
from aiohttp_jinja2 import template
|
||||||
|
from typing import Any, Dict, List
|
||||||
|
|
||||||
|
from service.models.user import User
|
||||||
|
|
||||||
|
from service.api.utils import wrap_exception, wrap_invalid_param
|
||||||
|
from service.api.views.common.login_base import LoginBaseView
|
||||||
|
|
||||||
|
|
||||||
|
class UsersHtmlView(LoginBaseView):
|
||||||
|
|
||||||
|
@template('users.jinja2')
|
||||||
|
async def get(self) -> Dict[str, Any]:
|
||||||
|
error = None
|
||||||
|
users: List[User] = []
|
||||||
|
|
||||||
|
try:
|
||||||
|
users = self.request.app['database'].get_users()
|
||||||
|
except Exception as e:
|
||||||
|
self.request.app.logger.exception('could not get users')
|
||||||
|
error = repr(e)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'request_error': error,
|
||||||
|
'users': users
|
||||||
|
}
|
||||||
|
|
||||||
|
async def post(self) -> Response:
|
||||||
|
data = await self.request.post()
|
||||||
|
|
||||||
|
required = ['action', 'username']
|
||||||
|
if any(param not in data for param in required):
|
||||||
|
return wrap_invalid_param(required, data)
|
||||||
|
|
||||||
|
try:
|
||||||
|
action = data.get('action')
|
||||||
|
username = data.get('username')
|
||||||
|
|
||||||
|
if action == 'add':
|
||||||
|
required = ['password', 'permission']
|
||||||
|
if any(param not in data for param in required):
|
||||||
|
return wrap_invalid_param(required, data)
|
||||||
|
await self.create_user(username, data.get('password'), data.get('permission'))
|
||||||
|
elif action == 'remove':
|
||||||
|
await self.remove_user(username)
|
||||||
|
else:
|
||||||
|
return wrap_invalid_param(['action'], data)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.request.app.logger.exception('could not manage users')
|
||||||
|
return wrap_exception(e, data)
|
||||||
|
|
||||||
|
return HTTPFound(self.request.url)
|
@ -11,12 +11,15 @@ import jinja2
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
|
from aiohttp_security import setup as setup_security
|
||||||
|
from aiohttp_security import CookiesIdentityPolicy
|
||||||
|
|
||||||
from service.core.config import Configuration
|
from service.core.config import Configuration
|
||||||
from service.core.database import Database
|
from service.core.database import Database
|
||||||
from service.core.loot_selector import LootSelector
|
from service.core.loot_selector import LootSelector
|
||||||
from service.core.party import Party
|
from service.core.party import Party
|
||||||
|
|
||||||
|
from .auth import AuthorizationPolicy, authorize_factory
|
||||||
from .routes import setup_routes
|
from .routes import setup_routes
|
||||||
|
|
||||||
|
|
||||||
@ -37,6 +40,12 @@ def setup_service(config: Configuration, database: Database, loot: LootSelector,
|
|||||||
|
|
||||||
app.middlewares.append(web.normalize_path_middleware(append_slash=False, remove_slash=True))
|
app.middlewares.append(web.normalize_path_middleware(append_slash=False, remove_slash=True))
|
||||||
|
|
||||||
|
# auth related
|
||||||
|
auth_required = config.getboolean('auth', 'enabled')
|
||||||
|
if auth_required:
|
||||||
|
setup_security(app, CookiesIdentityPolicy(), AuthorizationPolicy(database))
|
||||||
|
app.middlewares.append(authorize_factory())
|
||||||
|
|
||||||
# routes
|
# routes
|
||||||
app.logger.info('setup routes')
|
app.logger.info('setup routes')
|
||||||
setup_routes(app)
|
setup_routes(app)
|
||||||
|
@ -13,6 +13,7 @@ from service.core.config import Configuration
|
|||||||
from service.core.database import Database
|
from service.core.database import Database
|
||||||
from service.core.loot_selector import LootSelector
|
from service.core.loot_selector import LootSelector
|
||||||
from service.core.party import Party
|
from service.core.party import Party
|
||||||
|
from service.models.user import User
|
||||||
|
|
||||||
|
|
||||||
class Application:
|
class Application:
|
||||||
@ -26,6 +27,9 @@ class Application:
|
|||||||
database.migration()
|
database.migration()
|
||||||
party = Party.get(database)
|
party = Party.get(database)
|
||||||
|
|
||||||
|
admin = User(self.config.get('auth', 'root_username'), self.config.get('auth', 'root_password'), 'admin')
|
||||||
|
database.insert_user(admin, True)
|
||||||
|
|
||||||
priority = self.config.get('settings', 'priority').split()
|
priority = self.config.get('settings', 'priority').split()
|
||||||
loot_selector = LootSelector(party, priority)
|
loot_selector = LootSelector(party, priority)
|
||||||
|
|
||||||
|
@ -18,6 +18,7 @@ from service.models.loot import Loot
|
|||||||
from service.models.piece import Piece
|
from service.models.piece import Piece
|
||||||
from service.models.player import Player, PlayerId
|
from service.models.player import Player, PlayerId
|
||||||
from service.models.upgrade import Upgrade
|
from service.models.upgrade import Upgrade
|
||||||
|
from service.models.user import User
|
||||||
|
|
||||||
from .config import Configuration
|
from .config import Configuration
|
||||||
from .exceptions import InvalidDatabase
|
from .exceptions import InvalidDatabase
|
||||||
@ -59,12 +60,21 @@ class Database:
|
|||||||
def delete_player(self, player_id: PlayerId) -> None:
|
def delete_player(self, player_id: PlayerId) -> None:
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def delete_user(self, username: str) -> None:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
def get_party(self) -> List[Player]:
|
def get_party(self) -> List[Player]:
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
def get_player(self, player_id: PlayerId) -> Optional[int]:
|
def get_player(self, player_id: PlayerId) -> Optional[int]:
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def get_user(self, username: str) -> Optional[User]:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def get_users(self) -> List[User]:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
def insert_piece(self, player_id: PlayerId, piece: Union[Piece, Upgrade]) -> None:
|
def insert_piece(self, player_id: PlayerId, piece: Union[Piece, Upgrade]) -> None:
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
@ -74,6 +84,9 @@ class Database:
|
|||||||
def insert_player(self, player: Player) -> None:
|
def insert_player(self, player: Player) -> None:
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def insert_user(self, user: User, hashed_password: bool) -> None:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
def migration(self) -> None:
|
def migration(self) -> None:
|
||||||
self.logger.info('perform migrations at {}'.format(self.connection))
|
self.logger.info('perform migrations at {}'.format(self.connection))
|
||||||
backend = get_backend(self.connection)
|
backend = get_backend(self.connection)
|
||||||
|
@ -6,6 +6,7 @@
|
|||||||
#
|
#
|
||||||
# License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
|
# License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
|
||||||
#
|
#
|
||||||
|
from passlib.hash import md5_crypt
|
||||||
from typing import List, Optional, Union
|
from typing import List, Optional, Union
|
||||||
|
|
||||||
from service.models.bis import BiS
|
from service.models.bis import BiS
|
||||||
@ -14,6 +15,7 @@ from service.models.loot import Loot
|
|||||||
from service.models.piece import Piece
|
from service.models.piece import Piece
|
||||||
from service.models.player import Player, PlayerId
|
from service.models.player import Player, PlayerId
|
||||||
from service.models.upgrade import Upgrade
|
from service.models.upgrade import Upgrade
|
||||||
|
from service.models.user import User
|
||||||
|
|
||||||
from .database import Database
|
from .database import Database
|
||||||
from .sqlite_helper import SQLiteHelper
|
from .sqlite_helper import SQLiteHelper
|
||||||
@ -58,6 +60,10 @@ class SQLiteDatabase(Database):
|
|||||||
cursor.execute('''delete from players where nick = ? and job = ?''',
|
cursor.execute('''delete from players where nick = ? and job = ?''',
|
||||||
(player_id.nick, player_id.job.name))
|
(player_id.nick, player_id.job.name))
|
||||||
|
|
||||||
|
def delete_user(self, username: str) -> None:
|
||||||
|
with SQLiteHelper(self.database_path) as cursor:
|
||||||
|
cursor.execute('''delete from users where username = ?''', (username,))
|
||||||
|
|
||||||
def get_party(self) -> List[Player]:
|
def get_party(self) -> List[Player]:
|
||||||
with SQLiteHelper(self.database_path) as cursor:
|
with SQLiteHelper(self.database_path) as cursor:
|
||||||
cursor.execute('''select * from bis''')
|
cursor.execute('''select * from bis''')
|
||||||
@ -78,6 +84,17 @@ class SQLiteDatabase(Database):
|
|||||||
player = cursor.fetchone()
|
player = cursor.fetchone()
|
||||||
return player['player_id'] if player is not None else None
|
return player['player_id'] if player is not None else None
|
||||||
|
|
||||||
|
def get_user(self, username: str) -> Optional[User]:
|
||||||
|
with SQLiteHelper(self.database_path) as cursor:
|
||||||
|
cursor.execute('''select * from users where username = ?''', (username,))
|
||||||
|
user = cursor.fetchone()
|
||||||
|
return User(user['username'], user['password'], user['permission']) if user is not None else None
|
||||||
|
|
||||||
|
def get_users(self) -> List[User]:
|
||||||
|
with SQLiteHelper(self.database_path) as cursor:
|
||||||
|
cursor.execute('''select * from users''')
|
||||||
|
return [User(user['username'], user['password'], user['permission']) for user in cursor.fetchall()]
|
||||||
|
|
||||||
def insert_piece(self, player_id: PlayerId, piece: Union[Piece, Upgrade]) -> None:
|
def insert_piece(self, player_id: PlayerId, piece: Union[Piece, Upgrade]) -> None:
|
||||||
player = self.get_player(player_id)
|
player = self.get_player(player_id)
|
||||||
if player is None:
|
if player is None:
|
||||||
@ -114,4 +131,15 @@ class SQLiteDatabase(Database):
|
|||||||
values
|
values
|
||||||
(?, ?, ?, ?, ?)''',
|
(?, ?, ?, ?, ?)''',
|
||||||
(Database.now(), player.nick, player.job.name, player.link, player.priority)
|
(Database.now(), player.nick, player.job.name, player.link, player.priority)
|
||||||
|
)
|
||||||
|
|
||||||
|
def insert_user(self, user: User, hashed_password: bool) -> None:
|
||||||
|
password = user.password if hashed_password else md5_crypt.hash(user.password)
|
||||||
|
with SQLiteHelper(self.database_path) as cursor:
|
||||||
|
cursor.execute(
|
||||||
|
'''replace into users
|
||||||
|
(username, password, permission)
|
||||||
|
values
|
||||||
|
(?, ?, ?)''',
|
||||||
|
(user.username, password, user.permission)
|
||||||
)
|
)
|
16
src/service/models/user.py
Normal file
16
src/service/models/user.py
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2019 Evgeniy Alekseev.
|
||||||
|
#
|
||||||
|
# This file is part of ffxivbis
|
||||||
|
# (see https://github.com/arcan1s/ffxivbis).
|
||||||
|
#
|
||||||
|
# License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
|
||||||
|
#
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class User:
|
||||||
|
username: str
|
||||||
|
password: str
|
||||||
|
permission: str
|
@ -8,10 +8,27 @@
|
|||||||
|
|
||||||
<body>
|
<body>
|
||||||
<center>
|
<center>
|
||||||
|
{% if logged is defined and logged %}
|
||||||
|
<form action="/api/v1/logout" method="post">
|
||||||
|
logged in as {{ logged|e }}
|
||||||
|
<input name="logout" id="logout" type="submit" value="logout"/>
|
||||||
|
</form>
|
||||||
|
{% else %}
|
||||||
|
<form action="/api/v1/login" method="post">
|
||||||
|
<input name="username" id="username" title="username" placeholder="username" type="text" required/>
|
||||||
|
<input name="password" id="password" title="password" placeholder="password" type="password" required/>
|
||||||
|
<input name="login" id="login" type="submit" value="login"/>
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
<h2><a href="/party" title="party">party</a></h2>
|
<h2><a href="/party" title="party">party</a></h2>
|
||||||
<h2><a href="/bis" title="bis management">bis</a></h2>
|
<h2><a href="/bis" title="bis management">bis</a></h2>
|
||||||
<h2><a href="/loot" title="loot management">loot</a></h2>
|
<h2><a href="/loot" title="loot management">loot</a></h2>
|
||||||
<h2><a href="/suggest" title="suggest loot">suggest</a></h2>
|
<h2><a href="/suggest" title="suggest loot">suggest</a></h2>
|
||||||
|
<hr>
|
||||||
|
<h2><a href="/admin/users" title="manage users">manage users</a></h2>
|
||||||
</center>
|
</center>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
51
templates/users.jinja2
Normal file
51
templates/users.jinja2
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<title>Users</title>
|
||||||
|
|
||||||
|
<link href="{{ static('styles.css') }}" rel="stylesheet" type="text/css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h2>users</h2>
|
||||||
|
|
||||||
|
{% include "error.jinja2" %}
|
||||||
|
{% include "search_line.jinja2" %}
|
||||||
|
|
||||||
|
<form action="/admin/users" method="post">
|
||||||
|
<input name="username" id="username" title="username" placeholder="username" type="text"/>
|
||||||
|
<input name="password" id="password" title="password" placeholder="password" type="password"/>
|
||||||
|
<select name="permission" id="permission" title="permission">
|
||||||
|
<option>get</option>
|
||||||
|
<option>post</option>
|
||||||
|
</select>
|
||||||
|
<input name="action" id="action" type="hidden" value="add"/>
|
||||||
|
<input name="add" id="add" type="submit" value="add"/>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<table id="result">
|
||||||
|
<tr>
|
||||||
|
<th>username</th>
|
||||||
|
<th>permission</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
{% for user in users %}
|
||||||
|
<tr>
|
||||||
|
<td class="include_search">{{ user.username|e }}</td>
|
||||||
|
<td>{{ user.permission|e }}</td>
|
||||||
|
<td>
|
||||||
|
<form action="/admin/users" method="post">
|
||||||
|
<input name="username" id="username" type="hidden" value="{{ user.username|e }}"/>
|
||||||
|
<input name="action" id="action" type="hidden" value="remove"/>
|
||||||
|
<input name="remove" id="remove" type="submit" value="x"/>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</table>
|
||||||
|
|
||||||
|
{% include "export_to_csv.jinja2" %}
|
||||||
|
{% include "root.jinja2" %}
|
||||||
|
{% include "search.jinja2" %}
|
||||||
|
</body>
|
||||||
|
</html>
|
Loading…
Reference in New Issue
Block a user