From 1ffc6090580b45bac4cadc0560f0cb8c14204842 Mon Sep 17 00:00:00 2001 From: Izalia Mae Date: Fri, 15 Mar 2024 06:46:31 -0400 Subject: [PATCH] add user management api endpoints and allow cookie for api auth --- relay/data/swagger.yaml | 117 +++++++++++++++++++++++++++++++++++ relay/database/connection.py | 29 ++++++--- relay/views/api.py | 63 ++++++++++++++++++- relay/views/base.py | 2 +- 4 files changed, 202 insertions(+), 9 deletions(-) diff --git a/relay/data/swagger.yaml b/relay/data/swagger.yaml index b4136ec..a2a51dc 100644 --- a/relay/data/swagger.yaml +++ b/relay/data/swagger.yaml @@ -13,6 +13,10 @@ schemes: - https securityDefinitions: + Cookie: + type: apiKey + in: cookie + name: user-token Bearer: type: apiKey name: Authorization @@ -549,6 +553,104 @@ paths: schema: $ref: "#/definitions/Error" + /v1/user: + get: + tags: + - User + description: Get a list of all local users + produces: + - application/json + responses: + "200": + description: List of users + schema: + type: array + items: + $ref: "#/definitions/User" + + post: + tags: + - User + description: Create a new user + parameters: + - in: formData + name: username + required: true + type: string + - in: formData + name: password + required: true + type: string + format: password + - in: formData + name: handle + required: false + type: string + format: email + produces: + - application/json + responses: + "200": + description: Newly created user + schema: + $ref: "#/definitions/User" + "404": + description: User already exists + schema: + $ref: "#/definitions/Error" + + patch: + tags: + - User + description: Update a user's password or handle + parameters: + - in: formData + name: username + required: true + type: string + - in: formData + name: password + required: false + type: string + format: password + - in: formData + name: handle + required: false + type: string + format: email + produces: + - application/json + responses: + "200": + description: Updated user data + schema: + $ref: "#/definitions/User" + "404": + description: User does not exist + schema: + $ref: "#/definitions/Error" + + delete: + tags: + - User + description: Delete a user + parameters: + - in: formData + name: username + required: true + type: string + produces: + - application/json + responses: + "202": + description: Successfully deleted user + schema: + $ref: "#/definitions/Message" + "404": + description: User not found + schema: + $ref: "#/definitions/Error" + /v1/whitelist: get: tags: @@ -748,6 +850,21 @@ definitions: description: Character string used for authenticating with the api type: string + User: + type: object + properties: + username: + description: Username of the account + type: string + handle: + description: Fediverse handle associated with the account + type: string + format: email + created: + description: Date the account was created + type: string + format: date-time + Whitelist: type: object properties: diff --git a/relay/database/connection.py b/relay/database/connection.py index 6f77c31..4f9ff21 100644 --- a/relay/database/connection.py +++ b/relay/database/connection.py @@ -190,13 +190,28 @@ class Connection(SqlConnection): return cur.one() # type: ignore - def put_user(self, username: str, password: str, handle: str | None = None) -> Row: - data = { - 'username': username, - 'hash': self.hasher.hash(password), - 'handle': handle, - 'created': datetime.now(tz = timezone.utc) - } + def put_user(self, username: str, password: str | None, handle: str | None = None) -> Row: + if self.get_user(username): + data = { + 'username': username + } + + if password: + data['password'] = password + + if handle: + data['handler'] = handle + + else: + if password is None: + raise ValueError('Password cannot be empty') + + data = { + 'username': username, + 'hash': self.hasher.hash(password), + 'handle': handle, + 'created': datetime.now(tz = timezone.utc) + } with self.run('put-user', data) as cur: return cur.one() # type: ignore diff --git a/relay/views/api.py b/relay/views/api.py index ddd80e4..96f42fa 100644 --- a/relay/views/api.py +++ b/relay/views/api.py @@ -34,7 +34,11 @@ def check_api_path(method: str, path: str) -> bool: @web.middleware async def handle_api_path(request: Request, handler: Callable) -> Response: try: - request['token'] = request.headers['Authorization'].replace('Bearer', '').strip() + if (token := request.cookies.get('user-token')): + request['token'] = token + + else: + request['token'] = request.headers['Authorization'].replace('Bearer', '').strip() with get_app().database.session() as conn: request['user'] = conn.get_user_by_token(request['token']) @@ -384,6 +388,63 @@ class SoftwareBan(View): return Response.new({'message': 'Unbanned software'}, ctype = 'json') +@register_route('/api/v1/user') +class User(View): + async def get(self, request: Request) -> Response: + with self.database.session() as conn: + items = [] + + for row in conn.execute('SELECT * FROM users'): + del row['hash'] + items.append(row) + + return Response.new(items, ctype = 'json') + + + async def post(self, request: Request) -> Response: + data = await self.get_api_data(['username', 'password'], ['handle']) + + if isinstance(data, Response): + return data + + with self.database.session() as conn: + if conn.get_user(data['username']): + return Response.new_error(404, 'User already exists', 'json') + + user = conn.put_user(**data) + del user['hash'] + + return Response.new(user, ctype = 'json') + + + async def patch(self, request: Request) -> Response: + data = await self.get_api_data(['username'], ['password', ['handle']]) + + if isinstance(data, Response): + return data + + with self.database.session(True) as conn: + user = conn.put_user(**data) + del user['hash'] + + return Response.new(user, ctype = 'json') + + + async def delete(self, request: Request) -> Response: + data = await self.get_api_data(['username'], []) + + if isinstance(data, Response): + return data + + with self.database.session(True) as conn: + if not conn.get_user(data['username']): + return Response.new_error(404, 'User does not exist', 'json') + + conn.del_user(data['username']) + + return Response.new({'message': 'Deleted user'}, ctype = 'json') + + @register_route('/api/v1/whitelist') class Whitelist(View): async def get(self, request: Request) -> Response: diff --git a/relay/views/base.py b/relay/views/base.py index 2e035f6..9709e34 100644 --- a/relay/views/base.py +++ b/relay/views/base.py @@ -126,7 +126,7 @@ class View(AbstractView): return Response.new_error(400, 'Invalid JSON data', 'json') else: - post_data = convert_data(await self.request.query) # type: ignore + post_data = convert_data(self.request.query) # type: ignore data = {}