geändert: Makefile
geändert: docker-compose.yml neue Datei: keys/ec_private.pem neue Datei: keys/ec_public.pem geändert: main.py geändert: requirements.txt
This commit is contained in:
@@ -1,20 +1,131 @@
|
||||
from fastapi import FastAPI, Depends
|
||||
from fastapi import FastAPI, Depends, HTTPException, Response, Request, status, Cookie
|
||||
from psycopg_pool import AsyncConnectionPool
|
||||
import psycopg
|
||||
import uvicorn
|
||||
import os
|
||||
from pydantic import BaseModel
|
||||
from pydantic import BaseModel, Field, model_validator, field_validator
|
||||
import time
|
||||
from datetime import date, datetime, timedelta, timezone
|
||||
from confluent_kafka import Producer
|
||||
import uuid
|
||||
import hashlib
|
||||
import jwt
|
||||
|
||||
class Item(BaseModel):
|
||||
name: str
|
||||
description: str
|
||||
reporter: str
|
||||
priority: int
|
||||
is_stupid: bool
|
||||
# class Item(BaseModel):
|
||||
# name: str
|
||||
# description: str
|
||||
# reporter: str
|
||||
# priority: int
|
||||
# is_stupid: bool
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
with open('keys/ec_private.pem', 'rb') as f:
|
||||
PRIVATE_KEY = f.read()
|
||||
|
||||
with open('keys/ec_public.pem', 'rb') as f:
|
||||
PUBLIC_KEY = f.read()
|
||||
|
||||
REGISTRATION_MINIMUM_AGE = 4745
|
||||
|
||||
def get_month_length(month, year = time.localtime().tm_year):
|
||||
assert not (month > 12 or month < 1)
|
||||
return {
|
||||
1: 31,
|
||||
2: 29 if year % 4 == 0 else 28,
|
||||
3: 31,
|
||||
4: 30,
|
||||
5: 31,
|
||||
6: 30,
|
||||
7: 31,
|
||||
8: 31,
|
||||
9: 30,
|
||||
10: 31,
|
||||
11: 30,
|
||||
12: 31
|
||||
}[month]
|
||||
|
||||
class User(BaseModel):
|
||||
email: str = Field(..., max_length = 64)
|
||||
password: str
|
||||
birthYear: int
|
||||
birthMonth: int = Field(..., ge=1, le=12)
|
||||
birthDay: int
|
||||
firstName: str = Field(..., max_length = 64)
|
||||
lastName: str = Field(..., max_length = 64)
|
||||
preInstalledApplications: dict
|
||||
|
||||
@model_validator(mode='before')
|
||||
def strip_all(cls, values):
|
||||
return {k: v.strip() if type(v) and k != 'password' == str else v for k, v in values.items()}
|
||||
|
||||
@field_validator('email')
|
||||
def validate_email(cls, value):
|
||||
def e(): raise ValueError('invalid e-mail address')
|
||||
if not '@' in value or ' ' in value:
|
||||
e()
|
||||
user, host = value.split('@')
|
||||
if host.endswith('.'):
|
||||
host = host[:-1]
|
||||
if not user or not host or '.' not in host or host.startswith('.'):
|
||||
e()
|
||||
labels = host.split('.')
|
||||
for l in labels:
|
||||
if len(l.encode()) < 1 or len(l.encode()) > 63:
|
||||
e()
|
||||
return value
|
||||
|
||||
@field_validator('password')
|
||||
def validate_password_security(cls, password):
|
||||
password = password
|
||||
number_of_caps = 0
|
||||
number_of_smalls = 0
|
||||
number_of_numbers = 0
|
||||
number_of_specials = 0
|
||||
for char in password:
|
||||
if char.isalnum():
|
||||
if char.isalpha():
|
||||
if char.capitalize() == char:
|
||||
number_of_caps += 1
|
||||
else:
|
||||
number_of_smalls += 1
|
||||
else:
|
||||
number_of_numbers += 1
|
||||
else:
|
||||
number_of_specials += 1
|
||||
if not (number_of_caps > 0 and number_of_numbers > 0 and number_of_smalls > 0 and number_of_specials > 0 and len(password) > 7):
|
||||
raise ValueError('the password does not match the requirements')
|
||||
return password
|
||||
|
||||
@model_validator(mode='before')
|
||||
def validate_pre_installed_applications(cls, values):
|
||||
...
|
||||
return values
|
||||
|
||||
@model_validator(mode='before')
|
||||
def validate_birth_date(cls, values):
|
||||
day = values.get('birthDay')
|
||||
if day < 1 or day > get_month_length(values.get('birthMonth'), values.get('birthYear')):
|
||||
raise ValueError('invalid birth day')
|
||||
today = date.today()
|
||||
birth_date = date(values.get('birthYear'), values.get('birthMonth'), day)
|
||||
if not (today >= birth_date):
|
||||
raise ValueError('the date cannot be in future')
|
||||
if (today - birth_date).days < REGISTRATION_MINIMUM_AGE:
|
||||
raise ValueError('age restriction, minimum age is 13 years')
|
||||
return values
|
||||
|
||||
|
||||
class Session(BaseModel):
|
||||
password: str
|
||||
stayLoggedIn: bool = Field(False)
|
||||
|
||||
app = FastAPI(
|
||||
# docs_url = None,
|
||||
# redoc_url = None,
|
||||
# openapi_url = None
|
||||
)
|
||||
pool: AsyncConnectionPool
|
||||
|
||||
|
||||
@app.on_event('startup')
|
||||
async def on_startup():
|
||||
global pool
|
||||
@@ -28,7 +139,8 @@ async def on_startup():
|
||||
|
||||
async with pool.connection() as conn:
|
||||
async with conn.cursor() as cursor:
|
||||
await cursor.execute('CREATE TABLE IF NOT EXISTS users (id SERIAL PRIMARY KEY, email VARCHAR(64), remail VARCHAR(64), phone VARCHAR(16), password VARCHAR(128))')
|
||||
await cursor.execute('CREATE TABLE IF NOT EXISTS users (id UUID PRIMARY KEY, email VARCHAR(64) UNIQUE, phone VARCHAR(32), password BYTEA)')
|
||||
await cursor.execute('CREATE TABLE IF NOT EXISTS recovery (id UUID PRIMARY KEY, remail VARCHAR(64))')
|
||||
print('[INFO] Database initialized')
|
||||
await conn.commit()
|
||||
|
||||
@@ -41,38 +153,144 @@ async def get_conn():
|
||||
async with pool.connection() as conn:
|
||||
yield conn
|
||||
|
||||
@app.get('/')
|
||||
def read_root():
|
||||
@app.post('/users')
|
||||
async def create_user(user: User, conn = Depends(get_conn)):
|
||||
'''Register a user'''
|
||||
async with conn.cursor() as cursor:
|
||||
user_id = uuid.uuid4()
|
||||
print(await (await cursor.execute('SELECT * FROM users')).fetchall())
|
||||
print(await (await cursor.execute('SELECT * FROM recovery')).fetchall())
|
||||
try:
|
||||
await cursor.execute('INSERT INTO users (id, email, phone, password) VALUES (%s, %s, %s, %s)', (user_id, user.email, None, hashlib.sha3_512(user.password.encode()).digest()))
|
||||
await cursor.execute('INSERT INTO recovery (id, remail) VALUES (%s, %s)', (user_id, None))
|
||||
except psycopg.errors.UniqueViolation:
|
||||
raise HTTPException(status_code=409, detail = 'this user is already registered')
|
||||
conn.commit()
|
||||
return {
|
||||
'message': 'microservice is running'
|
||||
'id': user_id,
|
||||
'email': user.email,
|
||||
'remail': None,
|
||||
'phone': None
|
||||
}
|
||||
|
||||
@app.post('/item')
|
||||
async def create_item(item: Item, conn = Depends(get_conn)):
|
||||
@app.post('/users/{user}/session')
|
||||
async def create_session(response: Response, session: Session, user: uuid.UUID, conn = Depends(get_conn)):
|
||||
'''Create a session'''
|
||||
async with conn.cursor() as cursor:
|
||||
await cursor.execute('INSERT INTO items (name, description, reporter, priority, is_stupid) VALUES (%s, %s, %s, %s, %s) RETURNING *', (item.name, item.description, item.reporter, item.priority, item.is_stupid))
|
||||
i = await cursor.fetchone()
|
||||
return {'id': i[0], 'description' : i[1], 'reporter': i[2], 'priority': i[3], 'is_stupid': i[4]}
|
||||
await cursor.execute('SELECT COUNT(*) FROM users WHERE id = %s', (user,))
|
||||
if not (await cursor.fetchone())[0]:
|
||||
raise HTTPException(status_code = 404, detail = 'user not found')
|
||||
await cursor.execute('SELECT password FROM users WHERE id = %s', (user,))
|
||||
if hashlib.sha3_512(session.password.encode()).digest() != (await cursor.fetchone())[0]:
|
||||
raise HTTPException(status_code = 401, detail = 'Unauthorized')
|
||||
payload = {
|
||||
'iss': 'http://localhost:1234',
|
||||
'sub': str(user),
|
||||
'aud': 'jcloud-auth',
|
||||
'iat': datetime.now(tz=timezone.utc),
|
||||
'exp': datetime.now(tz=timezone.utc) + timedelta(days=20),
|
||||
}
|
||||
token = jwt.encode(
|
||||
payload,
|
||||
PRIVATE_KEY,
|
||||
algorithm = 'ES256',
|
||||
# headers = {
|
||||
# 'kid': 'ec-key-2025-01'
|
||||
# }
|
||||
)
|
||||
response.set_cookie(
|
||||
key = 'auth_token',
|
||||
value = token,
|
||||
max_age = 1728000 if session.stayLoggedIn else None,
|
||||
httponly = True,
|
||||
secure = False,
|
||||
samesite = 'strict'
|
||||
)
|
||||
response.set_cookie(
|
||||
key = 'user',
|
||||
value = str(user),
|
||||
max_age = 1728000 if session.stayLoggedIn else None,
|
||||
httponly = True,
|
||||
secure = False,
|
||||
samesite = 'strict',
|
||||
)
|
||||
response.set_cookie
|
||||
return {
|
||||
'user': user,
|
||||
'authToken': token,
|
||||
'exp': datetime.now(tz=timezone.utc) + timedelta(days=20)
|
||||
}
|
||||
|
||||
@app.get('/item/{item_id}')
|
||||
async def read_item(item_id, conn = Depends(get_conn)):
|
||||
@app.delete('/users/session', status_code = status.HTTP_204_NO_CONTENT)
|
||||
async def delete_session(response: Response):
|
||||
'''Delete a session'''
|
||||
response.set_cookie(
|
||||
key = 'auth_token',
|
||||
value = '',
|
||||
max_age = -1,
|
||||
httponly = True,
|
||||
secure = False
|
||||
)
|
||||
response.set_cookie(
|
||||
key = 'user',
|
||||
value = '',
|
||||
max_age = -1,
|
||||
httponly = True,
|
||||
secure = False
|
||||
)
|
||||
|
||||
@app.get('/users/{user}/validity')
|
||||
async def validate_token(user: uuid.UUID, conn = Depends(get_conn), Auth: str = Cookie(...)):
|
||||
'''Validate a token'''
|
||||
async with conn.cursor() as cursor:
|
||||
await cursor.execute('SELECT * FROM items WHERE id = %s', (item_id,))
|
||||
i = await cursor.fetchone()
|
||||
return {'id': i[0], 'name' : i[1], 'description': i[2], 'reporter': i[3], 'priority': i[4], 'is_stupid': i[5]}
|
||||
await cursor.execute('SELECT COUNT(*) FROM users WHERE id = %s', (user,))
|
||||
if not (await cursor.fetchone())[0]:
|
||||
raise HTTPException(status_code = 404, detail = 'User not found')
|
||||
try:
|
||||
jwt.decode(
|
||||
Auth,
|
||||
PUBLIC_KEY,
|
||||
algorithms = ['ES256'],
|
||||
audience = 'jcloud-auth',
|
||||
issuer = 'http://localhost:1234',
|
||||
subject = str(user)
|
||||
)
|
||||
except:
|
||||
raise HTTPException(status_code = 401, detail = 'Unauthorized')
|
||||
return b'success'
|
||||
|
||||
|
||||
@app.get('/items')
|
||||
async def read_item(conn = Depends(get_conn)):
|
||||
async with conn.cursor() as cursor:
|
||||
await cursor.execute('SELECT * FROM items')
|
||||
items = await cursor.fetchall()
|
||||
return [{'id': i[0], 'description' : i[1], 'reporter': i[2], 'priority': i[3], 'is_stupid': i[4]} for i in items]
|
||||
@app.get('/health')
|
||||
async def get_health_status():
|
||||
'''Sends `OK`'''
|
||||
return b'OK'
|
||||
|
||||
@app.get('/datadir')
|
||||
async def get_datadir(conn = Depends(get_conn)):
|
||||
async with conn.cursor() as cursor:
|
||||
await cursor.execute('SHOW data_directory')
|
||||
return {'res': await cursor.fetchall()}
|
||||
# @app.post('/item')
|
||||
# async def create_item(item: Item, conn = Depends(get_conn)):
|
||||
# async with conn.cursor() as cursor:
|
||||
# await cursor.execute('INSERT INTO items (name, description, reporter, priority, is_stupid) VALUES (%s, %s, %s, %s, %s) RETURNING *', (item.name, item.description, item.reporter, item.priority, item.is_stupid))
|
||||
# i = await cursor.fetchone()
|
||||
# return {'id': i[0], 'description' : i[1], 'reporter': i[2], 'priority': i[3], 'is_stupid': i[4]}
|
||||
|
||||
# @app.get('/item/{item_id}')
|
||||
# async def read_item(item_id, conn = Depends(get_conn)):
|
||||
# async with conn.cursor() as cursor:
|
||||
# await cursor.execute('SELECT * FROM items WHERE id = %s', (item_id,))
|
||||
# i = await cursor.fetchone()
|
||||
# return {'id': i[0], 'name' : i[1], 'description': i[2], 'reporter': i[3], 'priority': i[4], 'is_stupid': i[5]}
|
||||
|
||||
# @app.get('/items')
|
||||
# async def read_item(conn = Depends(get_conn)):
|
||||
# async with conn.cursor() as cursor:
|
||||
# await cursor.execute('SELECT * FROM items')
|
||||
# items = await cursor.fetchall()
|
||||
# return [{'id': i[0], 'description' : i[1], 'reporter': i[2], 'priority': i[3], 'is_stupid': i[4]} for i in items]
|
||||
|
||||
# @app.get('/datadir')
|
||||
# async def get_datadir(conn = Depends(get_conn)):
|
||||
# async with conn.cursor() as cursor:
|
||||
# await cursor.execute('SHOW data_directory')
|
||||
# return {'res': await cursor.fetchall()}
|
||||
|
||||
if __name__ == '__main__':
|
||||
uvicorn.run(
|
||||
|
||||
Reference in New Issue
Block a user