From aeb044d1a0037ed03e6668f9da6cd55719337228 Mon Sep 17 00:00:00 2001 From: sam Date: Fri, 5 Apr 2024 17:35:08 +0200 Subject: [PATCH] remove cookie auth from backend, add initial types to frontend --- foxnouns/app.py | 13 ++++------ foxnouns/blueprints/__init__.py | 3 ++- foxnouns/blueprints/v2/auth/__init__.py | 19 +++++++++++++++ foxnouns/blueprints/v2/auth/discord.py | 2 +- foxnouns/db/redis.py | 5 ++++ foxnouns/settings.py | 4 +++- frontend/src/lib/entities.ts | 32 +++++++++++++++++++++++++ frontend/src/lib/request.ts | 28 ++++++++++++++++++---- frontend/src/routes/+layout.server.ts | 9 +++---- frontend/src/routes/+layout.svelte | 4 ++++ poetry.lock | 2 +- pyproject.toml | 1 + 12 files changed, 101 insertions(+), 21 deletions(-) create mode 100644 foxnouns/db/redis.py create mode 100644 frontend/src/lib/entities.ts diff --git a/foxnouns/app.py b/foxnouns/app.py index ef3fb2e..4bd6cf6 100644 --- a/foxnouns/app.py +++ b/foxnouns/app.py @@ -19,11 +19,7 @@ app = cors( ) QuartSchema(app) -for bp in ( - blueprints.users_blueprint, - blueprints.members_blueprint, - blueprints.meta_blueprint, -): +for bp in blueprints.__all__: app.register_blueprint(bp) @@ -50,11 +46,10 @@ async def handle_500(_): @app.before_request async def get_user_from_token(): - """Get the current user from a token given in the `Authorization` header or the `pronounscc-token` cookie. + """Get the current user from a token given in the `Authorization` header. If no token is set, does nothing; if an invalid token is set, raises an error.""" - token = request.headers.get("Authorization", None) or request.cookies.get( - "pronounscc-token", None - ) + + token = request.headers.get("Authorization", None) if not token: return diff --git a/foxnouns/blueprints/__init__.py b/foxnouns/blueprints/__init__.py index aa1ec7d..492e8af 100644 --- a/foxnouns/blueprints/__init__.py +++ b/foxnouns/blueprints/__init__.py @@ -1,5 +1,6 @@ from .v2.members import bp as members_blueprint from .v2.meta import bp as meta_blueprint from .v2.users import bp as users_blueprint +from .v2.auth.discord import bp as discord_auth_blueprint -__all__ = [users_blueprint, members_blueprint, meta_blueprint] +__all__ = [users_blueprint, members_blueprint, meta_blueprint, discord_auth_blueprint] diff --git a/foxnouns/blueprints/v2/auth/__init__.py b/foxnouns/blueprints/v2/auth/__init__.py index 8d5c920..6461084 100644 --- a/foxnouns/blueprints/v2/auth/__init__.py +++ b/foxnouns/blueprints/v2/auth/__init__.py @@ -1,10 +1,29 @@ from datetime import datetime from pydantic import BaseModel, Field +from quart import Blueprint +from quart_schema import validate_response +from foxnouns.settings import BASE_DOMAIN from foxnouns.models.user import SelfUserModel +bp = Blueprint("auth_v2", __name__) + + +class URLsResponse(BaseModel): + discord: str | None = Field(default=None) + google: str | None = Field(default=None) + tumblr: str | None = Field(default=None) + + +@bp.post("/api/v2/auth/urls", host=BASE_DOMAIN) +@validate_response(URLsResponse, 200) +async def urls(): + # TODO: build authorization URLs + callback URLs, store state in Redis + raise NotImplementedError() + + class OAuthCallbackRequest(BaseModel): callback_domain: str code: str diff --git a/foxnouns/blueprints/v2/auth/discord.py b/foxnouns/blueprints/v2/auth/discord.py index ff2774f..5f873b7 100644 --- a/foxnouns/blueprints/v2/auth/discord.py +++ b/foxnouns/blueprints/v2/auth/discord.py @@ -5,7 +5,7 @@ from foxnouns.settings import BASE_DOMAIN from . import BaseCallbackResponse, OAuthCallbackRequest -bp = Blueprint("discord", __name__) +bp = Blueprint("discord_v2", __name__) @bp.post("/api/v2/auth/discord/callback", host=BASE_DOMAIN) diff --git a/foxnouns/db/redis.py b/foxnouns/db/redis.py new file mode 100644 index 0000000..4791790 --- /dev/null +++ b/foxnouns/db/redis.py @@ -0,0 +1,5 @@ +from redis import asyncio as aioredis + +from foxnouns.settings import REDIS_URL + +redis = aioredis.from_url(REDIS_URL) diff --git a/foxnouns/settings.py b/foxnouns/settings.py index a5e352a..480243d 100644 --- a/foxnouns/settings.py +++ b/foxnouns/settings.py @@ -15,7 +15,7 @@ with env.prefixed("DATABASE_"): } # The Redis database used for Celery and ephemeral storage. -REDIS_URL: str = env("REDIS_URL", "redis://localhost") +REDIS_URL = env("REDIS_URL", "redis://localhost") with env.prefixed("MINIO_"): MINIO = { @@ -31,6 +31,8 @@ with env.prefixed("MINIO_"): BASE_DOMAIN = env("BASE_DOMAIN") # The base domain for short URLs. SHORT_DOMAIN = env("SHORT_DOMAIN", "prns.localhost") +# The base URL used for the frontend. This will usually be the same as BASE_DOMAIN prefixed with https://. +FRONTEND_BASE = env("FRONTEND_DOMAIN", f"https://{BASE_DOMAIN}") # Secret key for signing tokens, generate with (for example) `openssl rand -base64 32` SECRET_KEY = env("SECRET_KEY") diff --git a/frontend/src/lib/entities.ts b/frontend/src/lib/entities.ts new file mode 100644 index 0000000..8f27fb9 --- /dev/null +++ b/frontend/src/lib/entities.ts @@ -0,0 +1,32 @@ +export type User = { + id: string; + name: string; + display_name: string | null; + bio: string | null; + avatar: string | null; + + names: FieldEntry[]; + pronouns: PronounEntry[]; + fields: ProfileField[]; +}; + +export type FieldEntry = { + value: string; + status: string; +}; + +export type ProfileField = { + name: string; + entries: FieldEntry[]; +}; + +export type PronounEntry = { + value: string; + status: string; + display: string | null; +}; + +export type Meta = { + users: number; + members: number; +}; diff --git a/frontend/src/lib/request.ts b/frontend/src/lib/request.ts index 1419e43..6be2e70 100644 --- a/frontend/src/lib/request.ts +++ b/frontend/src/lib/request.ts @@ -1,5 +1,8 @@ +import type { Cookies, ServerLoadEvent } from "@sveltejs/kit"; + export type FetchOptions = { fetchFn?: typeof fetch; + token?: string; // eslint-disable-next-line @typescript-eslint/no-explicit-any data?: any; version?: number; @@ -22,13 +25,17 @@ export default async function request( path: string, opts: FetchOptions = {}, ): Promise { - const { data, version, extraHeaders } = opts; + const { token, data, version, extraHeaders } = opts; const fetchFn = opts.fetchFn ?? fetch; const resp = await fetchFn(`/api/v${version ?? 2}${path}`, { method, body: data ? JSON.stringify(data) : undefined, - headers: { ...extraHeaders, "Content-Type": "application/json" }, + headers: { + ...extraHeaders, + ...(token ? { Authorization: token } : {}), + "Content-Type": "application/json", + }, }); if (resp.status < 200 || resp.status >= 400) throw await resp.json(); @@ -50,14 +57,27 @@ export async function fastRequest( path: string, opts: FetchOptions = {}, ): Promise { - const { data, version, extraHeaders } = opts; + const { token, data, version, extraHeaders } = opts; const fetchFn = opts.fetchFn ?? fetch; const resp = await fetchFn(`/api/v2${version ?? 2}${path}`, { method, body: data ? JSON.stringify(data) : undefined, - headers: { ...extraHeaders, "Content-Type": "application/json" }, + headers: { + ...extraHeaders, + ...(token ? { Authorization: token } : {}), + "Content-Type": "application/json", + }, }); if (resp.status < 200 || resp.status >= 400) throw await resp.json(); } + +/** + * Helper function to get a token from a request cookie. + * Accepts both a cookie object ({ cookies }) or a request object (req). + * @param s A Cookies or ServerLoadEvent object + * @returns A token, or `undefined` if no token is set. + */ +export const getToken = (s: Cookies | ServerLoadEvent) => + "cookies" in s ? s.cookies.get("pronounscc-token") : s.get("pronounscc-token"); diff --git a/frontend/src/routes/+layout.server.ts b/frontend/src/routes/+layout.server.ts index 86566b7..db43f67 100644 --- a/frontend/src/routes/+layout.server.ts +++ b/frontend/src/routes/+layout.server.ts @@ -1,12 +1,13 @@ -import request from "$lib/request"; +import request, { getToken } from "$lib/request"; +import type { User, Meta } from "$lib/entities"; export async function load({ fetch, cookies }) { - const meta = await request("GET", "/meta", { fetchFn: fetch }); + const meta = await request("GET", "/meta", { fetchFn: fetch }); let user; if (cookies.get("pronounscc-token")) { - user = await request("GET", "/users/@me", { fetchFn: fetch }); + user = await request("GET", "/users/@me", { fetchFn: fetch, token: getToken(cookies) }); } - return { meta, user }; + return { meta, user, token: getToken(cookies) }; } diff --git a/frontend/src/routes/+layout.svelte b/frontend/src/routes/+layout.svelte index ede5c47..54e3c4a 100644 --- a/frontend/src/routes/+layout.svelte +++ b/frontend/src/routes/+layout.svelte @@ -8,4 +8,8 @@ {JSON.stringify(data.meta)} +{#if data.user} + {JSON.stringify(data.user)} +{/if} + diff --git a/poetry.lock b/poetry.lock index 083c8b3..8c56861 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1513,4 +1513,4 @@ h11 = ">=0.9.0,<1" [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "0f97996ff854041ec5bc98541863488c29ae65f44652721af67f3e06875c6bbb" +content-hash = "3013b1d9175f34895133e708e35dea7d3401a07b5869c857e30d0aac41ce7add" diff --git a/pyproject.toml b/pyproject.toml index 336df98..7a9e749 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,6 +25,7 @@ alembic = "^1.13.1" quart-cors = "^0.7.0" minio = "^7.2.5" pyvips = "^2.2.2" +redis = "^5.0.3" [tool.poetry.group.dev] optional = true