typescript + sqlite + more things
This commit is contained in:
parent
3e78a67b04
commit
0e5b9aa324
33 changed files with 2123 additions and 318 deletions
|
@ -60,8 +60,8 @@ app.get("/crypto/algo", () => {
|
||||||
|
|
||||||
app.put("/asset", {
|
app.put("/asset", {
|
||||||
async handler(request, reply) {
|
async handler(request, reply) {
|
||||||
let user = await userFromSessionKey(request.query.session_key);
|
let { user, limits } = await userFromSessionKey(request.query.session_key);
|
||||||
if (user.assets.length >= user.limits.assetCount) {
|
if (user.assets.length >= limits.maxAssetCount) {
|
||||||
reply.code(403);
|
reply.code(403);
|
||||||
return "Max asset count reached. Contact support or upgrade your plan";
|
return "Max asset count reached. Contact support or upgrade your plan";
|
||||||
}
|
}
|
||||||
|
@ -102,7 +102,7 @@ app.put("/asset", {
|
||||||
|
|
||||||
app.get("/asset", {
|
app.get("/asset", {
|
||||||
async handler(request, reply) {
|
async handler(request, reply) {
|
||||||
let user = await userFromSessionKey(request.query.session_key);
|
let { user, limits } = await userFromSessionKey(request.query.session_key);
|
||||||
|
|
||||||
if ('statusCode' in user) {
|
if ('statusCode' in user) {
|
||||||
reply.code(500);
|
reply.code(500);
|
||||||
|
|
|
@ -3,3 +3,4 @@ IDENTITY_API_JWT_SECRET = "cc7e0d44fd473002f1c42167459001140ec6389b7353f8088f4d9
|
||||||
IDENTITY_API_JWT_ALG = "HS256"
|
IDENTITY_API_JWT_ALG = "HS256"
|
||||||
IDENTITY_API_ASSET_API_ENDPOINT = "http://localhost:3001"
|
IDENTITY_API_ASSET_API_ENDPOINT = "http://localhost:3001"
|
||||||
IDENTITY_API_ASSET_API_M2M_REFRESH_INTERVAL_MS = 60000
|
IDENTITY_API_ASSET_API_M2M_REFRESH_INTERVAL_MS = 60000
|
||||||
|
IDENTITY_SQLITE_PATH = ".database/identity.sqlite"
|
2
identity-api/.gitignore
vendored
2
identity-api/.gitignore
vendored
|
@ -1,3 +1,5 @@
|
||||||
node_modules/
|
node_modules/
|
||||||
|
dist/
|
||||||
.yarn
|
.yarn
|
||||||
.env
|
.env
|
||||||
|
.database
|
76
identity-api/docs/database/schema.dbml
Normal file
76
identity-api/docs/database/schema.dbml
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
// You can render this DBML file using specialized software or websites such as:
|
||||||
|
// https://dbdiagram.io/
|
||||||
|
|
||||||
|
Table limits {
|
||||||
|
id varchar [primary key]
|
||||||
|
current_asset_count integer [not null]
|
||||||
|
max_asset_count integer [not null]
|
||||||
|
}
|
||||||
|
|
||||||
|
Table users {
|
||||||
|
id varchar [primary key]
|
||||||
|
created_at timestamp [not null]
|
||||||
|
last_connected_at timestamp [not null]
|
||||||
|
email varchar [not null]
|
||||||
|
password varchar [not null]
|
||||||
|
name varchar [not null]
|
||||||
|
limits varchar [not null]
|
||||||
|
assets varchar [not null, note: 'Comma separated list']
|
||||||
|
}
|
||||||
|
|
||||||
|
Table session_keys {
|
||||||
|
key varchar [primary key]
|
||||||
|
user_id varchar [not null]
|
||||||
|
}
|
||||||
|
|
||||||
|
Table heirs {
|
||||||
|
id varchar [primary key]
|
||||||
|
user_id varchar [not null]
|
||||||
|
created_at timestamp [not null]
|
||||||
|
name varchar [not null]
|
||||||
|
email varchar
|
||||||
|
}
|
||||||
|
|
||||||
|
Table entries {
|
||||||
|
id varchar [primary key]
|
||||||
|
user_id varchar [not null]
|
||||||
|
created_at timestamp [not null]
|
||||||
|
feelings text [not null, note: 'Comma separated JSON-encoded list']
|
||||||
|
assets text [not null, note: 'Comma separated list']
|
||||||
|
title text
|
||||||
|
description text
|
||||||
|
|
||||||
|
kind varchar [not null, note: 'Kind of entry']
|
||||||
|
music_entry varchar
|
||||||
|
location_entry varchar
|
||||||
|
date_entry varchar
|
||||||
|
}
|
||||||
|
|
||||||
|
Table music_entries {
|
||||||
|
id varchar [primary key]
|
||||||
|
artist varchar [not null]
|
||||||
|
title varchar [not null]
|
||||||
|
links text [not null, note: 'Comma separated list']
|
||||||
|
universal_ids text [not null, note: 'Comma separated JSON-encoded list of Universal IDs']
|
||||||
|
}
|
||||||
|
|
||||||
|
Table location_entries {
|
||||||
|
id varchar [primary key]
|
||||||
|
location_text text
|
||||||
|
location_coordinates varchar [note: 'JSON encoded location']
|
||||||
|
}
|
||||||
|
|
||||||
|
Table date_entries {
|
||||||
|
id varchar [primary key]
|
||||||
|
referenced_date timestamp
|
||||||
|
}
|
||||||
|
|
||||||
|
Ref: users.limits > limits.id
|
||||||
|
|
||||||
|
Ref: heirs.user_id > users.id
|
||||||
|
Ref: entries.user_id > users.id
|
||||||
|
Ref: session_keys.user_id > users.id
|
||||||
|
|
||||||
|
Ref: entries.music_entry > music_entries.id
|
||||||
|
Ref: entries.location_entry > location_entries.id
|
||||||
|
Ref: entries.date_entry > date_entries.id
|
|
@ -8,19 +8,26 @@
|
||||||
"packageManager": "yarn@4.3.0",
|
"packageManager": "yarn@4.3.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fastify/cors": "^9.0.1",
|
"@fastify/cors": "^9.0.1",
|
||||||
|
"@fastify/type-provider-typebox": "^4.0.0",
|
||||||
|
"@sinclair/typebox": "^0.32.34",
|
||||||
|
"argon2": "^0.40.3",
|
||||||
|
"better-sqlite3": "^11.1.1",
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
|
"drizzle-orm": "^0.31.2",
|
||||||
"fastify": "^4.27.0",
|
"fastify": "^4.27.0",
|
||||||
"jose": "^5.4.0"
|
"jose": "^5.4.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node src/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",
|
||||||
|
"@types/node": "^20.14.9",
|
||||||
"eslint": "9.x",
|
"eslint": "9.x",
|
||||||
"globals": "^15.5.0",
|
"globals": "^15.5.0",
|
||||||
"prettier": "3.3.2"
|
"prettier": "3.3.2",
|
||||||
|
"typescript": "^5.5.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,8 +14,12 @@
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import { TypeBoxTypeProvider } from "@fastify/type-provider-typebox";
|
||||||
import Fastify from "fastify";
|
import Fastify from "fastify";
|
||||||
|
|
||||||
export default Fastify({
|
let app = Fastify({
|
||||||
logger: true,
|
logger: true,
|
||||||
});
|
}).withTypeProvider<TypeBoxTypeProvider>()
|
||||||
|
|
||||||
|
export type AppInterface = typeof app;
|
||||||
|
export default app;
|
File diff suppressed because one or more lines are too long
121
identity-api/src/auth.ts
Normal file
121
identity-api/src/auth.ts
Normal file
|
@ -0,0 +1,121 @@
|
||||||
|
// Identity. Store your memories and mental belongings
|
||||||
|
// Copyright (C) 2024 Sofía Aritz
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as published
|
||||||
|
// by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import * as argon2 from "argon2";
|
||||||
|
import { randomUUID } from "node:crypto";
|
||||||
|
import * as Jose from "jose";
|
||||||
|
import { JWT_ALG, JWT_SECRET } from "./consts.js";
|
||||||
|
import { DatabaseInterface, toDBList } from "./database.js";
|
||||||
|
|
||||||
|
|
||||||
|
export type AuthInterface = Awaited<ReturnType<typeof startAuth>>;
|
||||||
|
|
||||||
|
export type User = {
|
||||||
|
id: string,
|
||||||
|
createdAt: number,
|
||||||
|
lastConnected: number,
|
||||||
|
name: string,
|
||||||
|
email: string,
|
||||||
|
password: string,
|
||||||
|
limitID: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NewUser = {
|
||||||
|
name: string,
|
||||||
|
email: string,
|
||||||
|
password: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UpdateUser = {
|
||||||
|
name?: string,
|
||||||
|
email?: string,
|
||||||
|
password?: string,
|
||||||
|
assets?: string[],
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function startAuth(database: DatabaseInterface) {
|
||||||
|
let funcs = {
|
||||||
|
user: async (uid: string) => await database.user(uid) satisfies User,
|
||||||
|
findUserByEmail: async (email: string) => await database.findUserByEmail(email) satisfies User,
|
||||||
|
findUserBySessionKey: async (sessionKey: string) => {
|
||||||
|
let key = await database.sessionKey(sessionKey);
|
||||||
|
return await database.user(key.userID);
|
||||||
|
},
|
||||||
|
updateUser: async (uid: string, newUser: UpdateUser) => {
|
||||||
|
let user = newUser as any;
|
||||||
|
user.assets = toDBList(user.assets);
|
||||||
|
|
||||||
|
return await database.updateUser(uid, user);
|
||||||
|
},
|
||||||
|
addUser: async (user: NewUser) => {
|
||||||
|
let result = await database.insertUser({
|
||||||
|
id: randomUUID(),
|
||||||
|
createdAt: Date.now(),
|
||||||
|
lastConnected: Date.now(),
|
||||||
|
name: user.name,
|
||||||
|
email: user.email,
|
||||||
|
password: await argon2.hash(user.password),
|
||||||
|
assets: toDBList([]),
|
||||||
|
// FIXME: This shouldn't be required, the DB interface overwrites it.
|
||||||
|
limitID: "",
|
||||||
|
}, {
|
||||||
|
id: randomUUID(),
|
||||||
|
currentAssetCount: 0,
|
||||||
|
maxAssetCount: 5,
|
||||||
|
});
|
||||||
|
|
||||||
|
return result satisfies User;
|
||||||
|
},
|
||||||
|
verifyPassword: async (user: User, password: string) => {
|
||||||
|
return await argon2.verify(user.password, password);
|
||||||
|
},
|
||||||
|
createSessionKey: async (uid) => {
|
||||||
|
return await database.insertSessionKey({
|
||||||
|
userID: uid,
|
||||||
|
key: randomUUID(),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
createJwt: async (uid) => {
|
||||||
|
let user = await funcs.user(uid);
|
||||||
|
|
||||||
|
return await new Jose.SignJWT({
|
||||||
|
uid: user.id,
|
||||||
|
email: user.email,
|
||||||
|
name: user.name,
|
||||||
|
})
|
||||||
|
.setProtectedHeader({ alg: JWT_ALG })
|
||||||
|
.setIssuedAt()
|
||||||
|
.setExpirationTime("4w")
|
||||||
|
.sign(JWT_SECRET);
|
||||||
|
},
|
||||||
|
verifyJwt: async (jwt) => {
|
||||||
|
return await Jose.jwtVerify<{
|
||||||
|
uid: string,
|
||||||
|
email: string,
|
||||||
|
name: string,
|
||||||
|
}>(jwt, JWT_SECRET);
|
||||||
|
},
|
||||||
|
cleanUser: (user: User) => {
|
||||||
|
let clean = user as any;
|
||||||
|
clean.password = undefined;
|
||||||
|
clean.limitID = undefined;
|
||||||
|
|
||||||
|
return clean;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return funcs;
|
||||||
|
}
|
|
@ -17,7 +17,12 @@
|
||||||
import "dotenv/config";
|
import "dotenv/config";
|
||||||
import app from "./app.js";
|
import app from "./app.js";
|
||||||
|
|
||||||
const REQUIRED_VARS = ["IDENTITY_API_JWT_SECRET", "IDENTITY_API_ASSET_API_ENDPOINT", "IDENTITY_API_JWT_ALG"];
|
const REQUIRED_VARS = [
|
||||||
|
"IDENTITY_API_JWT_SECRET",
|
||||||
|
"IDENTITY_API_ASSET_API_ENDPOINT",
|
||||||
|
"IDENTITY_API_JWT_ALG",
|
||||||
|
"IDENTITY_SQLITE_PATH",
|
||||||
|
];
|
||||||
|
|
||||||
REQUIRED_VARS.forEach((element) => {
|
REQUIRED_VARS.forEach((element) => {
|
||||||
if (
|
if (
|
||||||
|
@ -32,7 +37,8 @@ REQUIRED_VARS.forEach((element) => {
|
||||||
export const IDENTITY_API_LANDING_MESSAGE = process.env["IDENTITY_API_LANDING_MESSAGE"] || "identity-api v1.0.0";
|
export const IDENTITY_API_LANDING_MESSAGE = process.env["IDENTITY_API_LANDING_MESSAGE"] || "identity-api v1.0.0";
|
||||||
export const JWT_SECRET = new TextEncoder().encode(process.env["IDENTITY_API_JWT_SECRET"]);
|
export const JWT_SECRET = new TextEncoder().encode(process.env["IDENTITY_API_JWT_SECRET"]);
|
||||||
export const JWT_ALG = process.env["IDENTITY_API_JWT_ALG"];
|
export const JWT_ALG = process.env["IDENTITY_API_JWT_ALG"];
|
||||||
export const LISTEN_PORT = process.env["IDENTITY_API_LISTEN_PORT"] || 3000;
|
export const LISTEN_PORT = Number(process.env["IDENTITY_API_LISTEN_PORT"]) || 3000;
|
||||||
export const ASSET_API_ENDPOINT = process.env["IDENTITY_API_ASSET_API_ENDPOINT"];
|
export const ASSET_API_ENDPOINT = process.env["IDENTITY_API_ASSET_API_ENDPOINT"];
|
||||||
export const ASSET_API_M2M_REFRESH_INTERVAL =
|
export const ASSET_API_M2M_REFRESH_INTERVAL =
|
||||||
process.env["IDENTITY_API_ASSET_API_M2M_REFRESH_INTERVAL_MS"] || 60 * 1000;
|
Number(process.env["IDENTITY_API_ASSET_API_M2M_REFRESH_INTERVAL_MS"]) || 60 * 1000;
|
||||||
|
export const SQLITE_PATH = process.env["IDENTITY_SQLITE_PATH"];
|
316
identity-api/src/database.ts
Normal file
316
identity-api/src/database.ts
Normal file
|
@ -0,0 +1,316 @@
|
||||||
|
// Identity. Store your memories and mental belongings
|
||||||
|
// Copyright (C) 2024 Sofía Aritz
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as published
|
||||||
|
// by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import { drizzle } from "drizzle-orm/better-sqlite3";
|
||||||
|
import Database from "better-sqlite3";
|
||||||
|
import { SQLITE_PATH } from "./consts.js";
|
||||||
|
import { sqliteTable } from "drizzle-orm/sqlite-core";
|
||||||
|
import { text, integer } from "drizzle-orm/sqlite-core";
|
||||||
|
import { asc, desc, eq, sql } from "drizzle-orm";
|
||||||
|
|
||||||
|
export type DatabaseInterface = Awaited<ReturnType<typeof startDatabase>>
|
||||||
|
|
||||||
|
export function toDBList(input: any[]): string {
|
||||||
|
return JSON.stringify(input);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fromDBList<T>(input: string): Array<T> {
|
||||||
|
return JSON.parse(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function startDatabase() {
|
||||||
|
let sqlite = new Database(SQLITE_PATH);
|
||||||
|
let database = drizzle(sqlite);
|
||||||
|
|
||||||
|
const limits = sqliteTable("limits", {
|
||||||
|
id: text("id").primaryKey(),
|
||||||
|
currentAssetCount: integer("current_asset_count").notNull(),
|
||||||
|
maxAssetCount: integer("max_asset_count").notNull(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const users = sqliteTable("users", {
|
||||||
|
id: text("id").primaryKey(),
|
||||||
|
createdAt: integer("created_at").notNull(),
|
||||||
|
lastConnected: integer("last_connected_at").notNull(),
|
||||||
|
name: text("name").notNull(),
|
||||||
|
email: text("email").notNull(),
|
||||||
|
password: text("password").notNull(),
|
||||||
|
assets: text("assets").notNull(),
|
||||||
|
limitID: text("limits")
|
||||||
|
.notNull()
|
||||||
|
.references(() => limits.id),
|
||||||
|
});
|
||||||
|
|
||||||
|
const session_keys = sqliteTable("session_keys", {
|
||||||
|
key: text("key").primaryKey(),
|
||||||
|
userID: text("user_id")
|
||||||
|
.notNull()
|
||||||
|
.references(() => users.id),
|
||||||
|
});
|
||||||
|
|
||||||
|
const heirs = sqliteTable("heirs", {
|
||||||
|
id: text("id").primaryKey(),
|
||||||
|
userID: text("user_id")
|
||||||
|
.notNull()
|
||||||
|
.references(() => users.id),
|
||||||
|
createdAt: integer("created_at").notNull(),
|
||||||
|
name: text("name").notNull(),
|
||||||
|
email: text("email"),
|
||||||
|
});
|
||||||
|
|
||||||
|
const musicEntries = sqliteTable("music_entries", {
|
||||||
|
id: text("id").primaryKey(),
|
||||||
|
artist: text("artist").notNull(),
|
||||||
|
title: text("title").notNull(),
|
||||||
|
links: text("links").notNull(),
|
||||||
|
universalIDs: text("universal_ids").notNull(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const locationEntries = sqliteTable("location_entries", {
|
||||||
|
id: text("id").primaryKey(),
|
||||||
|
locationText: text("location_text"),
|
||||||
|
locationCoordinates: text("location_coordinates"),
|
||||||
|
});
|
||||||
|
|
||||||
|
const dateEntries = sqliteTable("date_entries", {
|
||||||
|
id: text("id").primaryKey(),
|
||||||
|
referencedDate: integer("referenced_date"),
|
||||||
|
});
|
||||||
|
|
||||||
|
const entries = sqliteTable("entries", {
|
||||||
|
id: text("id").primaryKey(),
|
||||||
|
userID: text("user_id")
|
||||||
|
.notNull()
|
||||||
|
.references(() => users.id),
|
||||||
|
createdAt: integer("created_at").notNull(),
|
||||||
|
feelings: text("feelings").notNull(),
|
||||||
|
assets: text("assets").notNull(),
|
||||||
|
title: text("title"),
|
||||||
|
description: text("description"),
|
||||||
|
kind: text("kind").notNull(),
|
||||||
|
musicEntry: text("music_entry").references(() => musicEntries.id),
|
||||||
|
locationEntry: text("location_entry").references(() => locationEntries.id),
|
||||||
|
dateEntry: text("date_entry").references(() => dateEntries.id),
|
||||||
|
});
|
||||||
|
|
||||||
|
await runMigrations(database);
|
||||||
|
|
||||||
|
let funcs = {
|
||||||
|
insertHeir: async (heir: typeof heirs.$inferInsert) => {
|
||||||
|
let result = await database.insert(heirs).values(heir).returning({ id: heirs.id });
|
||||||
|
return result[0].id;
|
||||||
|
},
|
||||||
|
removeHeir: async (id: string) => {
|
||||||
|
await database.delete(heirs).where(eq(heirs.id, id));
|
||||||
|
},
|
||||||
|
listHeirs: async (userID: string) => {
|
||||||
|
return await database.select().from(heirs).where(eq(heirs.userID, userID));
|
||||||
|
},
|
||||||
|
insertUser: async (user: typeof users.$inferInsert, limit: typeof limits.$inferInsert) => {
|
||||||
|
let limitsResult = await database.insert(limits).values(limit).returning({ id: limits.id });
|
||||||
|
user.limitID = limitsResult[0].id;
|
||||||
|
|
||||||
|
let userResult = await database.insert(users).values(user).returning();
|
||||||
|
return userResult[0];
|
||||||
|
},
|
||||||
|
user: async (userID: string) => {
|
||||||
|
let result = await database.select().from(users).where(eq(users.id, userID));
|
||||||
|
return result[0];
|
||||||
|
},
|
||||||
|
userLimits: async (limitsID: string) => {
|
||||||
|
let result = await database.select().from(limits).where(eq(limits.id, limitsID));
|
||||||
|
return result[0];
|
||||||
|
},
|
||||||
|
updateUser: async (userID: string, newUser: {
|
||||||
|
name?: string,
|
||||||
|
email?: string,
|
||||||
|
password?: string,
|
||||||
|
}) => {
|
||||||
|
let result = await database.update(users).set(newUser).where(eq(users.id, userID)).returning();
|
||||||
|
return result[0];
|
||||||
|
},
|
||||||
|
findUserByEmail: async (email: string) => {
|
||||||
|
let result = await database.select().from(users).where(eq(users.email, email));
|
||||||
|
return result[0];
|
||||||
|
},
|
||||||
|
insertSessionKey: async (key: typeof session_keys.$inferInsert) => {
|
||||||
|
let result = await database.insert(session_keys).values(key).returning({ key: session_keys.key });
|
||||||
|
return result[0].key;
|
||||||
|
},
|
||||||
|
sessionKey: async (key: string) => {
|
||||||
|
let result = await database.select().from(session_keys).where(eq(session_keys.key, key));
|
||||||
|
return result[0];
|
||||||
|
},
|
||||||
|
findSessionKeyByUserID: async (userID: string) => {
|
||||||
|
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) => {
|
||||||
|
if (entry.kind === "album" || entry.kind === "song") {
|
||||||
|
let result = await database.insert(musicEntries).values(musicEntry).returning({ id: musicEntries.id });
|
||||||
|
entry.musicEntry = result[0].id;
|
||||||
|
} else if (entry.kind === "environment") {
|
||||||
|
let result = await database
|
||||||
|
.insert(locationEntries)
|
||||||
|
.values(locationEntry)
|
||||||
|
.returning({ id: locationEntries.id });
|
||||||
|
entry.locationEntry = result[0].id;
|
||||||
|
} else if (entry.kind === "date") {
|
||||||
|
let result = await database.insert(dateEntries).values(dateEntry).returning({ id: dateEntries.id });
|
||||||
|
entry.dateEntry = result[0].id;
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = await database.insert(entries).values(entry).returning({ id: entries.id });
|
||||||
|
return result[0].id;
|
||||||
|
},
|
||||||
|
removeEntry: async (entryID: string) => {
|
||||||
|
await database.delete(entries).where(eq(entries.id, entryID));
|
||||||
|
},
|
||||||
|
entryPage: async (userID: string, offset: number, limit: number) => {
|
||||||
|
let result = await database
|
||||||
|
.select()
|
||||||
|
.from(entries)
|
||||||
|
.where(eq(entries.userID, userID))
|
||||||
|
.limit(limit)
|
||||||
|
.offset(offset)
|
||||||
|
.orderBy(desc(entries.createdAt));
|
||||||
|
|
||||||
|
for (let key in result) {
|
||||||
|
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 = {
|
||||||
|
referencedDate: new Date(dateDetails["referencedDate"]).toISOString()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
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 funcs;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runMigrations(database) {
|
||||||
|
await database.run(sql`CREATE TABLE IF NOT EXISTS limits (
|
||||||
|
id varchar PRIMARY KEY,
|
||||||
|
current_asset_count integer NOT NULL,
|
||||||
|
max_asset_count integer NOT NULL
|
||||||
|
);`);
|
||||||
|
|
||||||
|
await database.run(sql`CREATE TABLE IF NOT EXISTS users (
|
||||||
|
id varchar PRIMARY KEY,
|
||||||
|
created_at timestamp NOT NULL,
|
||||||
|
last_connected_at timestamp NOT NULL,
|
||||||
|
email varchar NOT NULL,
|
||||||
|
password varchar NOT NULL,
|
||||||
|
name varchar NOT NULL,
|
||||||
|
limits varchar NOT NULL,
|
||||||
|
assets varchar NOT NULL,
|
||||||
|
FOREIGN KEY (limits) REFERENCES limits (id)
|
||||||
|
);`);
|
||||||
|
|
||||||
|
await database.run(sql`
|
||||||
|
CREATE TABLE IF NOT EXISTS session_keys (
|
||||||
|
key varchar PRIMARY KEY,
|
||||||
|
user_id varchar NOT NULL,
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users (id)
|
||||||
|
);`);
|
||||||
|
|
||||||
|
await database.run(sql`
|
||||||
|
CREATE TABLE IF NOT EXISTS heirs (
|
||||||
|
id varchar PRIMARY KEY,
|
||||||
|
user_id varchar NOT NULL,
|
||||||
|
created_at timestamp NOT NULL,
|
||||||
|
name varchar NOT NULL,
|
||||||
|
email varchar,
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users (id)
|
||||||
|
);`);
|
||||||
|
|
||||||
|
await database.run(sql`
|
||||||
|
CREATE TABLE IF NOT EXISTS entries (
|
||||||
|
id varchar PRIMARY KEY,
|
||||||
|
user_id varchar NOT NULL,
|
||||||
|
created_at timestamp NOT NULL,
|
||||||
|
feelings text NOT NULL,
|
||||||
|
assets text NOT NULL,
|
||||||
|
title text,
|
||||||
|
description text,
|
||||||
|
kind varchar NOT NULL,
|
||||||
|
music_entry varchar,
|
||||||
|
location_entry varchar,
|
||||||
|
date_entry varchar,
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users (id),
|
||||||
|
FOREIGN KEY (music_entry) REFERENCES music_entries (id),
|
||||||
|
FOREIGN KEY (location_entry) REFERENCES location_entries (id),
|
||||||
|
FOREIGN KEY (date_entry) REFERENCES date_entries (id)
|
||||||
|
);`);
|
||||||
|
|
||||||
|
await database.run(sql`
|
||||||
|
CREATE TABLE IF NOT EXISTS music_entries (
|
||||||
|
id varchar PRIMARY KEY,
|
||||||
|
artist varchar NOT NULL,
|
||||||
|
title varchar NOT NULL,
|
||||||
|
links text NOT NULL,
|
||||||
|
universal_ids text NOT NULL
|
||||||
|
);`);
|
||||||
|
|
||||||
|
await database.run(sql`
|
||||||
|
CREATE TABLE IF NOT EXISTS location_entries (
|
||||||
|
id varchar PRIMARY KEY,
|
||||||
|
location_text text,
|
||||||
|
location_coordinates varchar
|
||||||
|
);`);
|
||||||
|
|
||||||
|
await database.run(sql`
|
||||||
|
CREATE TABLE IF NOT EXISTS date_entries (
|
||||||
|
id varchar PRIMARY KEY,
|
||||||
|
referenced_date timestamp
|
||||||
|
);`);
|
||||||
|
}
|
|
@ -20,8 +20,10 @@ import { startAuth } from "./auth.js";
|
||||||
|
|
||||||
import cors from "@fastify/cors";
|
import cors from "@fastify/cors";
|
||||||
import { registerRoutes } from "./routes/index.js";
|
import { registerRoutes } from "./routes/index.js";
|
||||||
|
import { startDatabase } from "./database.js";
|
||||||
|
|
||||||
let auth = await startAuth();
|
let database = await startDatabase();
|
||||||
|
let auth = await startAuth(database);
|
||||||
|
|
||||||
app.addSchema({
|
app.addSchema({
|
||||||
$id: "schema://identity/authorization",
|
$id: "schema://identity/authorization",
|
||||||
|
@ -36,7 +38,7 @@ app.register(cors, {
|
||||||
origin: true,
|
origin: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
registerRoutes(app, auth);
|
registerRoutes(app, auth, database);
|
||||||
|
|
||||||
app.get("/", async () => {
|
app.get("/", async () => {
|
||||||
return IDENTITY_API_LANDING_MESSAGE;
|
return IDENTITY_API_LANDING_MESSAGE;
|
|
@ -14,10 +14,11 @@
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
|
||||||
import { ASSET_API_ENDPOINT } from "../../consts.js";
|
import { ASSET_API_ENDPOINT } from "../../consts.js";
|
||||||
|
|
||||||
export default function register(app) {
|
import type { AppInterface } from "../../app.js";
|
||||||
|
|
||||||
|
export default function register(app: AppInterface) {
|
||||||
app.get("/asset/endpoint", {
|
app.get("/asset/endpoint", {
|
||||||
async handler() {
|
async handler() {
|
||||||
return ASSET_API_ENDPOINT;
|
return ASSET_API_ENDPOINT;
|
|
@ -14,9 +14,10 @@
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import type { AppInterface } from "../../app.js";
|
||||||
|
|
||||||
import endpoint from "./endpoint.js";
|
import endpoint from "./endpoint.js";
|
||||||
|
|
||||||
export default function registerRoutes(app, auth) {
|
export default function registerRoutes(app: AppInterface) {
|
||||||
endpoint(app, auth);
|
endpoint(app);
|
||||||
}
|
}
|
|
@ -14,8 +14,10 @@
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import type { AppInterface } from "../../app.js";
|
||||||
|
import type { AuthInterface } from "../../auth.js";
|
||||||
|
|
||||||
export default function register(app, auth) {
|
export default function register(app: AppInterface, auth: AuthInterface) {
|
||||||
app.get("/auth/account", {
|
app.get("/auth/account", {
|
||||||
async handler(request, reply) {
|
async handler(request, reply) {
|
||||||
let jwt = request.headers["authorization"].replace("Bearer", "").trim();
|
let jwt = request.headers["authorization"].replace("Bearer", "").trim();
|
||||||
|
@ -27,9 +29,7 @@ export default function register(app, auth) {
|
||||||
}
|
}
|
||||||
|
|
||||||
let user = await auth.user(payload.uid);
|
let user = await auth.user(payload.uid);
|
||||||
user.password = undefined;
|
return auth.cleanUser(user);
|
||||||
user.entries = undefined;
|
|
||||||
return user;
|
|
||||||
},
|
},
|
||||||
schema: {
|
schema: {
|
||||||
headers: { $ref: "schema://identity/authorization" },
|
headers: { $ref: "schema://identity/authorization" },
|
|
@ -14,8 +14,10 @@
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import type { AppInterface } from "../../app.js";
|
||||||
|
import type { AuthInterface } from "../../auth.js";
|
||||||
|
|
||||||
export default function register(app, auth) {
|
export default function register(app: AppInterface, auth: AuthInterface) {
|
||||||
app.get("/auth/genkey", {
|
app.get("/auth/genkey", {
|
||||||
async handler(request) {
|
async handler(request) {
|
||||||
let jwt = request.headers["authorization"].replace("Bearer", "").trim();
|
let jwt = request.headers["authorization"].replace("Bearer", "").trim();
|
|
@ -1,41 +0,0 @@
|
||||||
// Identity. Store your memories and mental belongings
|
|
||||||
// Copyright (C) 2024 Sofía Aritz
|
|
||||||
//
|
|
||||||
// This program is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Affero General Public License as published
|
|
||||||
// by the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
//
|
|
||||||
// This program is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Affero General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
|
|
||||||
export default function register(app, auth) {
|
|
||||||
app.post("/auth/heirs", {
|
|
||||||
async handler(request, reply) {
|
|
||||||
let jwt = request.headers["authorization"].replace("Bearer", "").trim();
|
|
||||||
let { payload } = await auth.verifyJwt(jwt);
|
|
||||||
|
|
||||||
if (payload.uid == null) {
|
|
||||||
reply.status(401);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let user = await auth.user(payload.uid);
|
|
||||||
user.heirs = request.body;
|
|
||||||
|
|
||||||
await auth.updateUser(payload.uid, user);
|
|
||||||
},
|
|
||||||
schema: {
|
|
||||||
headers: { $ref: "schema://identity/authorization" },
|
|
||||||
body: {
|
|
||||||
type: "array",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
90
identity-api/src/routes/auth/heirs.ts
Normal file
90
identity-api/src/routes/auth/heirs.ts
Normal file
|
@ -0,0 +1,90 @@
|
||||||
|
// Identity. Store your memories and mental belongings
|
||||||
|
// Copyright (C) 2024 Sofía Aritz
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as published
|
||||||
|
// by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import { randomUUID } from "node:crypto";
|
||||||
|
import type { AppInterface } from "../../app.js";
|
||||||
|
import type { AuthInterface } from "../../auth.js";
|
||||||
|
import { DatabaseInterface } from "../../database.js";
|
||||||
|
import { Static, Type } from "@sinclair/typebox";
|
||||||
|
|
||||||
|
const Body = Type.Object({
|
||||||
|
contactMethod: Type.String(),
|
||||||
|
name: Type.String(),
|
||||||
|
value: Type.String(),
|
||||||
|
})
|
||||||
|
|
||||||
|
type BodyType = Static<typeof Body>;
|
||||||
|
|
||||||
|
export default function register(app: AppInterface, auth: AuthInterface, database: DatabaseInterface) {
|
||||||
|
app.put<{ Body: BodyType }>("/auth/heirs", {
|
||||||
|
async handler(request, reply) {
|
||||||
|
let jwt = request.headers["authorization"].replace("Bearer", "").trim();
|
||||||
|
let { payload } = await auth.verifyJwt(jwt);
|
||||||
|
|
||||||
|
if (payload.uid == null) {
|
||||||
|
reply.status(401);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.body.contactMethod !== "email") {
|
||||||
|
reply.status(400);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await database.insertHeir({
|
||||||
|
id: randomUUID(),
|
||||||
|
createdAt: Date.now(),
|
||||||
|
userID: payload.uid,
|
||||||
|
name: request.body.name,
|
||||||
|
email: request.body.value,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (await database.listHeirs(payload.uid))
|
||||||
|
.map(v => v["contactMethod"] = "email")
|
||||||
|
.map(v => v["value"] = v["email"])
|
||||||
|
.map(v => v["email"] = undefined);
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
headers: { $ref: "schema://identity/authorization" },
|
||||||
|
body: Body,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
app.delete<{ Body: string }>("/auth/heirs", {
|
||||||
|
async handler(request, reply) {
|
||||||
|
let jwt = request.headers["authorization"].replace("Bearer", "").trim();
|
||||||
|
let { payload } = await auth.verifyJwt(jwt);
|
||||||
|
|
||||||
|
if (payload.uid == null) {
|
||||||
|
reply.status(401);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await database.removeHeir(request.body);
|
||||||
|
|
||||||
|
return (await database.listHeirs(payload.uid))
|
||||||
|
.map(v => v["contactMethod"] = "email")
|
||||||
|
.map(v => v["value"] = v["email"])
|
||||||
|
.map(v => v["email"] = undefined);
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
headers: { $ref: "schema://identity/authorization" },
|
||||||
|
body: {
|
||||||
|
type: "string",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
|
@ -14,6 +14,9 @@
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import type { AppInterface } from "../../app.js";
|
||||||
|
import type { AuthInterface } from "../../auth.js";
|
||||||
|
import { DatabaseInterface } from "../../database.js";
|
||||||
|
|
||||||
import account from "./account.js";
|
import account from "./account.js";
|
||||||
import genkey from "./genkey.js";
|
import genkey from "./genkey.js";
|
||||||
|
@ -21,10 +24,10 @@ import heirs from "./heirs.js";
|
||||||
import login from "./login.js";
|
import login from "./login.js";
|
||||||
import register from "./register.js";
|
import register from "./register.js";
|
||||||
|
|
||||||
export default function registerRoutes(app, auth) {
|
export default function registerRoutes(app: AppInterface, auth: AuthInterface, database: DatabaseInterface) {
|
||||||
account(app, auth);
|
account(app, auth);
|
||||||
genkey(app, auth);
|
genkey(app, auth);
|
||||||
heirs(app, auth);
|
heirs(app, auth, database);
|
||||||
login(app, auth);
|
login(app, auth);
|
||||||
register(app, auth);
|
register(app, auth);
|
||||||
}
|
}
|
|
@ -14,14 +14,24 @@
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import { Static, Type } from "@sinclair/typebox";
|
||||||
|
import type { AppInterface } from "../../app.js";
|
||||||
|
import type { AuthInterface } from "../../auth.js";
|
||||||
|
|
||||||
export default function register(app, auth) {
|
const Body = Type.Object({
|
||||||
app.post("/auth/login", {
|
email: Type.String({ format: "email" }),
|
||||||
|
password: Type.String(),
|
||||||
|
});
|
||||||
|
|
||||||
|
type BodyType = Static<typeof Body>;
|
||||||
|
|
||||||
|
export default function register(app: AppInterface, auth: AuthInterface) {
|
||||||
|
app.post<{ Body: BodyType }>("/auth/login", {
|
||||||
async handler(request, reply) {
|
async handler(request, reply) {
|
||||||
let user = await auth.findUserByEmail(request.body.email);
|
let user = await auth.findUserByEmail(request.body.email);
|
||||||
|
|
||||||
if (user != null && user.password == request.body.password) {
|
if (user != null && auth.verifyPassword(user, request.body.password)) {
|
||||||
let token = await auth.createJwt(user.uid);
|
let token = await auth.createJwt(user.id);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
token,
|
token,
|
||||||
|
@ -34,14 +44,7 @@ export default function register(app, auth) {
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
schema: {
|
schema: {
|
||||||
body: {
|
body: Body,
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
email: { type: "string" },
|
|
||||||
password: { type: "string" },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
required: ["email", "password"],
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
|
@ -14,23 +14,29 @@
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import { Static, Type } from "@sinclair/typebox";
|
||||||
|
import type { AppInterface } from "../../app.js";
|
||||||
|
import type { AuthInterface } from "../../auth.js";
|
||||||
|
|
||||||
export default function register(app, auth) {
|
const Body = Type.Object({
|
||||||
app.post("/auth/register", {
|
name: Type.String(),
|
||||||
|
email: Type.String({ format: "email" }),
|
||||||
|
password: Type.String(),
|
||||||
|
});
|
||||||
|
|
||||||
|
type BodyType = Static<typeof Body>;
|
||||||
|
|
||||||
|
export default function register(app: AppInterface, auth: AuthInterface) {
|
||||||
|
app.post<{ Body: BodyType }>("/auth/register", {
|
||||||
async handler(request, reply) {
|
async handler(request, reply) {
|
||||||
if ((await auth.findUserByEmail(request.body.email)) == null) {
|
if ((await auth.findUserByEmail(request.body.email)) == null) {
|
||||||
let user = await auth.addUser({
|
let user = await auth.addUser({
|
||||||
email: request.body.email,
|
email: request.body.email,
|
||||||
password: request.body.password,
|
password: request.body.password,
|
||||||
name: request.body.name,
|
name: request.body.name,
|
||||||
assets: [],
|
|
||||||
limits: {
|
|
||||||
assetCount: 5,
|
|
||||||
},
|
|
||||||
entries: [],
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return { token: await auth.createJwt(user.uid) };
|
return { token: await auth.createJwt(user.id) };
|
||||||
}
|
}
|
||||||
|
|
||||||
reply.code(400);
|
reply.code(400);
|
||||||
|
@ -39,15 +45,7 @@ export default function register(app, auth) {
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
schema: {
|
schema: {
|
||||||
body: {
|
body: Body,
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
name: { type: "string" },
|
|
||||||
email: { type: "string" },
|
|
||||||
password: { type: "string" },
|
|
||||||
},
|
|
||||||
required: ["name", "email", "password"],
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
|
@ -1,68 +0,0 @@
|
||||||
// Identity. Store your memories and mental belongings
|
|
||||||
// Copyright (C) 2024 Sofía Aritz
|
|
||||||
//
|
|
||||||
// This program is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Affero General Public License as published
|
|
||||||
// by the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
//
|
|
||||||
// This program is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Affero General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
|
|
||||||
import { randomUUID } from "node:crypto";
|
|
||||||
import list from "./list.js";
|
|
||||||
|
|
||||||
export default function registerRoutes(app, auth) {
|
|
||||||
list(app, auth);
|
|
||||||
|
|
||||||
app.delete("/entry", {
|
|
||||||
async handler(request) {
|
|
||||||
let jwt = request.headers["authorization"].replace("Bearer", "").trim();
|
|
||||||
let { payload } = await auth.verifyJwt(jwt);
|
|
||||||
|
|
||||||
let user = await auth.user(payload.uid);
|
|
||||||
user.entries = user.entries.filter((v) => v.id !== request.query.entry_id);
|
|
||||||
|
|
||||||
await auth.updateUser(payload.uid, user);
|
|
||||||
},
|
|
||||||
schema: {
|
|
||||||
headers: { $ref: "schema://identity/authorization" },
|
|
||||||
query: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
entry_id: { type: "string" },
|
|
||||||
},
|
|
||||||
required: ["entry_id"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
app.put("/entry", {
|
|
||||||
async handler(request) {
|
|
||||||
let jwt = request.headers["authorization"].replace("Bearer", "").trim();
|
|
||||||
let { payload } = await auth.verifyJwt(jwt);
|
|
||||||
|
|
||||||
let user = await auth.user(payload.uid);
|
|
||||||
request.body.entry.id = randomUUID().toString();
|
|
||||||
user.entries = [request.body.entry, ...user.entries];
|
|
||||||
|
|
||||||
await auth.updateUser(payload.uid, user);
|
|
||||||
},
|
|
||||||
schema: {
|
|
||||||
headers: { $ref: "schema://identity/authorization" },
|
|
||||||
body: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
entry: { type: "object" },
|
|
||||||
},
|
|
||||||
required: ["entry"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
145
identity-api/src/routes/entry/index.ts
Normal file
145
identity-api/src/routes/entry/index.ts
Normal file
|
@ -0,0 +1,145 @@
|
||||||
|
// Identity. Store your memories and mental belongings
|
||||||
|
// Copyright (C) 2024 Sofía Aritz
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as published
|
||||||
|
// by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import { randomUUID } from "node:crypto";
|
||||||
|
import list from "./list.js";
|
||||||
|
|
||||||
|
import type { AppInterface } from "../../app.js";
|
||||||
|
import type { AuthInterface } from "../../auth.js";
|
||||||
|
import { toDBList, type DatabaseInterface } from "../../database.js";
|
||||||
|
import { Static, Type } from "@sinclair/typebox";
|
||||||
|
|
||||||
|
const EntryIDQuery = Type.Object({
|
||||||
|
entry_id: Type.String(),
|
||||||
|
})
|
||||||
|
|
||||||
|
type EntryIDQueryType = Static<typeof EntryIDQuery>;
|
||||||
|
|
||||||
|
const PutEntryBody = Type.Object({
|
||||||
|
entry: Type.Object({
|
||||||
|
title: Type.Optional(Type.String()),
|
||||||
|
description: Type.Optional(Type.String()),
|
||||||
|
creationDate: Type.String(),
|
||||||
|
assets: Type.Array(Type.String()),
|
||||||
|
feelings: Type.Array(
|
||||||
|
Type.Union([
|
||||||
|
Type.String(),
|
||||||
|
Type.Object({
|
||||||
|
identifier: Type.String(),
|
||||||
|
description: Type.String(),
|
||||||
|
backgroundColor: Type.String(),
|
||||||
|
textColor: Type.String(),
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
),
|
||||||
|
base: Type.Union([
|
||||||
|
Type.Object({
|
||||||
|
kind: Type.String(),
|
||||||
|
|
||||||
|
}),
|
||||||
|
Type.Object({
|
||||||
|
kind: Type.String(),
|
||||||
|
artist: Type.String(),
|
||||||
|
title: Type.String(),
|
||||||
|
link: Type.Array(Type.String()),
|
||||||
|
id: Type.Array(Type.Object({
|
||||||
|
provider: Type.String(),
|
||||||
|
id: Type.String(),
|
||||||
|
})),
|
||||||
|
}),
|
||||||
|
Type.Object({
|
||||||
|
kind: Type.String(),
|
||||||
|
location: Type.Union([
|
||||||
|
Type.String(),
|
||||||
|
Type.Object({
|
||||||
|
latitude: Type.Number(),
|
||||||
|
longitude: Type.Number(),
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
}),
|
||||||
|
Type.Object({
|
||||||
|
kind: Type.String(),
|
||||||
|
referencedDate: Type.String(),
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
type PutEntryBodyType = Static<typeof PutEntryBody>;
|
||||||
|
|
||||||
|
export default function registerRoutes(app: AppInterface, auth: AuthInterface, database: DatabaseInterface) {
|
||||||
|
list(app, auth, database);
|
||||||
|
|
||||||
|
app.delete<{ Querystring: EntryIDQueryType }>("/entry", {
|
||||||
|
async handler(request) {
|
||||||
|
let jwt = request.headers["authorization"].replace("Bearer", "").trim();
|
||||||
|
let { payload } = await auth.verifyJwt(jwt);
|
||||||
|
|
||||||
|
let user = await auth.user(payload.uid);
|
||||||
|
database.removeEntry(request.query.entry_id);
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
headers: { $ref: "schema://identity/authorization" },
|
||||||
|
querystring: EntryIDQuery,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
app.put<{ Body: PutEntryBodyType }>("/entry", {
|
||||||
|
async handler(request) {
|
||||||
|
let jwt = request.headers["authorization"].replace("Bearer", "").trim();
|
||||||
|
let { payload } = await auth.verifyJwt(jwt);
|
||||||
|
|
||||||
|
let entry = request.body.entry;
|
||||||
|
|
||||||
|
let musicEntry, locationEntry, dateEntry;
|
||||||
|
if ((entry.base.kind === "album" || entry.base.kind === "song") && 'artist' in entry.base) {
|
||||||
|
musicEntry = {
|
||||||
|
id: randomUUID(),
|
||||||
|
title: entry.base.title,
|
||||||
|
artist: entry.base.artist,
|
||||||
|
links: toDBList(entry.base.link),
|
||||||
|
universalIDs: toDBList(entry.base.id),
|
||||||
|
}
|
||||||
|
} else if (entry.base.kind === "environment" && 'location' in entry.base) {
|
||||||
|
locationEntry = {
|
||||||
|
id: randomUUID(),
|
||||||
|
locationText: typeof entry.base.location === "string" ? entry.base.location : undefined,
|
||||||
|
locationCoordinates: typeof entry.base.location === "string" ? undefined : JSON.stringify(entry.base.location),
|
||||||
|
}
|
||||||
|
} else if (entry.base.kind === "date" && 'referencedDate' in entry.base) {
|
||||||
|
dateEntry = {
|
||||||
|
id: randomUUID(),
|
||||||
|
referencedDate: Date.parse(entry.base.referencedDate),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await database.insertEntry({
|
||||||
|
id: randomUUID(),
|
||||||
|
userID: payload.uid,
|
||||||
|
feelings: toDBList(entry.feelings),
|
||||||
|
assets: toDBList(entry.assets),
|
||||||
|
title: entry.title,
|
||||||
|
description: entry.description,
|
||||||
|
kind: entry.base.kind,
|
||||||
|
createdAt: Date.now(),
|
||||||
|
}, musicEntry, locationEntry, dateEntry);
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
headers: { $ref: "schema://identity/authorization" },
|
||||||
|
body: PutEntryBody,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
|
@ -14,9 +14,20 @@
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import { Static, Type } from "@sinclair/typebox";
|
||||||
|
import type { AppInterface } from "../../app.js";
|
||||||
|
import type { AuthInterface } from "../../auth.js";
|
||||||
|
import type { DatabaseInterface } from "../../database.js";
|
||||||
|
|
||||||
export default function register(app, auth) {
|
const Query = Type.Object({
|
||||||
app.get("/entry/list", {
|
limit: Type.Number(),
|
||||||
|
offset: Type.Number(),
|
||||||
|
})
|
||||||
|
|
||||||
|
type QueryType = Static<typeof Query>;
|
||||||
|
|
||||||
|
export default function register(app: AppInterface, auth: AuthInterface, database: DatabaseInterface) {
|
||||||
|
app.get<{ Querystring: QueryType }>("/entry/list", {
|
||||||
async handler(request, reply) {
|
async handler(request, reply) {
|
||||||
if (request.query.offset < 0 || request.query.limit <= 0) {
|
if (request.query.offset < 0 || request.query.limit <= 0) {
|
||||||
reply.status(400);
|
reply.status(400);
|
||||||
|
@ -26,19 +37,11 @@ export default function register(app, auth) {
|
||||||
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);
|
||||||
|
|
||||||
let user = await auth.user(payload.uid);
|
return await database.entryPage(payload.uid, request.query.offset, request.query.limit)
|
||||||
return user.entries.slice(request.query.offset, request.query.offset + request.query.limit);
|
|
||||||
},
|
},
|
||||||
schema: {
|
schema: {
|
||||||
headers: { $ref: "schema://identity/authorization" },
|
headers: { $ref: "schema://identity/authorization" },
|
||||||
query: {
|
querystring: Query,
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
limit: { type: "number" },
|
|
||||||
offset: { type: "number" },
|
|
||||||
},
|
|
||||||
required: ["limit", "offset"],
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
|
@ -14,15 +14,18 @@
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
|
||||||
import m2m from "./m2m/index.js";
|
import m2m from "./m2m/index.js";
|
||||||
import asset from "./asset/index.js";
|
import asset from "./asset/index.js";
|
||||||
import entry from "./entry/index.js";
|
import entry from "./entry/index.js";
|
||||||
import authRoutes from "./auth/index.js";
|
import authRoutes from "./auth/index.js";
|
||||||
|
|
||||||
export function registerRoutes(app, auth) {
|
import type { AppInterface } from "../app.js";
|
||||||
m2m(app, auth);
|
import type { DatabaseInterface } from "../database.js";
|
||||||
asset(app, auth);
|
import type { AuthInterface } from "../auth.js";
|
||||||
entry(app, auth);
|
|
||||||
authRoutes(app, auth);
|
export function registerRoutes(app: AppInterface, auth: AuthInterface, database: DatabaseInterface) {
|
||||||
|
m2m(app, auth, database);
|
||||||
|
asset(app);
|
||||||
|
entry(app, auth, database);
|
||||||
|
authRoutes(app, auth, database);
|
||||||
}
|
}
|
|
@ -14,24 +14,30 @@
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
|
||||||
import { contentFromSigned, verifySignature } from "../../m2m.js";
|
import { contentFromSigned, verifySignature } from "../../m2m.js";
|
||||||
|
|
||||||
export default function register(app, auth) {
|
import type { AppInterface } from "../../app.js";
|
||||||
|
import type { AuthInterface } from "../../auth.js";
|
||||||
|
import { DatabaseInterface, fromDBList } from "../../database.js";
|
||||||
|
|
||||||
|
export default function register(app: AppInterface, auth: AuthInterface, database: DatabaseInterface) {
|
||||||
app.post("/m2m/account", {
|
app.post("/m2m/account", {
|
||||||
async handler(request, reply) {
|
async handler(request, reply) {
|
||||||
if (!verifySignature(request.body)) {
|
if (!verifySignature(request.body)) {
|
||||||
reply.statusCode(401);
|
reply.status(401);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
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);
|
||||||
user.password = undefined;
|
let limits = await database.userLimits(user.limitID);
|
||||||
user.entries = undefined;
|
(user.assets as any) = fromDBList(user.assets);
|
||||||
|
|
||||||
return user;
|
return {
|
||||||
|
user: auth.cleanUser(user),
|
||||||
|
limits,
|
||||||
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
|
@ -14,23 +14,29 @@
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
|
||||||
import { contentFromSigned, verifySignature } from "../../m2m.js";
|
import { contentFromSigned, verifySignature } from "../../m2m.js";
|
||||||
|
|
||||||
export default function register(app, auth) {
|
import type { AppInterface } from "../../app.js";
|
||||||
|
import type { AuthInterface } from "../../auth.js";
|
||||||
|
import { fromDBList } from "../../database.js";
|
||||||
|
|
||||||
|
export default function register(app: AppInterface, auth: AuthInterface) {
|
||||||
app.put("/m2m/asset", {
|
app.put("/m2m/asset", {
|
||||||
async handler(request, reply) {
|
async handler(request, reply) {
|
||||||
if (!verifySignature(request.body)) {
|
if (!verifySignature(request.body)) {
|
||||||
reply.statusCode(401);
|
reply.status(401);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
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);
|
||||||
user.assets.push(body.asset_id);
|
let assets = fromDBList(user.assets) as string[];
|
||||||
|
assets.push(body.asset_id);
|
||||||
|
|
||||||
await auth.updateUser(user.uid, user);
|
await auth.updateUser(user.id, {
|
||||||
|
assets,
|
||||||
|
});
|
||||||
app.log.info((await auth.findUserBySessionKey(body.session_key)).assets);
|
app.log.info((await auth.findUserBySessionKey(body.session_key)).assets);
|
||||||
},
|
},
|
||||||
});
|
});
|
|
@ -14,11 +14,14 @@
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
|
||||||
import asset from "./asset.js";
|
import asset from "./asset.js";
|
||||||
import account from "./account.js";
|
import account from "./account.js";
|
||||||
|
|
||||||
export default function registerRoutes(app, auth) {
|
import type { AppInterface } from "../../app.js";
|
||||||
|
import type { AuthInterface } from "../../auth.js";
|
||||||
|
import { DatabaseInterface } from "../../database.js";
|
||||||
|
|
||||||
|
export default function registerRoutes(app: AppInterface, auth: AuthInterface, database: DatabaseInterface) {
|
||||||
asset(app, auth);
|
asset(app, auth);
|
||||||
account(app, auth);
|
account(app, auth, database);
|
||||||
}
|
}
|
13
identity-api/tsconfig.json
Normal file
13
identity-api/tsconfig.json
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "NodeNext",
|
||||||
|
"target": "ES2021",
|
||||||
|
"moduleResolution": "NodeNext",
|
||||||
|
"sourceMap": true,
|
||||||
|
"outDir": "dist",
|
||||||
|
"types": ["node"],
|
||||||
|
"skipLibCheck": true
|
||||||
|
},
|
||||||
|
"include": ["./src/**/*"],
|
||||||
|
"exclude": ["./node_modules/**/*"]
|
||||||
|
}
|
File diff suppressed because it is too large
Load diff
|
@ -14,7 +14,14 @@ export type Credentials = {
|
||||||
token: string;
|
token: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type InsertHeir = {
|
||||||
|
contactMethod: 'email';
|
||||||
|
name: string;
|
||||||
|
value: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type AccountHeir = {
|
export type AccountHeir = {
|
||||||
|
id: string;
|
||||||
contactMethod: 'email';
|
contactMethod: 'email';
|
||||||
name: string;
|
name: string;
|
||||||
value: string;
|
value: string;
|
||||||
|
@ -22,8 +29,8 @@ export type AccountHeir = {
|
||||||
|
|
||||||
export type Account = {
|
export type Account = {
|
||||||
uid: string;
|
uid: string;
|
||||||
|
email: string;
|
||||||
name: string;
|
name: string;
|
||||||
heirs: AccountHeir[];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function sendRequest(
|
function sendRequest(
|
||||||
|
@ -119,14 +126,21 @@ export async function deleteEntry(credentials: Credentials, entry_id: string): P
|
||||||
await sendRequest('/entry', credentials, { method: 'DELETE' }, `?entry_id=${entry_id}`);
|
await sendRequest('/entry', credentials, { method: 'DELETE' }, `?entry_id=${entry_id}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function updateHeirs(credentials: Credentials, heirs: AccountHeir[]): Promise<void> {
|
export async function insertHeir(credentials: Credentials, heirs: InsertHeir): Promise<AccountHeir[]> {
|
||||||
await sendRequest('/auth/heirs', credentials, {
|
return await asJson(sendRequest('/auth/heirs', credentials, {
|
||||||
method: 'POST',
|
method: 'PUT',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
},
|
},
|
||||||
body: JSON.stringify(heirs)
|
body: JSON.stringify(heirs)
|
||||||
});
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function removeHeir(credentials: Credentials, heirID: string): Promise<AccountHeir[]> {
|
||||||
|
return await asJson(sendRequest('/auth/heirs', credentials, {
|
||||||
|
method: 'DELETE',
|
||||||
|
body: heirID,
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function uploadAsset(session_key: string, file: File): Promise<string> {
|
export async function uploadAsset(session_key: string, file: File): Promise<string> {
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { createForm } from 'felte';
|
import { createForm } from 'felte';
|
||||||
import { account, credentials, refreshAccount } from '$lib/stores';
|
import { account, credentials, refreshAccount } from '$lib/stores';
|
||||||
import { type AccountHeir, updateHeirs } from '$lib/api';
|
import { type AccountHeir, insertHeir, removeHeir } from '$lib/api';
|
||||||
|
|
||||||
credentials.subscribe(
|
credentials.subscribe(
|
||||||
(v) => v == null && setTimeout(() => (window.location.pathname = '/auth/login'), 200)
|
(v) => v == null && setTimeout(() => (window.location.pathname = '/auth/login'), 200)
|
||||||
|
@ -24,7 +24,7 @@
|
||||||
let currentHeirs = structuredClone($account!.heirs);
|
let currentHeirs = structuredClone($account!.heirs);
|
||||||
let updatedHeirs = [heir, ...currentHeirs];
|
let updatedHeirs = [heir, ...currentHeirs];
|
||||||
|
|
||||||
await updateHeirs($credentials!, updatedHeirs);
|
//await updateHeirs($credentials!, updatedHeirs);
|
||||||
await refreshAccount();
|
await refreshAccount();
|
||||||
|
|
||||||
heirWizard = false;
|
heirWizard = false;
|
||||||
|
|
|
@ -124,6 +124,9 @@
|
||||||
{:then entries}
|
{:then entries}
|
||||||
<div class="mt-3.5 flex justify-center">
|
<div class="mt-3.5 flex justify-center">
|
||||||
<div class="flex w-[60%] flex-col">
|
<div class="flex w-[60%] flex-col">
|
||||||
|
<h1 class="pb-3.5 text-2xl">
|
||||||
|
Welcome back, <span class="font-bold">{$account?.name}</span>.
|
||||||
|
</h1>
|
||||||
{#if entries.length === 0}
|
{#if entries.length === 0}
|
||||||
<a
|
<a
|
||||||
href="/entry/new"
|
href="/entry/new"
|
||||||
|
@ -133,10 +136,6 @@
|
||||||
<h2 class="text-xl font-semibold">Add an entry</h2>
|
<h2 class="text-xl font-semibold">Add an entry</h2>
|
||||||
</a>
|
</a>
|
||||||
{:else}
|
{:else}
|
||||||
<h1 class="pb-3.5 text-2xl">
|
|
||||||
Welcome back, <span class="font-bold">{$account?.name}</span>
|
|
||||||
.
|
|
||||||
</h1>
|
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
{#await overview}
|
{#await overview}
|
||||||
<span>Loading...</span>
|
<span>Loading...</span>
|
||||||
|
|
Loading…
Reference in a new issue