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", {
|
||||
async handler(request, reply) {
|
||||
let user = await userFromSessionKey(request.query.session_key);
|
||||
if (user.assets.length >= user.limits.assetCount) {
|
||||
let { user, limits } = await userFromSessionKey(request.query.session_key);
|
||||
if (user.assets.length >= limits.maxAssetCount) {
|
||||
reply.code(403);
|
||||
return "Max asset count reached. Contact support or upgrade your plan";
|
||||
}
|
||||
|
@ -102,7 +102,7 @@ app.put("/asset", {
|
|||
|
||||
app.get("/asset", {
|
||||
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) {
|
||||
reply.code(500);
|
||||
|
|
|
@ -3,3 +3,4 @@ IDENTITY_API_JWT_SECRET = "cc7e0d44fd473002f1c42167459001140ec6389b7353f8088f4d9
|
|||
IDENTITY_API_JWT_ALG = "HS256"
|
||||
IDENTITY_API_ASSET_API_ENDPOINT = "http://localhost:3001"
|
||||
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/
|
||||
dist/
|
||||
.yarn
|
||||
.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",
|
||||
"dependencies": {
|
||||
"@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",
|
||||
"drizzle-orm": "^0.31.2",
|
||||
"fastify": "^4.27.0",
|
||||
"jose": "^5.4.0"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "node src/index.js",
|
||||
"start": "tsc && node dist/index.js",
|
||||
"lint:fix": "eslint --fix && prettier . --write",
|
||||
"lint": "eslint && prettier . --check"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.5.0",
|
||||
"@types/node": "^20.14.9",
|
||||
"eslint": "9.x",
|
||||
"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
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
import { TypeBoxTypeProvider } from "@fastify/type-provider-typebox";
|
||||
import Fastify from "fastify";
|
||||
|
||||
export default Fastify({
|
||||
let app = Fastify({
|
||||
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 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) => {
|
||||
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 JWT_SECRET = new TextEncoder().encode(process.env["IDENTITY_API_JWT_SECRET"]);
|
||||
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_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 { 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({
|
||||
$id: "schema://identity/authorization",
|
||||
|
@ -36,7 +38,7 @@ app.register(cors, {
|
|||
origin: true,
|
||||
});
|
||||
|
||||
registerRoutes(app, auth);
|
||||
registerRoutes(app, auth, database);
|
||||
|
||||
app.get("/", async () => {
|
||||
return IDENTITY_API_LANDING_MESSAGE;
|
|
@ -14,10 +14,11 @@
|
|||
// 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 { 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", {
|
||||
async handler() {
|
||||
return ASSET_API_ENDPOINT;
|
|
@ -14,9 +14,10 @@
|
|||
// 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 type { AppInterface } from "../../app.js";
|
||||
|
||||
import endpoint from "./endpoint.js";
|
||||
|
||||
export default function registerRoutes(app, auth) {
|
||||
endpoint(app, auth);
|
||||
export default function registerRoutes(app: AppInterface) {
|
||||
endpoint(app);
|
||||
}
|
|
@ -14,8 +14,10 @@
|
|||
// 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 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", {
|
||||
async handler(request, reply) {
|
||||
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);
|
||||
user.password = undefined;
|
||||
user.entries = undefined;
|
||||
return user;
|
||||
return auth.cleanUser(user);
|
||||
},
|
||||
schema: {
|
||||
headers: { $ref: "schema://identity/authorization" },
|
|
@ -14,8 +14,10 @@
|
|||
// 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 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", {
|
||||
async handler(request) {
|
||||
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
|
||||
// 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 genkey from "./genkey.js";
|
||||
|
@ -21,10 +24,10 @@ import heirs from "./heirs.js";
|
|||
import login from "./login.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);
|
||||
genkey(app, auth);
|
||||
heirs(app, auth);
|
||||
heirs(app, auth, database);
|
||||
login(app, auth);
|
||||
register(app, auth);
|
||||
}
|
|
@ -14,14 +14,24 @@
|
|||
// 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 { Static, Type } from "@sinclair/typebox";
|
||||
import type { AppInterface } from "../../app.js";
|
||||
import type { AuthInterface } from "../../auth.js";
|
||||
|
||||
export default function register(app, auth) {
|
||||
app.post("/auth/login", {
|
||||
const Body = Type.Object({
|
||||
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) {
|
||||
let user = await auth.findUserByEmail(request.body.email);
|
||||
|
||||
if (user != null && user.password == request.body.password) {
|
||||
let token = await auth.createJwt(user.uid);
|
||||
if (user != null && auth.verifyPassword(user, request.body.password)) {
|
||||
let token = await auth.createJwt(user.id);
|
||||
|
||||
return {
|
||||
token,
|
||||
|
@ -34,14 +44,7 @@ export default function register(app, auth) {
|
|||
};
|
||||
},
|
||||
schema: {
|
||||
body: {
|
||||
type: "object",
|
||||
properties: {
|
||||
email: { type: "string" },
|
||||
password: { type: "string" },
|
||||
},
|
||||
},
|
||||
required: ["email", "password"],
|
||||
body: Body,
|
||||
},
|
||||
});
|
||||
}
|
|
@ -14,23 +14,29 @@
|
|||
// 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 { Static, Type } from "@sinclair/typebox";
|
||||
import type { AppInterface } from "../../app.js";
|
||||
import type { AuthInterface } from "../../auth.js";
|
||||
|
||||
export default function register(app, auth) {
|
||||
app.post("/auth/register", {
|
||||
const Body = Type.Object({
|
||||
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) {
|
||||
if ((await auth.findUserByEmail(request.body.email)) == null) {
|
||||
let user = await auth.addUser({
|
||||
email: request.body.email,
|
||||
password: request.body.password,
|
||||
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);
|
||||
|
@ -39,15 +45,7 @@ export default function register(app, auth) {
|
|||
};
|
||||
},
|
||||
schema: {
|
||||
body: {
|
||||
type: "object",
|
||||
properties: {
|
||||
name: { type: "string" },
|
||||
email: { type: "string" },
|
||||
password: { type: "string" },
|
||||
},
|
||||
required: ["name", "email", "password"],
|
||||
},
|
||||
body: Body,
|
||||
},
|
||||
});
|
||||
}
|
|
@ -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
|
||||
// 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) {
|
||||
app.get("/entry/list", {
|
||||
const Query = Type.Object({
|
||||
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) {
|
||||
if (request.query.offset < 0 || request.query.limit <= 0) {
|
||||
reply.status(400);
|
||||
|
@ -26,19 +37,11 @@ export default function register(app, auth) {
|
|||
let jwt = request.headers["authorization"].replace("Bearer", "").trim();
|
||||
let { payload } = await auth.verifyJwt(jwt);
|
||||
|
||||
let user = await auth.user(payload.uid);
|
||||
return user.entries.slice(request.query.offset, request.query.offset + request.query.limit);
|
||||
return await database.entryPage(payload.uid, request.query.offset, request.query.limit)
|
||||
},
|
||||
schema: {
|
||||
headers: { $ref: "schema://identity/authorization" },
|
||||
query: {
|
||||
type: "object",
|
||||
properties: {
|
||||
limit: { type: "number" },
|
||||
offset: { type: "number" },
|
||||
},
|
||||
required: ["limit", "offset"],
|
||||
},
|
||||
querystring: Query,
|
||||
},
|
||||
});
|
||||
}
|
|
@ -14,15 +14,18 @@
|
|||
// 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 m2m from "./m2m/index.js";
|
||||
import asset from "./asset/index.js";
|
||||
import entry from "./entry/index.js";
|
||||
import authRoutes from "./auth/index.js";
|
||||
|
||||
export function registerRoutes(app, auth) {
|
||||
m2m(app, auth);
|
||||
asset(app, auth);
|
||||
entry(app, auth);
|
||||
authRoutes(app, auth);
|
||||
import type { AppInterface } from "../app.js";
|
||||
import type { DatabaseInterface } from "../database.js";
|
||||
import type { AuthInterface } from "../auth.js";
|
||||
|
||||
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
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
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", {
|
||||
async handler(request, reply) {
|
||||
if (!verifySignature(request.body)) {
|
||||
reply.statusCode(401);
|
||||
reply.status(401);
|
||||
return;
|
||||
}
|
||||
|
||||
let body = JSON.parse(contentFromSigned(request.body));
|
||||
|
||||
let user = await auth.findUserBySessionKey(body.session_key);
|
||||
user.password = undefined;
|
||||
user.entries = undefined;
|
||||
let limits = await database.userLimits(user.limitID);
|
||||
(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
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
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", {
|
||||
async handler(request, reply) {
|
||||
if (!verifySignature(request.body)) {
|
||||
reply.statusCode(401);
|
||||
reply.status(401);
|
||||
return;
|
||||
}
|
||||
|
||||
let body = JSON.parse(contentFromSigned(request.body));
|
||||
|
||||
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);
|
||||
},
|
||||
});
|
|
@ -14,11 +14,14 @@
|
|||
// 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 asset from "./asset.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);
|
||||
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;
|
||||
};
|
||||
|
||||
export type InsertHeir = {
|
||||
contactMethod: 'email';
|
||||
name: string;
|
||||
value: string;
|
||||
};
|
||||
|
||||
export type AccountHeir = {
|
||||
id: string;
|
||||
contactMethod: 'email';
|
||||
name: string;
|
||||
value: string;
|
||||
|
@ -22,8 +29,8 @@ export type AccountHeir = {
|
|||
|
||||
export type Account = {
|
||||
uid: string;
|
||||
email: string;
|
||||
name: string;
|
||||
heirs: AccountHeir[];
|
||||
};
|
||||
|
||||
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}`);
|
||||
}
|
||||
|
||||
export async function updateHeirs(credentials: Credentials, heirs: AccountHeir[]): Promise<void> {
|
||||
await sendRequest('/auth/heirs', credentials, {
|
||||
method: 'POST',
|
||||
export async function insertHeir(credentials: Credentials, heirs: InsertHeir): Promise<AccountHeir[]> {
|
||||
return await asJson(sendRequest('/auth/heirs', credentials, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
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> {
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<script lang="ts">
|
||||
import { createForm } from 'felte';
|
||||
import { account, credentials, refreshAccount } from '$lib/stores';
|
||||
import { type AccountHeir, updateHeirs } from '$lib/api';
|
||||
import { type AccountHeir, insertHeir, removeHeir } from '$lib/api';
|
||||
|
||||
credentials.subscribe(
|
||||
(v) => v == null && setTimeout(() => (window.location.pathname = '/auth/login'), 200)
|
||||
|
@ -24,7 +24,7 @@
|
|||
let currentHeirs = structuredClone($account!.heirs);
|
||||
let updatedHeirs = [heir, ...currentHeirs];
|
||||
|
||||
await updateHeirs($credentials!, updatedHeirs);
|
||||
//await updateHeirs($credentials!, updatedHeirs);
|
||||
await refreshAccount();
|
||||
|
||||
heirWizard = false;
|
||||
|
|
|
@ -124,6 +124,9 @@
|
|||
{:then entries}
|
||||
<div class="mt-3.5 flex justify-center">
|
||||
<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}
|
||||
<a
|
||||
href="/entry/new"
|
||||
|
@ -133,10 +136,6 @@
|
|||
<h2 class="text-xl font-semibold">Add an entry</h2>
|
||||
</a>
|
||||
{:else}
|
||||
<h1 class="pb-3.5 text-2xl">
|
||||
Welcome back, <span class="font-bold">{$account?.name}</span>
|
||||
.
|
||||
</h1>
|
||||
<div class="flex gap-2">
|
||||
{#await overview}
|
||||
<span>Loading...</span>
|
||||
|
|
Loading…
Reference in a new issue