add user management api endpoints and allow cookie for api auth

This commit is contained in:
Izalia Mae 2024-03-15 06:46:31 -04:00
parent 08f4f0e72d
commit 1ffc609058
4 changed files with 202 additions and 9 deletions

View file

@ -13,6 +13,10 @@ schemes:
- https - https
securityDefinitions: securityDefinitions:
Cookie:
type: apiKey
in: cookie
name: user-token
Bearer: Bearer:
type: apiKey type: apiKey
name: Authorization name: Authorization
@ -549,6 +553,104 @@ paths:
schema: schema:
$ref: "#/definitions/Error" $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: /v1/whitelist:
get: get:
tags: tags:
@ -748,6 +850,21 @@ definitions:
description: Character string used for authenticating with the api description: Character string used for authenticating with the api
type: string 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: Whitelist:
type: object type: object
properties: properties:

View file

@ -190,13 +190,28 @@ class Connection(SqlConnection):
return cur.one() # type: ignore return cur.one() # type: ignore
def put_user(self, username: str, password: str, handle: str | None = None) -> Row: def put_user(self, username: str, password: str | None, handle: str | None = None) -> Row:
data = { if self.get_user(username):
'username': username, data = {
'hash': self.hasher.hash(password), 'username': username
'handle': handle, }
'created': datetime.now(tz = timezone.utc)
} 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: with self.run('put-user', data) as cur:
return cur.one() # type: ignore return cur.one() # type: ignore

View file

@ -34,7 +34,11 @@ def check_api_path(method: str, path: str) -> bool:
@web.middleware @web.middleware
async def handle_api_path(request: Request, handler: Callable) -> Response: async def handle_api_path(request: Request, handler: Callable) -> Response:
try: 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: with get_app().database.session() as conn:
request['user'] = conn.get_user_by_token(request['token']) request['user'] = conn.get_user_by_token(request['token'])
@ -384,6 +388,63 @@ class SoftwareBan(View):
return Response.new({'message': 'Unbanned software'}, ctype = 'json') 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') @register_route('/api/v1/whitelist')
class Whitelist(View): class Whitelist(View):
async def get(self, request: Request) -> Response: async def get(self, request: Request) -> Response:

View file

@ -126,7 +126,7 @@ class View(AbstractView):
return Response.new_error(400, 'Invalid JSON data', 'json') return Response.new_error(400, 'Invalid JSON data', 'json')
else: else:
post_data = convert_data(await self.request.query) # type: ignore post_data = convert_data(self.request.query) # type: ignore
data = {} data = {}