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.
|
||||
* `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
|
||||
@ -93,6 +121,14 @@ Service which allows to manage savage loot distribution easy.
|
||||
* `xivapi_key`: xivapi developer key, string, optional.
|
||||
* `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
|
||||
|
||||
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]
|
||||
host = 0.0.0.0
|
||||
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']),
|
||||
|
||||
install_requires=[
|
||||
'aiohttp_jinja2',
|
||||
'aiohttp',
|
||||
'aiohttp_jinja2',
|
||||
'aiohttp_security',
|
||||
'Jinja2',
|
||||
'passlib',
|
||||
'requests',
|
||||
'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 service.api.views.api.bis import BiSView
|
||||
from service.api.views.api.loot import LootView
|
||||
from service.api.views.api.player import PlayerView
|
||||
from service.api.views.html.bis import BiSHtmlView
|
||||
from service.api.views.html.index import IndexHtmlView
|
||||
from service.api.views.html.loot import LootHtmlView
|
||||
from service.api.views.html.loot_suggest import LootSuggestHtmlView
|
||||
from service.api.views.html.player import PlayerHtmlView
|
||||
from service.api.views.html.static import StaticHtmlView
|
||||
from .views.api.bis import BiSView
|
||||
from .views.api.login import LoginView
|
||||
from .views.api.logout import LogoutView
|
||||
from .views.api.loot import LootView
|
||||
from .views.api.player import PlayerView
|
||||
from .views.html.bis import BiSHtmlView
|
||||
from .views.html.index import IndexHtmlView
|
||||
from .views.html.loot import LootHtmlView
|
||||
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:
|
||||
# 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_post('/api/v1/party', PlayerView)
|
||||
|
||||
@ -47,3 +55,8 @@ def setup_routes(app: Application) -> None:
|
||||
|
||||
app.router.add_get('/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
|
||||
|
||||
from aiohttp.web import Response
|
||||
from aiohttp.web import HTTPException, Response
|
||||
from typing import Any, Mapping, List
|
||||
|
||||
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:
|
||||
if isinstance(exception, HTTPException):
|
||||
raise exception # reraise return
|
||||
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_jinja2 import template
|
||||
from aiohttp_security import authorized_userid
|
||||
from typing import Any, Dict
|
||||
|
||||
|
||||
@ -15,4 +16,8 @@ class IndexHtmlView(View):
|
||||
|
||||
@template('index.jinja2')
|
||||
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)
|
||||
|
||||
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)
|
||||
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:
|
||||
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
|
||||
|
||||
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.database import Database
|
||||
from service.core.loot_selector import LootSelector
|
||||
from service.core.party import Party
|
||||
|
||||
from .auth import AuthorizationPolicy, authorize_factory
|
||||
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))
|
||||
|
||||
# auth related
|
||||
auth_required = config.getboolean('auth', 'enabled')
|
||||
if auth_required:
|
||||
setup_security(app, CookiesIdentityPolicy(), AuthorizationPolicy(database))
|
||||
app.middlewares.append(authorize_factory())
|
||||
|
||||
# routes
|
||||
app.logger.info('setup routes')
|
||||
setup_routes(app)
|
||||
|
@ -13,6 +13,7 @@ from service.core.config import Configuration
|
||||
from service.core.database import Database
|
||||
from service.core.loot_selector import LootSelector
|
||||
from service.core.party import Party
|
||||
from service.models.user import User
|
||||
|
||||
|
||||
class Application:
|
||||
@ -26,6 +27,9 @@ class Application:
|
||||
database.migration()
|
||||
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()
|
||||
loot_selector = LootSelector(party, priority)
|
||||
|
||||
|
@ -18,6 +18,7 @@ from service.models.loot import Loot
|
||||
from service.models.piece import Piece
|
||||
from service.models.player import Player, PlayerId
|
||||
from service.models.upgrade import Upgrade
|
||||
from service.models.user import User
|
||||
|
||||
from .config import Configuration
|
||||
from .exceptions import InvalidDatabase
|
||||
@ -59,12 +60,21 @@ class Database:
|
||||
def delete_player(self, player_id: PlayerId) -> None:
|
||||
raise NotImplementedError
|
||||
|
||||
def delete_user(self, username: str) -> None:
|
||||
raise NotImplementedError
|
||||
|
||||
def get_party(self) -> List[Player]:
|
||||
raise NotImplementedError
|
||||
|
||||
def get_player(self, player_id: PlayerId) -> Optional[int]:
|
||||
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:
|
||||
raise NotImplementedError
|
||||
|
||||
@ -74,6 +84,9 @@ class Database:
|
||||
def insert_player(self, player: Player) -> None:
|
||||
raise NotImplementedError
|
||||
|
||||
def insert_user(self, user: User, hashed_password: bool) -> None:
|
||||
raise NotImplementedError
|
||||
|
||||
def migration(self) -> None:
|
||||
self.logger.info('perform migrations at {}'.format(self.connection))
|
||||
backend = get_backend(self.connection)
|
||||
|
@ -6,6 +6,7 @@
|
||||
#
|
||||
# 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 service.models.bis import BiS
|
||||
@ -14,6 +15,7 @@ from service.models.loot import Loot
|
||||
from service.models.piece import Piece
|
||||
from service.models.player import Player, PlayerId
|
||||
from service.models.upgrade import Upgrade
|
||||
from service.models.user import User
|
||||
|
||||
from .database import Database
|
||||
from .sqlite_helper import SQLiteHelper
|
||||
@ -58,6 +60,10 @@ class SQLiteDatabase(Database):
|
||||
cursor.execute('''delete from players where nick = ? and job = ?''',
|
||||
(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]:
|
||||
with SQLiteHelper(self.database_path) as cursor:
|
||||
cursor.execute('''select * from bis''')
|
||||
@ -78,6 +84,17 @@ class SQLiteDatabase(Database):
|
||||
player = cursor.fetchone()
|
||||
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:
|
||||
player = self.get_player(player_id)
|
||||
if player is None:
|
||||
@ -114,4 +131,15 @@ class SQLiteDatabase(Database):
|
||||
values
|
||||
(?, ?, ?, ?, ?)''',
|
||||
(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>
|
||||
<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="/bis" title="bis management">bis</a></h2>
|
||||
<h2><a href="/loot" title="loot management">loot</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>
|
||||
</body>
|
||||
</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