remove cookie auth from backend, add initial types to frontend
This commit is contained in:
parent
c6eaf49779
commit
aeb044d1a0
12 changed files with 101 additions and 21 deletions
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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]
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
5
foxnouns/db/redis.py
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
from redis import asyncio as aioredis
|
||||||
|
|
||||||
|
from foxnouns.settings import REDIS_URL
|
||||||
|
|
||||||
|
redis = aioredis.from_url(REDIS_URL)
|
|
@ -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")
|
||||||
|
|
32
frontend/src/lib/entities.ts
Normal file
32
frontend/src/lib/entities.ts
Normal 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;
|
||||||
|
};
|
|
@ -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");
|
||||||
|
|
|
@ -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) };
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
2
poetry.lock
generated
|
@ -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"
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue