summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--authentication/endpoints.py99
-rw-r--r--authentication/templates/base.html65
-rw-r--r--authentication/templates/register.html24
-rw-r--r--management/__init__.py0
-rw-r--r--management/endpoints.py50
-rw-r--r--management/templates/base.html7
-rw-r--r--management/templates/dashboard.html9
-rw-r--r--management/templates/users.html11
-rw-r--r--templates/global.html60
-rw-r--r--ttun_server/__init__.py4
10 files changed, 254 insertions, 75 deletions
diff --git a/authentication/endpoints.py b/authentication/endpoints.py
index f94d55b..18f3ba6 100644
--- a/authentication/endpoints.py
+++ b/authentication/endpoints.py
@@ -1,12 +1,11 @@
1import json 1import json
2from base64 import b64encode 2from base64 import b64encode
3from functools import partial 3from functools import partial
4from typing import Annotated 4from typing import Annotated, Self
5from uuid import uuid7 5from uuid import uuid7
6 6
7from alembic.testing import requirements
8from fastapi import FastAPI, Request, Form, Query 7from fastapi import FastAPI, Request, Form, Query
9from pydantic import BaseModel, ValidationError 8from pydantic import BaseModel, ValidationError, model_validator, EmailStr
10from sqlmodel import select 9from sqlmodel import select
11from starlette.authentication import requires 10from starlette.authentication import requires
12from starlette.middleware.authentication import AuthenticationMiddleware 11from starlette.middleware.authentication import AuthenticationMiddleware
@@ -31,24 +30,32 @@ oauth.add_middleware(SessionMiddleware, secret_key=conf.SECRET_KEY)
31auth_server = AuthorizationServer() 30auth_server = AuthorizationServer()
32code_grant = partial(AuthorizationCodeGrant, server=auth_server) 31code_grant = partial(AuthorizationCodeGrant, server=auth_server)
33 32
34templates = Jinja2Templates(directory='authentication/templates') 33templates = Jinja2Templates(directory=[
34 'templates',
35 'authentication/templates'
36])
37
35 38
36class LoginForm(BaseModel): 39class LoginForm(BaseModel):
37 username: str 40 username: str
38 password: str 41 password: str
39 42
43
40async def base_login(request: Request, form: LoginForm | None = None, errors: dict[str, str] | None = None): 44async def base_login(request: Request, form: LoginForm | None = None, errors: dict[str, str] | None = None):
41 return templates.TemplateResponse(request, 'login.html', context={ 45 return templates.TemplateResponse(request, 'login.html', context={
42 'form': form, 46 'form': form,
43 'errors': errors, 47 'errors': errors,
44 }) 48 })
45 49
50
46@authentication.get('/login/', response_class=HTMLResponse) 51@authentication.get('/login/', response_class=HTMLResponse)
47async def get_login(request: Request, next: Annotated[str, Query()] = None): 52async def get_login(request: Request, next: Annotated[str, Query()] = None):
48 return await base_login(request) 53 return await base_login(request)
49 54
55
50@authentication.post('/login/', response_class=HTMLResponse) 56@authentication.post('/login/', response_class=HTMLResponse)
51async def post_login(request: Request, username: Annotated[str, Form()] = '', password: Annotated[str, Form()] = '', next: Annotated[str, Query()] = None): 57async def post_login(request: Request, username: Annotated[str, Form()] = '', password: Annotated[str, Form()] = '',
58 next: Annotated[str, Query()] = None):
52 form = None 59 form = None
53 errors = {} 60 errors = {}
54 61
@@ -70,35 +77,101 @@ async def post_login(request: Request, username: Annotated[str, Form()] = '', pa
70 request.session.update({'id': str(session.id)}) 77 request.session.update({'id': str(session.id)})
71 78
72 if next is not None: 79 if next is not None:
73 return RedirectResponse(next) 80 return RedirectResponse(next, status_code=303)
74 81
75 except ValidationError as exc: 82 except ValidationError as exc:
76 errors.update({ e['loc'][0]: e['msg'] for e in exc.errors() }) 83 errors.update({e['loc'][0]: e['msg'] for e in exc.errors()})
77 84
78 return await base_login(request, form, errors) 85 return await base_login(request, form, errors)
79 86
80async def base_authorize(request: Request, user: User | None = None): 87
81 if not conf.IS_LOCAL: 88@authentication.get('/logout/')
89def logout(request: Request):
90 with get_session_context() as db_session:
91 db_session.query(Session).filter(Session.id == request.session.get('id')).delete()
92
93 request.session.clear()
94 return RedirectResponse(request.headers.get('Referer', '/management/dashboard/'), status_code=303)
95
96
97class RegisterForm(BaseModel):
98 email: EmailStr
99 password: str
100 verify_password: str
101
102 @model_validator(mode='after')
103 def check_passwords_match(self) -> Self:
104 if self.password != self.verify_password:
105 raise ValueError('Passwords do not match')
106 return self
107
108
109async def base_register(request: Request, form: RegisterForm | None = None, errors: dict[str, str] | None = None):
110 return templates.TemplateResponse(request, 'register.html', context={
111 'form': form,
112 'errors': errors,
113 })
114
115
116@authentication.get('/register/', response_class=HTMLResponse)
117async def get_register(request: Request, next: Annotated[str, Query()] = None):
118 return await base_register(request)
119
120
121@authentication.post('/register/', response_class=HTMLResponse)
122async def post_register(request: Request, email: Annotated[str, Form()] = '', password: Annotated[str, Form()] = '',
123 verify_password: Annotated[str, Form()] = '', next: Annotated[str, Query()] = None):
124 form = None
125 errors = {}
126
127 try:
128 form = RegisterForm.model_validate({
129 'email': email,
130 'password': password,
131 'verify_password': verify_password
132 })
133
82 with get_session_context() as db_session: 134 with get_session_context() as db_session:
83 db_session.query(Session).filter(Session.id == request.session.get('id')).delete() 135 user = User(username=email, email=email)
136 user.set_password(password)
137 db_session.add(user)
138
139 session = Session(id=uuid7(), user=user)
140 db_session.add(session)
141
142 request.session.update({'id': str(session.id)})
143
144 if next is not None:
145 return RedirectResponse(next, status_code=303)
84 146
147 except ValidationError as exc:
148 errors.update({e['loc'][0]: e['msg'] for e in exc.errors()})
149
150 return await base_register(request, form, errors)
151
152
153async def base_authorize(request: Request, user: User | None = None):
85 return auth_server.create_authorization_response(request, request.user, grant=None) 154 return auth_server.create_authorization_response(request, request.user, grant=None)
86 155
156
87@oauth.get('/authorize/') 157@oauth.get('/authorize/')
88@requires(['authenticated', 'session'], redirect='get_login') 158@requires(['authenticated', 'session'], redirect='get_login')
89async def get_authorize(request: Request): 159async def get_authorize(request: Request):
90 return await base_authorize(request, request.user) 160 return await base_authorize(request, request.user)
91 161
162
92@oauth.post('/authorize/') 163@oauth.post('/authorize/')
93@requires(['authenticated', 'session'], redirect='get_login') 164@requires(['authenticated', 'session'], redirect='get_login')
94async def post_authorize(request: Request): 165async def post_authorize(request: Request):
95 return await base_authorize(request, request.user) 166 return await base_authorize(request, request.user)
96 167
168
97@oauth.post('/token/') 169@oauth.post('/token/')
98async def post_token(request: Request): 170async def post_token(request: Request):
99 await prepare_oauth_request(request) 171 await prepare_oauth_request(request)
100 return auth_server.create_token_response(request) 172 return auth_server.create_token_response(request)
101 173
174
102@authentication.get('/connect/') 175@authentication.get('/connect/')
103async def connect(request: Request): 176async def connect(request: Request):
104 origin = request.url.scheme + '://' + request.url.netloc 177 origin = request.url.scheme + '://' + request.url.netloc
@@ -107,16 +180,18 @@ async def connect(request: Request):
107 app = session.exec(select(ClientApplication)).first() 180 app = session.exec(select(ClientApplication)).first()
108 181
109 return RedirectResponse( 182 return RedirectResponse(
110 '/oauth' + get_url_path_with_query(oauth, 'get_authorize', { 183 '/oauth' + get_url_path_with_query(oauth, 'get_authorize', {
111 'client_id': app.client_id, 184 'client_id': app.client_id,
112 'redirect_uri': f'{origin}/oauth' + get_url_path_with_query(oauth, 'callback'), 185 'redirect_uri': f'{origin}/oauth' + get_url_path_with_query(oauth, 'callback'),
113 'response_type': 'code', 186 'response_type': 'code',
114 'state': b64encode(json.dumps({ 187 'state': b64encode(json.dumps({
115 'client_id': app.client_id, 188 'client_id': app.client_id,
116 }).encode()).decode(), 189 }).encode()).decode(),
117 }) 190 }),
191 status_code=303,
118 ) 192 )
119 193
194
120@oauth.get('/config/') 195@oauth.get('/config/')
121async def get_config(request: Request): 196async def get_config(request: Request):
122 with get_session_context() as session: 197 with get_session_context() as session:
diff --git a/authentication/templates/base.html b/authentication/templates/base.html
index 47d007e..9874313 100644
--- a/authentication/templates/base.html
+++ b/authentication/templates/base.html
@@ -1,63 +1,4 @@
1<!DOCTYPE html> 1{% extends 'global.html' %}
2<html lang="en"> 2{% block template %}
3<head>
4 <meta charset="UTF-8">
5 <title>TTUN</title>
6 <style>
7/* http://meyerweb.com/eric/tools/css/reset/