remove cookie auth from backend, add initial types to frontend

This commit is contained in:
sam 2024-04-05 17:35:08 +02:00
parent c6eaf49779
commit aeb044d1a0
Signed by: sam
GPG key ID: B4EF20DDE721CAA1
12 changed files with 101 additions and 21 deletions

View file

@ -19,11 +19,7 @@ app = cors(
) )
QuartSchema(app) QuartSchema(app)
for bp in ( for bp in blueprints.__all__:
blueprints.users_blueprint,
blueprints.members_blueprint,
blueprints.meta_blueprint,
):
app.register_blueprint(bp) app.register_blueprint(bp)
@ -50,11 +46,10 @@ async def handle_500(_):
@app.before_request @app.before_request
async def get_user_from_token(): 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.""" 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: if not token:
return return

View file

@ -1,5 +1,6 @@
from .v2.members import bp as members_blueprint from .v2.members import bp as members_blueprint
from .v2.meta import bp as meta_blueprint from .v2.meta import bp as meta_blueprint
from .v2.users import bp as users_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]

View file

@ -1,10 +1,29 @@
from datetime import datetime from datetime import datetime
from pydantic import BaseModel, Field 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 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): class OAuthCallbackRequest(BaseModel):
callback_domain: str callback_domain: str
code: str code: str

View file

@ -5,7 +5,7 @@ from foxnouns.settings import BASE_DOMAIN
from . import BaseCallbackResponse, OAuthCallbackRequest from . import BaseCallbackResponse, OAuthCallbackRequest
bp = Blueprint("discord", __name__) bp = Blueprint("discord_v2", __name__)
@bp.post("/api/v2/auth/discord/callback", host=BASE_DOMAIN) @bp.post("/api/v2/auth/discord/callback", host=BASE_DOMAIN)

5
foxnouns/db/redis.py Normal file
View file

@ -0,0 +1,5 @@
from redis import asyncio as aioredis
from foxnouns.settings import REDIS_URL
redis = aioredis.from_url(REDIS_URL)

View file

@ -15,7 +15,7 @@ with env.prefixed("DATABASE_"):
} }
# The Redis database used for Celery and ephemeral storage. # 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_"): with env.prefixed("MINIO_"):
MINIO = { MINIO = {
@ -31,6 +31,8 @@ with env.prefixed("MINIO_"):
BASE_DOMAIN = env("BASE_DOMAIN") BASE_DOMAIN = env("BASE_DOMAIN")
# The base domain for short URLs. # The base domain for short URLs.
SHORT_DOMAIN = env("SHORT_DOMAIN", "prns.localhost") 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 for signing tokens, generate with (for example) `openssl rand -base64 32`
SECRET_KEY = env("SECRET_KEY") SECRET_KEY = env("SECRET_KEY")

View file

@ -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;
};

View file

@ -1,5 +1,8 @@
import type { Cookies, ServerLoadEvent } from "@sveltejs/kit";
export type FetchOptions = { export type FetchOptions = {
fetchFn?: typeof fetch; fetchFn?: typeof fetch;
token?: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
data?: any; data?: any;
version?: number; version?: number;
@ -22,13 +25,17 @@ export default async function request<T>(
path: string, path: string,
opts: FetchOptions = {}, opts: FetchOptions = {},
): Promise<T> { ): Promise<T> {
const { data, version, extraHeaders } = opts; const { token, data, version, extraHeaders } = opts;
const fetchFn = opts.fetchFn ?? fetch; const fetchFn = opts.fetchFn ?? fetch;
const resp = await fetchFn(`/api/v${version ?? 2}${path}`, { const resp = await fetchFn(`/api/v${version ?? 2}${path}`, {
method, method,
body: data ? JSON.stringify(data) : undefined, 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(); if (resp.status < 200 || resp.status >= 400) throw await resp.json();
@ -50,14 +57,27 @@ export async function fastRequest(
path: string, path: string,
opts: FetchOptions = {}, opts: FetchOptions = {},
): Promise<void> { ): Promise<void> {
const { data, version, extraHeaders } = opts; const { token, data, version, extraHeaders } = opts;
const fetchFn = opts.fetchFn ?? fetch; const fetchFn = opts.fetchFn ?? fetch;
const resp = await fetchFn(`/api/v2${version ?? 2}${path}`, { const resp = await fetchFn(`/api/v2${version ?? 2}${path}`, {
method, method,
body: data ? JSON.stringify(data) : undefined, 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(); 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");

View file

@ -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 }) { export async function load({ fetch, cookies }) {
const meta = await request("GET", "/meta", { fetchFn: fetch }); const meta = await request<Meta>("GET", "/meta", { fetchFn: fetch });
let user; let user;
if (cookies.get("pronounscc-token")) { if (cookies.get("pronounscc-token")) {
user = await request("GET", "/users/@me", { fetchFn: fetch }); user = await request<User>("GET", "/users/@me", { fetchFn: fetch, token: getToken(cookies) });
} }
return { meta, user }; return { meta, user, token: getToken(cookies) };
} }

View file

@ -8,4 +8,8 @@
{JSON.stringify(data.meta)} {JSON.stringify(data.meta)}
{#if data.user}
{JSON.stringify(data.user)}
{/if}
<slot /> <slot />

2
poetry.lock generated
View file

@ -1513,4 +1513,4 @@ h11 = ">=0.9.0,<1"
[metadata] [metadata]
lock-version = "2.0" lock-version = "2.0"
python-versions = "^3.11" python-versions = "^3.11"
content-hash = "0f97996ff854041ec5bc98541863488c29ae65f44652721af67f3e06875c6bbb" content-hash = "3013b1d9175f34895133e708e35dea7d3401a07b5869c857e30d0aac41ce7add"

View file

@ -25,6 +25,7 @@ alembic = "^1.13.1"
quart-cors = "^0.7.0" quart-cors = "^0.7.0"
minio = "^7.2.5" minio = "^7.2.5"
pyvips = "^2.2.2" pyvips = "^2.2.2"
redis = "^5.0.3"
[tool.poetry.group.dev] [tool.poetry.group.dev]
optional = true optional = true