This commit is contained in:
Sofía Aritz 2024-06-30 18:13:29 +02:00
parent ed4ea1db40
commit b9f36b5c2c
Signed by: sofia
GPG key ID: 90B5116E3542B28F
12 changed files with 160 additions and 131 deletions

View file

@ -1,4 +1,8 @@
import globals from "globals"; import globals from "globals";
import pluginJs from "@eslint/js"; import pluginJs from "@eslint/js";
export default [{ languageOptions: { globals: globals.node } }, pluginJs.configs.recommended]; export default [
{ ignores: ["**/dist/*"] },
{ languageOptions: { globals: globals.node } },
pluginJs.configs.recommended,
];

View file

@ -19,8 +19,8 @@
}, },
"scripts": { "scripts": {
"start": "tsc && node dist/index.js", "start": "tsc && node dist/index.js",
"lint:fix": "eslint --fix && prettier . --write", "lint:fix": "eslint . --fix && prettier . --write",
"lint": "eslint && prettier . --check" "lint": "eslint . && prettier . --check"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.5.0", "@eslint/js": "^9.5.0",

View file

@ -19,7 +19,7 @@ import Fastify from "fastify";
let app = Fastify({ let app = Fastify({
logger: true, logger: true,
}).withTypeProvider<TypeBoxTypeProvider>() }).withTypeProvider<TypeBoxTypeProvider>();
export type AppInterface = typeof app; export type AppInterface = typeof app;
export default app; export default app;

View file

@ -20,36 +20,35 @@ import * as Jose from "jose";
import { JWT_ALG, JWT_SECRET } from "./consts.js"; import { JWT_ALG, JWT_SECRET } from "./consts.js";
import { DatabaseInterface, toDBList } from "./database.js"; import { DatabaseInterface, toDBList } from "./database.js";
export type AuthInterface = Awaited<ReturnType<typeof startAuth>>; export type AuthInterface = Awaited<ReturnType<typeof startAuth>>;
export type User = { export type User = {
id: string, id: string;
createdAt: number, createdAt: number;
lastConnected: number, lastConnected: number;
name: string, name: string;
email: string, email: string;
password: string, password: string;
limitID: string, limitID: string;
} };
export type NewUser = { export type NewUser = {
name: string, name: string;
email: string, email: string;
password: string, password: string;
} };
export type UpdateUser = { export type UpdateUser = {
name?: string, name?: string;
email?: string, email?: string;
password?: string, password?: string;
assets?: string[], assets?: string[];
} };
export async function startAuth(database: DatabaseInterface) { export async function startAuth(database: DatabaseInterface) {
let funcs = { let funcs = {
user: async (uid: string) => await database.user(uid) satisfies User, user: async (uid: string) => (await database.user(uid)) satisfies User,
findUserByEmail: async (email: string) => await database.findUserByEmail(email) satisfies User, findUserByEmail: async (email: string) => (await database.findUserByEmail(email)) satisfies User,
findUserBySessionKey: async (sessionKey: string) => { findUserBySessionKey: async (sessionKey: string) => {
let key = await database.sessionKey(sessionKey); let key = await database.sessionKey(sessionKey);
return await database.user(key.userID); return await database.user(key.userID);
@ -61,21 +60,24 @@ export async function startAuth(database: DatabaseInterface) {
return await database.updateUser(uid, user); return await database.updateUser(uid, user);
}, },
addUser: async (user: NewUser) => { addUser: async (user: NewUser) => {
let result = await database.insertUser({ let result = await database.insertUser(
id: randomUUID(), {
createdAt: Date.now(), id: randomUUID(),
lastConnected: Date.now(), createdAt: Date.now(),
name: user.name, lastConnected: Date.now(),
email: user.email, name: user.name,
password: await argon2.hash(user.password), email: user.email,
assets: toDBList([]), password: await argon2.hash(user.password),
// FIXME: This shouldn't be required, the DB interface overwrites it. assets: toDBList([]),
limitID: "", // FIXME: This shouldn't be required, the DB interface overwrites it.
}, { limitID: "",
id: randomUUID(), },
currentAssetCount: 0, {
maxAssetCount: 5, id: randomUUID(),
}); currentAssetCount: 0,
maxAssetCount: 5,
},
);
return result satisfies User; return result satisfies User;
}, },
@ -103,9 +105,9 @@ export async function startAuth(database: DatabaseInterface) {
}, },
verifyJwt: async (jwt) => { verifyJwt: async (jwt) => {
return await Jose.jwtVerify<{ return await Jose.jwtVerify<{
uid: string, uid: string;
email: string, email: string;
name: string, name: string;
}>(jwt, JWT_SECRET); }>(jwt, JWT_SECRET);
}, },
cleanUser: (user: User) => { cleanUser: (user: User) => {
@ -114,7 +116,7 @@ export async function startAuth(database: DatabaseInterface) {
clean.limitID = undefined; clean.limitID = undefined;
return clean; return clean;
} },
}; };
return funcs; return funcs;

View file

@ -21,14 +21,14 @@ import { sqliteTable } from "drizzle-orm/sqlite-core";
import { text, integer } from "drizzle-orm/sqlite-core"; import { text, integer } from "drizzle-orm/sqlite-core";
import { asc, desc, eq, sql } from "drizzle-orm"; import { asc, desc, eq, sql } from "drizzle-orm";
export type DatabaseInterface = Awaited<ReturnType<typeof startDatabase>> export type DatabaseInterface = Awaited<ReturnType<typeof startDatabase>>;
export function toDBList(input: any[]): string { export function toDBList(input: any[]): string {
return JSON.stringify(input); return JSON.stringify(input);
} }
export function fromDBList<T>(input: string): Array<T> { export function fromDBList<T>(input: string): Array<T> {
return JSON.parse(input) return JSON.parse(input);
} }
export async function startDatabase() { export async function startDatabase() {
@ -114,10 +114,10 @@ export async function startDatabase() {
return result[0].id; return result[0].id;
}, },
removeHeir: async (id: string) => { removeHeir: async (id: string) => {
await database.delete(heirs).where(eq(heirs.id, id)); await database.delete(heirs).where(eq(heirs.id, id));
}, },
listHeirs: async (userID: string) => { listHeirs: async (userID: string) => {
return await database.select().from(heirs).where(eq(heirs.userID, userID)); return await database.select().from(heirs).where(eq(heirs.userID, userID));
}, },
insertUser: async (user: typeof users.$inferInsert, limit: typeof limits.$inferInsert) => { insertUser: async (user: typeof users.$inferInsert, limit: typeof limits.$inferInsert) => {
let limitsResult = await database.insert(limits).values(limit).returning({ id: limits.id }); let limitsResult = await database.insert(limits).values(limit).returning({ id: limits.id });
@ -131,14 +131,17 @@ export async function startDatabase() {
return result[0]; return result[0];
}, },
userLimits: async (limitsID: string) => { userLimits: async (limitsID: string) => {
let result = await database.select().from(limits).where(eq(limits.id, limitsID)); let result = await database.select().from(limits).where(eq(limits.id, limitsID));
return result[0]; return result[0];
}, },
updateUser: async (userID: string, newUser: { updateUser: async (
name?: string, userID: string,
email?: string, newUser: {
password?: string, name?: string;
}) => { email?: string;
password?: string;
},
) => {
let result = await database.update(users).set(newUser).where(eq(users.id, userID)).returning(); let result = await database.update(users).set(newUser).where(eq(users.id, userID)).returning();
return result[0]; return result[0];
}, },
@ -157,7 +160,12 @@ export async function startDatabase() {
findSessionKeyByUserID: async (userID: string) => { findSessionKeyByUserID: async (userID: string) => {
return await database.select().from(session_keys).where(eq(session_keys.userID, userID)); return await database.select().from(session_keys).where(eq(session_keys.userID, userID));
}, },
insertEntry: async (entry: typeof entries.$inferInsert, musicEntry?: typeof musicEntries.$inferInsert, locationEntry?: typeof locationEntries.$inferInsert, dateEntry?: typeof dateEntries.$inferInsert) => { insertEntry: async (
entry: typeof entries.$inferInsert,
musicEntry?: typeof musicEntries.$inferInsert,
locationEntry?: typeof locationEntries.$inferInsert,
dateEntry?: typeof dateEntries.$inferInsert,
) => {
if (entry.kind === "album" || entry.kind === "song") { if (entry.kind === "album" || entry.kind === "song") {
let result = await database.insert(musicEntries).values(musicEntry).returning({ id: musicEntries.id }); let result = await database.insert(musicEntries).values(musicEntry).returning({ id: musicEntries.id });
entry.musicEntry = result[0].id; entry.musicEntry = result[0].id;
@ -176,7 +184,7 @@ export async function startDatabase() {
return result[0].id; return result[0].id;
}, },
removeEntry: async (entryID: string) => { removeEntry: async (entryID: string) => {
await database.delete(entries).where(eq(entries.id, entryID)); await database.delete(entries).where(eq(entries.id, entryID));
}, },
entryPage: async (userID: string, offset: number, limit: number) => { entryPage: async (userID: string, offset: number, limit: number) => {
let result = await database let result = await database
@ -188,46 +196,54 @@ export async function startDatabase() {
.orderBy(desc(entries.createdAt)); .orderBy(desc(entries.createdAt));
for (let key in result) { for (let key in result) {
if (!result.hasOwnProperty(key)) { continue; } if (!result.hasOwnProperty(key)) {
continue;
let entry = structuredClone(result[key]);
let base = {};
if (entry.musicEntry != null) {
let musicDetails = (await database.select().from(musicEntries).where(eq(musicEntries.id, entry.musicEntry)))[0];
(musicDetails["link"] as any) = fromDBList(musicDetails.links);
(musicDetails["id"] as any) = fromDBList(musicDetails.universalIDs);
musicDetails["links"] = undefined;
musicDetails["universalIDs"] = undefined;
base = musicDetails;
} else if (entry.locationEntry != null) {
let locationDetails = (await database.select().from(locationEntries).where(eq(locationEntries.id, entry.locationEntry)))[0];
if (locationDetails.locationCoordinates != null) {
base = { location: JSON.parse(locationDetails.locationCoordinates) }
} else if (locationDetails.locationText != null) {
base = { location: locationDetails.locationText }
} }
} else if (entry.dateEntry != null) {
let dateDetails = (await database.select().from(dateEntries).where(eq(dateEntries.id, entry.dateEntry)))[0];
base = { let entry = structuredClone(result[key]);
referencedDate: new Date(dateDetails["referencedDate"]).toISOString() let base = {};
}; if (entry.musicEntry != null) {
} let musicDetails = (
await database.select().from(musicEntries).where(eq(musicEntries.id, entry.musicEntry))
)[0];
(musicDetails["link"] as any) = fromDBList(musicDetails.links);
(musicDetails["id"] as any) = fromDBList(musicDetails.universalIDs);
base["kind"] = entry.kind musicDetails["links"] = undefined;
musicDetails["universalIDs"] = undefined;
base = musicDetails;
} else if (entry.locationEntry != null) {
let locationDetails = (
await database.select().from(locationEntries).where(eq(locationEntries.id, entry.locationEntry))
)[0];
result[key]["creationDate"] = new Date(entry.createdAt).toISOString(); if (locationDetails.locationCoordinates != null) {
(result[key] as any)["feelings"] = fromDBList(entry.feelings); base = { location: JSON.parse(locationDetails.locationCoordinates) };
(result[key] as any)["assets"] = fromDBList(entry.assets); } else if (locationDetails.locationText != null) {
result[key]["base"] = base; base = { location: locationDetails.locationText };
}
} else if (entry.dateEntry != null) {
let dateDetails = (
await database.select().from(dateEntries).where(eq(dateEntries.id, entry.dateEntry))
)[0];
result[key].kind = undefined; base = {
result[key].userID = undefined; referencedDate: new Date(dateDetails["referencedDate"]).toISOString(),
result[key].musicEntry = undefined; };
result[key].locationEntry = undefined; }
result[key].dateEntry = undefined;
base["kind"] = entry.kind;
result[key]["creationDate"] = new Date(entry.createdAt).toISOString();
(result[key] as any)["feelings"] = fromDBList(entry.feelings);
(result[key] as any)["assets"] = fromDBList(entry.assets);
result[key]["base"] = base;
result[key].kind = undefined;
result[key].userID = undefined;
result[key].musicEntry = undefined;
result[key].locationEntry = undefined;
result[key].dateEntry = undefined;
} }
return result; return result;

View file

@ -24,7 +24,7 @@ const Body = Type.Object({
contactMethod: Type.String(), contactMethod: Type.String(),
name: Type.String(), name: Type.String(),
value: Type.String(), value: Type.String(),
}) });
type BodyType = Static<typeof Body>; type BodyType = Static<typeof Body>;
@ -53,9 +53,9 @@ export default function register(app: AppInterface, auth: AuthInterface, databas
}); });
return (await database.listHeirs(payload.uid)) return (await database.listHeirs(payload.uid))
.map(v => v["contactMethod"] = "email") .map((v) => (v["contactMethod"] = "email"))
.map(v => v["value"] = v["email"]) .map((v) => (v["value"] = v["email"]))
.map(v => v["email"] = undefined); .map((v) => (v["email"] = undefined));
}, },
schema: { schema: {
headers: { $ref: "schema://identity/authorization" }, headers: { $ref: "schema://identity/authorization" },
@ -76,9 +76,9 @@ export default function register(app: AppInterface, auth: AuthInterface, databas
await database.removeHeir(request.body); await database.removeHeir(request.body);
return (await database.listHeirs(payload.uid)) return (await database.listHeirs(payload.uid))
.map(v => v["contactMethod"] = "email") .map((v) => (v["contactMethod"] = "email"))
.map(v => v["value"] = v["email"]) .map((v) => (v["value"] = v["email"]))
.map(v => v["email"] = undefined); .map((v) => (v["email"] = undefined));
}, },
schema: { schema: {
headers: { $ref: "schema://identity/authorization" }, headers: { $ref: "schema://identity/authorization" },

View file

@ -24,7 +24,7 @@ const Body = Type.Object({
password: Type.String(), password: Type.String(),
}); });
type BodyType = Static<typeof Body>; type BodyType = Static<typeof Body>;
export default function register(app: AppInterface, auth: AuthInterface) { export default function register(app: AppInterface, auth: AuthInterface) {
app.post<{ Body: BodyType }>("/auth/register", { app.post<{ Body: BodyType }>("/auth/register", {
@ -35,7 +35,7 @@ export default function register(app: AppInterface, auth: AuthInterface) {
password: request.body.password, password: request.body.password,
name: request.body.name, name: request.body.name,
}); });
return { token: await auth.createJwt(user.id) }; return { token: await auth.createJwt(user.id) };
} }

View file

@ -24,7 +24,7 @@ import { Static, Type } from "@sinclair/typebox";
const EntryIDQuery = Type.Object({ const EntryIDQuery = Type.Object({
entry_id: Type.String(), entry_id: Type.String(),
}) });
type EntryIDQueryType = Static<typeof EntryIDQuery>; type EntryIDQueryType = Static<typeof EntryIDQuery>;
@ -43,22 +43,23 @@ const PutEntryBody = Type.Object({
backgroundColor: Type.String(), backgroundColor: Type.String(),
textColor: Type.String(), textColor: Type.String(),
}), }),
]) ]),
), ),
base: Type.Union([ base: Type.Union([
Type.Object({ Type.Object({
kind: Type.String(), kind: Type.String(),
}), }),
Type.Object({ Type.Object({
kind: Type.String(), kind: Type.String(),
artist: Type.String(), artist: Type.String(),
title: Type.String(), title: Type.String(),
link: Type.Array(Type.String()), link: Type.Array(Type.String()),
id: Type.Array(Type.Object({ id: Type.Array(
provider: Type.String(), Type.Object({
id: Type.String(), provider: Type.String(),
})), id: Type.String(),
}),
),
}), }),
Type.Object({ Type.Object({
kind: Type.String(), kind: Type.String(),
@ -75,7 +76,7 @@ const PutEntryBody = Type.Object({
referencedDate: Type.String(), referencedDate: Type.String(),
}), }),
]), ]),
}) }),
}); });
type PutEntryBodyType = Static<typeof PutEntryBody>; type PutEntryBodyType = Static<typeof PutEntryBody>;
@ -105,37 +106,43 @@ export default function registerRoutes(app: AppInterface, auth: AuthInterface, d
let entry = request.body.entry; let entry = request.body.entry;
let musicEntry, locationEntry, dateEntry; let musicEntry, locationEntry, dateEntry;
if ((entry.base.kind === "album" || entry.base.kind === "song") && 'artist' in entry.base) { if ((entry.base.kind === "album" || entry.base.kind === "song") && "artist" in entry.base) {
musicEntry = { musicEntry = {
id: randomUUID(), id: randomUUID(),
title: entry.base.title, title: entry.base.title,
artist: entry.base.artist, artist: entry.base.artist,
links: toDBList(entry.base.link), links: toDBList(entry.base.link),
universalIDs: toDBList(entry.base.id), universalIDs: toDBList(entry.base.id),
} };
} else if (entry.base.kind === "environment" && 'location' in entry.base) { } else if (entry.base.kind === "environment" && "location" in entry.base) {
locationEntry = { locationEntry = {
id: randomUUID(), id: randomUUID(),
locationText: typeof entry.base.location === "string" ? entry.base.location : undefined, locationText: typeof entry.base.location === "string" ? entry.base.location : undefined,
locationCoordinates: typeof entry.base.location === "string" ? undefined : JSON.stringify(entry.base.location), locationCoordinates:
} typeof entry.base.location === "string" ? undefined : JSON.stringify(entry.base.location),
} else if (entry.base.kind === "date" && 'referencedDate' in entry.base) { };
} else if (entry.base.kind === "date" && "referencedDate" in entry.base) {
dateEntry = { dateEntry = {
id: randomUUID(), id: randomUUID(),
referencedDate: Date.parse(entry.base.referencedDate), referencedDate: Date.parse(entry.base.referencedDate),
} };
} }
await database.insertEntry({ await database.insertEntry(
id: randomUUID(), {
userID: payload.uid, id: randomUUID(),
feelings: toDBList(entry.feelings), userID: payload.uid,
assets: toDBList(entry.assets), feelings: toDBList(entry.feelings),
title: entry.title, assets: toDBList(entry.assets),
description: entry.description, title: entry.title,
kind: entry.base.kind, description: entry.description,
createdAt: Date.now(), kind: entry.base.kind,
}, musicEntry, locationEntry, dateEntry); createdAt: Date.now(),
},
musicEntry,
locationEntry,
dateEntry,
);
}, },
schema: { schema: {
headers: { $ref: "schema://identity/authorization" }, headers: { $ref: "schema://identity/authorization" },

View file

@ -22,7 +22,7 @@ import type { DatabaseInterface } from "../../database.js";
const Query = Type.Object({ const Query = Type.Object({
limit: Type.Number(), limit: Type.Number(),
offset: Type.Number(), offset: Type.Number(),
}) });
type QueryType = Static<typeof Query>; type QueryType = Static<typeof Query>;
@ -37,7 +37,7 @@ export default function register(app: AppInterface, auth: AuthInterface, databas
let jwt = request.headers["authorization"].replace("Bearer", "").trim(); let jwt = request.headers["authorization"].replace("Bearer", "").trim();
let { payload } = await auth.verifyJwt(jwt); let { payload } = await auth.verifyJwt(jwt);
return await database.entryPage(payload.uid, request.query.offset, request.query.limit) return await database.entryPage(payload.uid, request.query.offset, request.query.limit);
}, },
schema: { schema: {
headers: { $ref: "schema://identity/authorization" }, headers: { $ref: "schema://identity/authorization" },

View file

@ -29,7 +29,7 @@ export default function register(app: AppInterface, auth: AuthInterface, databas
} }
let body = JSON.parse(contentFromSigned(request.body)); let body = JSON.parse(contentFromSigned(request.body));
let user = await auth.findUserBySessionKey(body.session_key); let user = await auth.findUserBySessionKey(body.session_key);
let limits = await database.userLimits(user.limitID); let limits = await database.userLimits(user.limitID);
(user.assets as any) = fromDBList(user.assets); (user.assets as any) = fromDBList(user.assets);

View file

@ -33,7 +33,7 @@ export default function register(app: AppInterface, auth: AuthInterface) {
let user = await auth.findUserBySessionKey(body.session_key); let user = await auth.findUserBySessionKey(body.session_key);
let assets = fromDBList(user.assets) as string[]; let assets = fromDBList(user.assets) as string[];
assets.push(body.asset_id); assets.push(body.asset_id);
await auth.updateUser(user.id, { await auth.updateUser(user.id, {
assets, assets,
}); });

View file

@ -10,4 +10,4 @@
}, },
"include": ["./src/**/*"], "include": ["./src/**/*"],
"exclude": ["./node_modules/**/*"] "exclude": ["./node_modules/**/*"]
} }