mirror of
				https://github.com/arcan1s/ffxivbis.git
				synced 2025-11-04 07:03:41 +00:00 
			
		
		
		
	auth support
This commit is contained in:
		
							
								
								
									
										36
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										36
									
								
								README.md
									
									
									
									
									
								
							@ -73,6 +73,34 @@ Service which allows to manage savage loot distribution easy.
 | 
				
			|||||||
    * `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
 | 
				
			||||||
 | 
					
 | 
				
			||||||
* `settings` section
 | 
					* `settings` section
 | 
				
			||||||
@ -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:
 | 
				
			||||||
@ -115,3 +132,14 @@ class SQLiteDatabase(Database):
 | 
				
			|||||||
                (?, ?, ?, ?, ?)''',
 | 
					                (?, ?, ?, ?, ?)''',
 | 
				
			||||||
                (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>
 | 
				
			||||||
		Reference in New Issue
	
	Block a user