feature: signup and login work
This commit is contained in:
parent
a4a750b35c
commit
3d42591ee5
18 changed files with 956 additions and 65 deletions
121
utils/api.ts
Normal file
121
utils/api.ts
Normal file
|
@ -0,0 +1,121 @@
|
|||
import { getSetCookies } from '@std/http/cookie';
|
||||
import { generateTotp } from '@stdext/crypto/totp';
|
||||
|
||||
export interface API_CLIENT {
|
||||
fetch: (url: string, options?: FETCH_OPTIONS, transform?: (obj: any) => any) => Promise<object>;
|
||||
}
|
||||
|
||||
export type API_CONFIG = {
|
||||
protocol: string;
|
||||
hostname: string;
|
||||
port: number;
|
||||
prefix: string;
|
||||
};
|
||||
|
||||
const DEFAULT_API_CONFIG: API_CONFIG = {
|
||||
protocol: 'http:',
|
||||
hostname: 'localhost',
|
||||
port: 80,
|
||||
prefix: ''
|
||||
};
|
||||
|
||||
export interface RETRY_OPTIONS {
|
||||
limit: number;
|
||||
methods: string[];
|
||||
status_codes: number[];
|
||||
}
|
||||
|
||||
export interface FETCH_OPTIONS extends RequestInit {
|
||||
retry?: RETRY_OPTIONS;
|
||||
json?: Record<string, any>;
|
||||
done?: (response: Response) => void;
|
||||
session?: Record<string, any>;
|
||||
totp_token?: string;
|
||||
}
|
||||
|
||||
const DEFAULT_TRANSFORM = (response_json: any) => {
|
||||
return response_json;
|
||||
};
|
||||
|
||||
export function api(api_config?: Record<string, any>): API_CLIENT {
|
||||
const config: API_CONFIG = {
|
||||
...DEFAULT_API_CONFIG,
|
||||
...(api_config ?? {})
|
||||
};
|
||||
|
||||
return {
|
||||
fetch: async (url: string, options?: FETCH_OPTIONS, transform?: (obj: any) => any) => {
|
||||
const prefix: string = `${config.protocol}//${config.hostname}:${config.port}${config.prefix}`;
|
||||
const retry: RETRY_OPTIONS = options?.retry ?? {
|
||||
limit: 0,
|
||||
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS'],
|
||||
status_codes: [500, 502, 503, 504, 521, 522, 524]
|
||||
};
|
||||
|
||||
const content_type = options?.json ? 'application/json' : 'text/plain';
|
||||
const headers = new Headers(options?.headers ?? {});
|
||||
headers.append('accept', 'application/json');
|
||||
if (options?.json || options?.body) {
|
||||
headers.append('content-type', content_type);
|
||||
}
|
||||
if (options?.session) {
|
||||
const cookies = getSetCookies(headers);
|
||||
|
||||
cookies.push({
|
||||
name: options.totp_token ?? 'totp',
|
||||
value: await generateTotp(options.session.secret),
|
||||
maxAge: 30,
|
||||
expires: Date.now() + 30_000,
|
||||
path: '/'
|
||||
});
|
||||
|
||||
for (const cookie of cookies) {
|
||||
headers.append(`x-${cookie.name}`, cookie.value);
|
||||
}
|
||||
headers.append('cookie', cookies.map((cookie) => `${cookie.name}=${cookie.value}`).join('; '));
|
||||
}
|
||||
|
||||
const request_options: RequestInit = {
|
||||
body: options?.json ? JSON.stringify(options.json, null, 2) : options?.body,
|
||||
method: options?.method ?? 'GET',
|
||||
credentials: options?.credentials ?? 'include',
|
||||
redirect: options?.redirect ?? 'follow',
|
||||
headers
|
||||
};
|
||||
|
||||
const response_transform = transform ?? DEFAULT_TRANSFORM;
|
||||
|
||||
let retries = 0;
|
||||
let delay = 1000;
|
||||
const resolved_url = `${prefix}${url}`;
|
||||
do {
|
||||
const response: Response = await fetch(resolved_url, request_options);
|
||||
if (
|
||||
retries < retry.limit && retry.status_codes.includes(response.status) &&
|
||||
retry.methods.includes(request_options.method ?? 'GET')
|
||||
) {
|
||||
++retries;
|
||||
await new Promise((resolve) => setTimeout(resolve, delay));
|
||||
delay *= 2;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (response.status > 400) {
|
||||
const error_response = await response.json();
|
||||
throw new Error('Bad Request', {
|
||||
cause: error_response?.cause ?? JSON.stringify(error_response)
|
||||
});
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const transformed = await response_transform(data);
|
||||
|
||||
if (options?.done) {
|
||||
options.done(response);
|
||||
}
|
||||
|
||||
return transformed;
|
||||
} while (retries < retry.limit);
|
||||
}
|
||||
};
|
||||
}
|
13
utils/canned_responses.ts
Normal file
13
utils/canned_responses.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
export const CANNED_RESPONSES: Record<string, () => Response> = {
|
||||
permission_denied: (): Response => {
|
||||
console.trace('denied');
|
||||
return Response.json({
|
||||
error: {
|
||||
message: 'Permission denied.',
|
||||
cause: 'permission_denied'
|
||||
}
|
||||
}, {
|
||||
status: 400
|
||||
});
|
||||
}
|
||||
};
|
43
utils/prechecks.ts
Normal file
43
utils/prechecks.ts
Normal file
|
@ -0,0 +1,43 @@
|
|||
import { getCookies } from 'jsr:@std/http/cookie';
|
||||
import { SESSIONS } from '../models/session.ts';
|
||||
import { verifyTotp } from 'jsr:@stdext/crypto/totp';
|
||||
import { USERS } from '../models/user.ts';
|
||||
import { PERMISSIONS_STORE } from '../models/user_permissions.ts';
|
||||
import { CANNED_RESPONSES } from './canned_responses.ts';
|
||||
|
||||
export type PRECHECK = (req: Request, meta: Record<string, any>) => Promise<Response | undefined> | Response | undefined;
|
||||
export type PRECHECK_TABLE = Record<string, PRECHECK[]>;
|
||||
|
||||
export const SESSION_ID_TOKEN: string = Deno.env.get('SESSION_ID_TOKEN') ?? 'session_id';
|
||||
export const SESSION_SECRET_TOKEN: string = Deno.env.get('SESSION_SECRET_TOKEN') ?? 'session_secret';
|
||||
export const TOTP_TOKEN: string = Deno.env.get('TOTP_TOKEN') ?? 'totp';
|
||||
|
||||
export async function get_session(request: Request, meta: Record<string, any>): Promise<undefined> {
|
||||
meta.now = meta.now ?? Date.now();
|
||||
meta.cookies = meta.cookies ?? getCookies(request.headers);
|
||||
meta.session_id = request.headers.get(`x-${SESSION_ID_TOKEN}`) ?? meta.cookies[SESSION_ID_TOKEN] ?? '';
|
||||
meta.session = meta.session_id?.length ? await SESSIONS.get(meta.session_id) : null;
|
||||
meta.valid_session = !!meta.session && meta.now < new Date(meta.session.timestamps.expires).valueOf();
|
||||
|
||||
meta.request_totp = request.headers.get(`x-${TOTP_TOKEN}`) ?? meta.cookies[TOTP_TOKEN] ?? '';
|
||||
meta.valid_totp = meta.valid_session && meta.session && meta.request_totp
|
||||
? await verifyTotp(meta.request_totp, meta.session.secret)
|
||||
: false;
|
||||
}
|
||||
|
||||
export async function get_user(request: Request, meta: Record<string, any>): Promise<undefined> {
|
||||
meta.now = meta.now ?? Date.now();
|
||||
meta.cookies = meta.cookies ?? getCookies(request.headers);
|
||||
|
||||
meta.user = meta.valid_totp && meta.session ? await USERS.get(meta.session.user_id) : null;
|
||||
meta.user_permissions = meta.valid_totp && meta.session ? await PERMISSIONS_STORE.get(meta.session.user_id) : null;
|
||||
}
|
||||
|
||||
export function require_user(
|
||||
_request: Request,
|
||||
meta: Record<string, any>
|
||||
): undefined | Response {
|
||||
if (!meta.user) {
|
||||
return CANNED_RESPONSES.permission_denied();
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue