From 77816595f5d7771dee3d3a825099bc7224c3252f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sof=C3=ADa=20Aritz?= Date: Wed, 26 Jun 2024 00:29:26 +0200 Subject: [PATCH] more progress yipee --- asset-api/index.js | 15 +- identity-api/.env.example | 5 + identity-api/.gitignore | 3 +- identity-api/index.js | 266 ------------------ identity-api/package.json | 8 +- identity-api/src/app.js | 5 + identity-api/src/auth.js | 159 +++++++++++ identity-api/src/consts.js | 18 ++ identity-api/src/index.js | 226 +++++++++++++++ identity-api/src/m2m.js | 76 +++++ identity-api/yarn.lock | 18 +- identity-web/package.json | 4 +- identity-web/src/lib/api.ts | 34 ++- identity-web/src/lib/entry.ts | 24 +- identity-web/src/lib/stores.ts | 14 +- .../src/routes/dashboard/+page.svelte | 156 +++------- .../src/routes/dashboard/Entries.svelte | 9 +- .../src/routes/dashboard/Overview.svelte | 26 ++ .../dashboard/utils/AssetPreview.svelte | 32 +++ .../src/routes/dashboard/utils/Entry.svelte | 4 +- .../routes/dashboard/utils/FeelingPill.svelte | 6 +- .../dashboard/utils/OverviewEntry.svelte | 22 ++ .../src/routes/entry/new/+page.svelte | 250 ++++++++++++++++ identity-web/yarn.lock | 20 ++ 24 files changed, 989 insertions(+), 411 deletions(-) create mode 100644 identity-api/.env.example delete mode 100644 identity-api/index.js create mode 100644 identity-api/src/app.js create mode 100644 identity-api/src/auth.js create mode 100644 identity-api/src/consts.js create mode 100644 identity-api/src/index.js create mode 100644 identity-api/src/m2m.js create mode 100644 identity-web/src/routes/dashboard/Overview.svelte create mode 100644 identity-web/src/routes/dashboard/utils/AssetPreview.svelte create mode 100644 identity-web/src/routes/dashboard/utils/OverviewEntry.svelte create mode 100644 identity-web/src/routes/entry/new/+page.svelte diff --git a/asset-api/index.js b/asset-api/index.js index 29417b2..a56965e 100644 --- a/asset-api/index.js +++ b/asset-api/index.js @@ -39,8 +39,13 @@ fastify.get("/crypto/algo", () => { }); fastify.put("/asset", { - async handler(request) { - await userFromSessionKey(request.query.session_key); + async handler(request, reply) { + let user = await userFromSessionKey(request.query.session_key); + if (user.assets.length >= user.limits.assetCount) { + reply.code(403); + return "Max asset count reached. Contact support or upgrade your plan"; + } + let file = await request.file(); let id = randomUUID().toString(); @@ -75,12 +80,18 @@ fastify.get("/asset", { async handler(request, reply) { let user = await userFromSessionKey(request.query.session_key); + if ('statusCode' in user) { + reply.code(500); + return "Something failed, please try again later" + } + if (user.assets.includes(request.query.asset_id)) { let path = join(ASSETS_FOLDER, request.query.asset_id); reply.type(mime.getType(path)); reply.send(await readFile(path)); } else { + reply.code(401); return "Not authorized"; } }, diff --git a/identity-api/.env.example b/identity-api/.env.example new file mode 100644 index 0000000..5699528 --- /dev/null +++ b/identity-api/.env.example @@ -0,0 +1,5 @@ +IDENTITY_API_LANDING_MESSAGE = "identity-api v1.0.0" +IDENTITY_API_JWT_SECRET = "cc7e0d44fd473002f1c42167459001140ec6389b7353f8088f4d9a95f2f596f2" +IDENTITY_API_JWT_ALG = "HS256" +IDENTITY_API_ASSET_API_ENDPOINT = "http://localhost:3001" +IDENTITY_API_ASSET_API_M2M_REFRESH_INTERVAL_MS = 60000 \ No newline at end of file diff --git a/identity-api/.gitignore b/identity-api/.gitignore index f2f5e07..94bf562 100644 --- a/identity-api/.gitignore +++ b/identity-api/.gitignore @@ -1,2 +1,3 @@ node_modules/ -.yarn \ No newline at end of file +.yarn +.env \ No newline at end of file diff --git a/identity-api/index.js b/identity-api/index.js deleted file mode 100644 index 23fad4b..0000000 --- a/identity-api/index.js +++ /dev/null @@ -1,266 +0,0 @@ -import { createVerify } from "node:crypto"; -import Fastify from "fastify"; -import * as Jose from "jose"; -import { v4 as uuidv4 } from "uuid"; -import assert from "node:assert"; - -const IDENTITY_API_LANDING_MESSAGE = "identity-api v1.0.0"; -const JWT_SECRET = new TextEncoder().encode("cc7e0d44fd473002f1c42167459001140ec6389b7353f8088f4d9a95f2f596f2"); -const JWT_ALG = "HS256"; - -const ASSET_API_ENDPOINT = "http://localhost:3001/"; -let ASSET_API_PUBKEY = await loadAssetPubkey(); -let ASSET_API_ALGORITHM = await loadAssetAlgo(); - -setInterval(async () => { - try { - let pubkey = await loadAssetPubkey(); - let algo = await loadAssetAlgo(); - - if (pubkey != null && algo != null) { - if (ASSET_API_PUBKEY !== pubkey) { - console.warn("The M2M public key has changed!"); - } - - if (ASSET_API_ALGORITHM !== algo) { - console.warn("The M2M algorith has changed!"); - } - - ASSET_API_PUBKEY = pubkey; - ASSET_API_ALGORITHM = algo; - console.debug("Successfully updated the M2M public key and algorithm"); - } - } catch (e) { - console.warn("Failed to update the M2M public key", e); - } -}, 60 * 1000); - -let session_keys = { - "uid:005d6417-a23c-48bd-b348-eafeae649b94": "e381ba8c-e18a-4bca-afce-b212b37bc26b", - "key:e381ba8c-e18a-4bca-afce-b212b37bc26b": "005d6417-a23c-48bd-b348-eafeae649b94", -}; - -let users = { - "jane@identity.net": { - uid: "005d6417-a23c-48bd-b348-eafeae649b94", - email: "jane@identity.net", - password: "12345678901234567890", - name: "Jane Doe", - assets: ["f9d040d6-598c-4483-952f-08e7d35d5420.jpg"], - }, -}; - -const fastify = Fastify({ - logger: true, -}); - -fastify.get("/", async () => { - return IDENTITY_API_LANDING_MESSAGE; -}); - -fastify.put("/m2m/asset", { - async handler(request, reply) { - if (!verifySignature(request.body, ASSET_API_PUBKEY)) { - reply.statusCode(401); - return; - } - - let body = JSON.parse(getContentFromSigned(request.body)); - - let uid = session_keys[`key:${body.session_key}`]; - let user = Object.values(users).filter((v) => v.uid === uid); - assert(user.length === 1); - - users[user[0].email].assets.push(body.asset_id); - console.log(users[user[0].email].assets); - }, -}); - -fastify.post("/m2m/account", { - async handler(request, reply) { - if (!verifySignature(request.body, ASSET_API_PUBKEY)) { - reply.statusCode(401); - return; - } - - let body = JSON.parse(getContentFromSigned(request.body)); - - let uid = session_keys[`key:${body.session_key}`]; - let user = Object.values(users).filter((v) => v.uid === uid); - - assert(user.length === 1); - user[0].password = undefined; - - return user[0]; - }, -}); - -fastify.get("/auth/genkey", { - async handler(request) { - let jwt = request.headers["authorization"].replace("Bearer", "").trim(); - let { payload } = await Jose.jwtVerify(jwt, JWT_SECRET); - - let key = uuidv4(); - session_keys[`uid:${payload.uid}`] = key; - session_keys[`key:${key}`] = payload.uid; - - return { - session_key: session_keys[payload.uid], - }; - }, - schema: { - headers: { - type: "object", - properties: { - authorization: { type: "string" }, - }, - }, - }, -}); - -fastify.get("/auth/account", { - async handler(request) { - let jwt = request.headers["authorization"].replace("Bearer", "").trim(); - let { payload } = await Jose.jwtVerify(jwt, JWT_SECRET); - - let user = users[payload.email]; - user.password = undefined; - return user; - }, - schema: { - headers: { - type: "object", - properties: { - authorization: { type: "string" }, - }, - }, - }, -}); - -fastify.post("/auth/login", { - async handler(request, reply) { - let user = users[request.body.email]; - if (user != null && user.password == request.body.password) { - let token = await new Jose.SignJWT({ - uid: user.uid, - email: request.body.email, - name: user.name, - }) - .setProtectedHeader({ alg: JWT_ALG }) - .setIssuedAt() - .setExpirationTime("4w") - .sign(JWT_SECRET); - - return { - token, - }; - } - - reply.code(400); - return { - error: "invalid credentials", - }; - }, - schema: { - body: { - type: "object", - properties: { - email: { type: "string" }, - password: { type: "string" }, - }, - }, - }, -}); - -fastify.post("/auth/register", { - async handler(request, reply) { - if (users[request.body.email] == null) { - users[request.body.email] = { - uid: uuidv4(), - email: request.body.email, - password: request.body.password, - name: request.body.name, - assets: [], - }; - - let user = users[request.body.email]; - let token = await new Jose.SignJWT({ - uid: user.uid, - email: request.body.email, - name: user.name, - }) - .setProtectedHeader({ alg: JWT_ALG }) - .setIssuedAt() - .setExpirationTime("4w") - .sign(JWT_SECRET); - - return { token }; - } - - reply.code(400); - return { - error: "invalid data", - }; - }, - schema: { - body: { - type: "object", - properties: { - name: { type: "string" }, - email: { type: "string" }, - password: { type: "string" }, - }, - }, - }, -}); - -fastify.listen({ port: 3000 }); - -async function loadAssetPubkey() { - let url = new URL(ASSET_API_ENDPOINT); - url.pathname = "/crypto/cert"; - - let res = await fetch(url); - return await res.text(); -} - -async function loadAssetAlgo() { - let url = new URL(ASSET_API_ENDPOINT); - url.pathname = "/crypto/algo"; - - let res = await fetch(url); - return await res.text(); -} - -/** - * - * @param {string} content - * @param {string} pubkeyText - */ -function verifySignature(content, pubkeyText) { - let parts = content - .replace("-----BEGIN SIGNED MESSAGE-----\n\n", "") - .replace("\n-----END SIGNATURE-----", "") - .split("\n\n-----BEGIN SIGNATURE-----\n\n"); - - assert(parts.length === 2); - - let verify = createVerify(ASSET_API_ALGORITHM); - verify.update(parts[0]); - - let pubkey = Buffer.from(pubkeyText, "ascii"); - let digest = Buffer.from(parts[1], "base64"); - - return verify.verify(pubkey, digest); -} - -function getContentFromSigned(content) { - let parts = content - .replace("-----BEGIN SIGNED MESSAGE-----\n\n", "") - .replace("\n-----END SIGNATURE-----", "") - .split("\n\n-----BEGIN SIGNATURE-----\n\n"); - - assert(parts.length === 2); - - return parts[0]; -} diff --git a/identity-api/package.json b/identity-api/package.json index 5e3cb01..d681160 100644 --- a/identity-api/package.json +++ b/identity-api/package.json @@ -1,18 +1,18 @@ { "name": "identity-api", "version": "1.0.0", - "main": "index.js", + "main": "src/index.js", "license": "MIT", "private": true, "type": "module", "packageManager": "yarn@4.3.0", "dependencies": { + "dotenv": "^16.4.5", "fastify": "^4.27.0", - "jose": "^5.4.0", - "uuid": "^10.0.0" + "jose": "^5.4.0" }, "scripts": { - "start": "node index.js", + "start": "node src/index.js", "lint:fix": "eslint --fix && prettier . --write", "lint": "eslint && prettier . --check" }, diff --git a/identity-api/src/app.js b/identity-api/src/app.js new file mode 100644 index 0000000..ca6dc4c --- /dev/null +++ b/identity-api/src/app.js @@ -0,0 +1,5 @@ +import Fastify from "fastify"; + +export default Fastify({ + logger: true, +}); \ No newline at end of file diff --git a/identity-api/src/auth.js b/identity-api/src/auth.js new file mode 100644 index 0000000..bcd5b18 --- /dev/null +++ b/identity-api/src/auth.js @@ -0,0 +1,159 @@ +import assert from "node:assert"; +import { randomUUID } from "node:crypto"; +import * as Jose from "jose"; +import { JWT_ALG, JWT_SECRET } from "./consts.js"; + +export async function startAuth() { + let session_keys = { + "uid:005d6417-a23c-48bd-b348-eafeae649b94": "e381ba8c-e18a-4bca-afce-b212b37bc26b", + "key:e381ba8c-e18a-4bca-afce-b212b37bc26b": "005d6417-a23c-48bd-b348-eafeae649b94", + }; + + let users = { + "jane@identity.net": { + uid: "005d6417-a23c-48bd-b348-eafeae649b94", + email: "jane@identity.net", + password: "12345678901234567890", + name: "Jane Doe", + assets: ["f9d040d6-598c-4483-952f-08e7d35d5420.jpg"], + limits: { + assetCount: 5, + }, + entries: [ + { + id: "0", + creationDate: new Date("2024-04-13"), + feelings: ["active", "happy"], + base: { + kind: "song", + id: [{ provider: "spotify", id: "53mChDyESfwn9Dz8poHRf6" }], + link: ["https://open.spotify.com/track/53mChDyESfwn9Dz8poHRf6"], + title: "Taking What's Not Yours", + artist: "TV Girl", + }, + assets: [], + }, + { + id: "1", + creationDate: new Date("2024-04-13"), + feelings: [], + base: { + kind: "album", + id: [{ provider: "spotify", id: "1d2PspdXmwrBEcOtquCvzT" }], + link: ["https://open.spotify.com/album/1d2PspdXmwrBEcOtquCvzT"], + title: "CHASER", + artist: "femtanyl", + }, + assets: [], + }, + { + id: "2", + creationDate: new Date("2024-04-26"), + feelings: ["excited"], + title: "SalmorejoTech 2024", + description: "SalmorejoTech is a great tech-event. I met some people and everything went great! :)", + base: { + kind: "event", + }, + assets: [], + }, + { + id: "3", + creationDate: new Date("2024-06-26"), + feelings: ["happy", "relaxed"], + //title: "At the sunflower field with Ms. Violet", + title: "Playing Minecraft with Mr. Pablo", + //description: "Ms. Violet is my friend, she is a great friend. We spent a good time at the sunflower field. I am lucky to have a friend like her.", + description: "Mr. Pablo is my friend, she is a great friend. We spent a good time playing Minecraft. I am lucky to have a friend like him.", + base: { + kind: "memory", + }, + assets: [], + }, + { + id: "4", + creationDate: new Date("2024-01-01"), + feelings: ["excited", "nervous"], + description: "New Year, New me! I'm really excited about what's going to happen this year, lots of changes. Changes may be scary, but they usually are for good!", + base: { + kind: "feeling", + }, + assets: ["f9d040d6-598c-4483-952f-08e7d35d5420.jpg"], + }, + { + id: "5", + creationDate: new Date("2024-04-28"), + feelings: ["happy", "relaxed"], + title: "The park", + description: "The park is a really chill place where I can go and relax for a bit before going to work.", + base: { + kind: "environment", + }, + assets: [], + }, + { + id: "6", + creationDate: new Date("2024-04-28"), + feelings: ["happy"], + description: "This day has been a great day! I've talked with my friends.", + base: { + kind: "date", + referencedDate: new Date("2024-04-27"), + }, + assets: [], + } + ], + }, + }; + + let funcs = { + user: async (uid) => { + let user = Object.values(users).filter((v) => v.uid === uid); + assert(user.length <= 1); + + return structuredClone(user[0]); + }, + findUserByEmail: async (email) => { + return structuredClone(users[email]); + }, + findUserBySessionKey: async (session_key) => { + let uid = session_keys[`key:${session_key}`]; + return await funcs.user(uid); + }, + updateUser: async (uid, newUser) => { + let user = await funcs.user(uid); + users[user.email] = newUser; + }, + addUser: async (user) => { + user.uid = randomUUID().toString(); + users[user.email] = user; + + return structuredClone(users[user.email]); + }, + createSessionKey: async (uid) => { + let key = randomUUID().toString(); + session_keys[`uid:${uid}`] = key; + session_keys[`key:${key}`] = uid; + + return key + }, + createJwt: async (uid) => { + let user = await funcs.user(uid) + + return await new Jose.SignJWT({ + uid: user.uid, + email: user.email, + name: user.name, + }) + .setProtectedHeader({ alg: JWT_ALG }) + .setIssuedAt() + .setExpirationTime("4w") + .sign(JWT_SECRET); + }, + verifyJwt: async (jwt) => { + return await Jose.jwtVerify(jwt, JWT_SECRET); + }, + } + + return funcs; +} \ No newline at end of file diff --git a/identity-api/src/consts.js b/identity-api/src/consts.js new file mode 100644 index 0000000..9af9d4e --- /dev/null +++ b/identity-api/src/consts.js @@ -0,0 +1,18 @@ +import "dotenv/config" +import app from "./app.js"; + +const REQUIRED_VARS = ["IDENTITY_API_JWT_SECRET", "IDENTITY_API_ASSET_API_ENDPOINT", "IDENTITY_API_JWT_ALG"]; + +REQUIRED_VARS.forEach(element => { + if (process.env[element] == null || (typeof process.env[element] === "string" && process.env[element].length === 0)) { + app.log.error(`Required environment variable was not set: ${element}`); + app.close().then(() => process.exit(1)) + } +}); + +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 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 \ No newline at end of file diff --git a/identity-api/src/index.js b/identity-api/src/index.js new file mode 100644 index 0000000..94eee26 --- /dev/null +++ b/identity-api/src/index.js @@ -0,0 +1,226 @@ +import app from "./app.js" +import { ASSET_API_ENDPOINT, IDENTITY_API_LANDING_MESSAGE, LISTEN_PORT } from "./consts.js" +import { contentFromSigned, verifySignature } from "./m2m.js"; +import { startAuth } from "./auth.js"; +import { randomUUID } from "node:crypto"; + +let auth = await startAuth(); + +app.get("/", async () => { + return IDENTITY_API_LANDING_MESSAGE; +}); + +app.put("/m2m/asset", { + async handler(request, reply) { + if (!verifySignature(request.body)) { + reply.statusCode(401); + return; + } + + let body = JSON.parse(contentFromSigned(request.body)); + + let user = await auth.findUserBySessionKey(body.session_key) + user.assets.push(body.asset_id); + + await auth.updateUser(user.uid, user); + app.log.info((await auth.findUserBySessionKey(body.session_key)).assets); + }, +}); + +app.post("/m2m/account", { + async handler(request, reply) { + if (!verifySignature(request.body)) { + reply.statusCode(401); + return; + } + + let body = JSON.parse(contentFromSigned(request.body)); + + let user = await auth.findUserBySessionKey(body.session_key) + user.password = undefined; + user.entries = undefined; + + return user; + }, +}); + +app.get("/asset/endpoint", { + async handler() { + return ASSET_API_ENDPOINT + } +}) + +app.put("/entry", { + async handler(request, reply) { + 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: { + type: "object", + properties: { + authorization: { type: "string" }, + }, + required: ["authorization"], + }, + body: { + type: "object", + properties: { + entry: { type: "object" }, + }, + required: ["entry"], + }, + }, +}) + +app.get("/entry/list", { + async handler(request, reply) { + if (request.query.offset < 0 || request.query.limit <= 0) { + reply.status(400); + return []; + } + + 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); + }, + schema: { + headers: { + type: "object", + properties: { + authorization: { type: "string" }, + }, + required: ["authorization"], + }, + query: { + type: "object", + properties: { + limit: { type: "number" }, + offset: { type: "number" }, + }, + required: ["limit", "offset"], + }, + }, +}) + +app.get("/auth/genkey", { + async handler(request) { + let jwt = request.headers["authorization"].replace("Bearer", "").trim(); + let { payload } = await auth.verifyJwt(jwt); + + let session_key = await auth.createSessionKey(payload.uid); + + return { + session_key, + }; + }, + schema: { + headers: { + type: "object", + properties: { + authorization: { type: "string" }, + }, + required: ["authorization"], + }, + }, +}); + +app.get("/auth/account", { + 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.password = undefined; + user.entries = undefined; + return user; + }, + schema: { + headers: { + type: "object", + properties: { + authorization: { type: "string" }, + }, + required: ["authorization"], + }, + }, +}); + +app.post("/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); + + return { + token, + }; + } + + reply.code(400); + return { + error: "invalid credentials", + }; + }, + schema: { + body: { + type: "object", + properties: { + email: { type: "string" }, + password: { type: "string" }, + }, + }, + required: ["email", "password"], + }, +}); + +app.post("/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) }; + } + + reply.code(400); + return { + error: "invalid data", + }; + }, + schema: { + body: { + type: "object", + properties: { + name: { type: "string" }, + email: { type: "string" }, + password: { type: "string" }, + }, + required: ["name", "email", "password"], + }, + }, +}); + +app.listen({ port: LISTEN_PORT }); diff --git a/identity-api/src/m2m.js b/identity-api/src/m2m.js new file mode 100644 index 0000000..f17925a --- /dev/null +++ b/identity-api/src/m2m.js @@ -0,0 +1,76 @@ +import { createVerify } from "node:crypto"; +import assert from "node:assert" +import app from "./app.js"; +import { ASSET_API_ENDPOINT, ASSET_API_M2M_REFRESH_INTERVAL } from "./consts.js"; + +let assetPubKey = await fetchAssetPubkey() +let assetAlgorithm = await fetchAssetAlgorithm() + +setInterval(async () => { + try { + let pubkey = await fetchAssetPubkey(); + let algo = await fetchAssetAlgorithm(); + + if (pubkey != null && algo != null) { + if (assetPubKey !== pubkey) { + app.log.warn("The M2M public key has changed!") + } + + if (assetAlgorithm !== algo) { + app.log.warn("The M2M algorith has changed!"); + } + + assetPubKey = pubkey; + assetAlgorithm = algo; + app.log.debug("Successfully updated the M2M credentials"); + } else { + app.log.warn("Failed to retrieve the M2M credentials"); + } + } catch (e) { + app.log.warn("Failed to update the M2M credentials"); + app.log.warn(e); + } +}, ASSET_API_M2M_REFRESH_INTERVAL) + +async function fetchAssetPubkey() { + let url = new URL(ASSET_API_ENDPOINT); + url.pathname = "/crypto/cert"; + + let res = await fetch(url); + return await res.text(); +} + +async function fetchAssetAlgorithm() { + let url = new URL(ASSET_API_ENDPOINT); + url.pathname = "/crypto/algo"; + + let res = await fetch(url); + return await res.text(); +} + +function partsFromSigned(content) { + let parts = content + .replace("-----BEGIN SIGNED MESSAGE-----\n\n", "") + .replace("\n-----END SIGNATURE-----", "") + .split("\n\n-----BEGIN SIGNATURE-----\n\n"); + + assert(parts.length === 2); + + return parts +} + +export function verifySignature(content) { + let parts = partsFromSigned(content) + + let verify = createVerify(assetAlgorithm); + verify.update(parts[0]); + + let pubkey = Buffer.from(assetPubKey, "ascii"); + let digest = Buffer.from(parts[1], "base64"); + + return verify.verify(pubkey, digest); +} + +export function contentFromSigned(content) { + return partsFromSigned(content)[0]; +} \ No newline at end of file diff --git a/identity-api/yarn.lock b/identity-api/yarn.lock index cf32065..0a3790b 100644 --- a/identity-api/yarn.lock +++ b/identity-api/yarn.lock @@ -379,6 +379,13 @@ __metadata: languageName: node linkType: hard +"dotenv@npm:^16.4.5": + version: 16.4.5 + resolution: "dotenv@npm:16.4.5" + checksum: 10c0/48d92870076832af0418b13acd6e5a5a3e83bb00df690d9812e94b24aff62b88ade955ac99a05501305b8dc8f1b0ee7638b18493deb6fe93d680e5220936292f + languageName: node + linkType: hard + "escape-string-regexp@npm:^4.0.0": version: 4.0.0 resolution: "escape-string-regexp@npm:4.0.0" @@ -706,12 +713,12 @@ __metadata: resolution: "identity-api@workspace:." dependencies: "@eslint/js": "npm:^9.5.0" + dotenv: "npm:^16.4.5" eslint: "npm:9.x" fastify: "npm:^4.27.0" globals: "npm:^15.5.0" jose: "npm:^5.4.0" prettier: "npm:3.3.2" - uuid: "npm:^10.0.0" languageName: unknown linkType: soft @@ -1285,15 +1292,6 @@ __metadata: languageName: node linkType: hard -"uuid@npm:^10.0.0": - version: 10.0.0 - resolution: "uuid@npm:10.0.0" - bin: - uuid: dist/bin/uuid - checksum: 10c0/eab18c27fe4ab9fb9709a5d5f40119b45f2ec8314f8d4cf12ce27e4c6f4ffa4a6321dc7db6c515068fa373c075b49691ba969f0010bf37f44c37ca40cd6bf7fe - languageName: node - linkType: hard - "which@npm:^2.0.1": version: 2.0.2 resolution: "which@npm:2.0.2" diff --git a/identity-web/package.json b/identity-web/package.json index 52a094b..282d4b3 100644 --- a/identity-web/package.json +++ b/identity-web/package.json @@ -36,8 +36,10 @@ "type": "module", "dependencies": { "@fortawesome/fontawesome-svg-core": "^6.5.2", + "@fortawesome/free-brands-svg-icons": "^6.5.2", "@fortawesome/free-solid-svg-icons": "^6.5.2", "@fortawesome/svelte-fontawesome": "^0.2.2", - "felte": "^1.2.14" + "felte": "^1.2.14", + "mime": "^4.0.3" } } diff --git a/identity-web/src/lib/api.ts b/identity-web/src/lib/api.ts index 87cf58b..9ad31c9 100644 --- a/identity-web/src/lib/api.ts +++ b/identity-web/src/lib/api.ts @@ -1,3 +1,5 @@ +import type { Entry, IdlessEntry } from "./entry" + const ENDPOINT = 'http://localhost:3000/' export type Credentials = { @@ -9,13 +11,14 @@ export type Account = { name: string, } -function sendRequest(path: string, request: RequestInit = {}, credentials?: Credentials) { +function sendRequest(path: string, credentials?: Credentials, request: RequestInit = {}, params: string = "") { if (typeof request !== "string" && credentials != null) { request.headers = { 'Authorization': `Bearer ${credentials.token}`, ...request.headers } } let url = new URL(ENDPOINT); url.pathname = path; + url.search = params return fetch(url, request) } @@ -30,7 +33,7 @@ export function login(credentials: { email: string, password: string, }): Promise<{ token: string, } | { error: string, }> { - return asJson(sendRequest('/auth/login', { + return asJson(sendRequest('/auth/login', undefined, { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -44,7 +47,7 @@ export function register(credentials: { email: string, password: string, }): Promise<{ token: string, } | { error: string, }> { - return asJson(sendRequest('/auth/register', { + return asJson(sendRequest('/auth/register', undefined, { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -54,5 +57,28 @@ export function register(credentials: { } export function accountData(credentials: Credentials): Promise { - return asJson(sendRequest('/auth/account', undefined, credentials)) + return asJson(sendRequest('/auth/account', credentials)) +} + +export function genSessionKey(credentials: Credentials): Promise<{ session_key: string } | { error: string }> { + return asJson(sendRequest('/auth/genkey', credentials)) +} + +export async function assetEndpoint(): Promise { + let res = await sendRequest("/asset/endpoint") + return res.text() +} + +export async function entryPage(credentials: Credentials, offset: number, limit: number): Promise { + return asJson(sendRequest('/entry/list', credentials, undefined, `?offset=${offset}&limit=${limit}`)) +} + +export async function addEntry(credentials: Credentials, entry: IdlessEntry): Promise { + await sendRequest('/entry', credentials, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({entry}), + }) } \ No newline at end of file diff --git a/identity-web/src/lib/entry.ts b/identity-web/src/lib/entry.ts index 9b8bf29..d64b282 100644 --- a/identity-web/src/lib/entry.ts +++ b/identity-web/src/lib/entry.ts @@ -1,17 +1,33 @@ export const TITLED_ENTRIES = ["event", "environment", "memory"]; +export const FEELINGS = ["relaxed", "afraid", "angry", "bad", "bored", "confused", "excited", "fine", "happy", "hurt", "in love", "mad", "nervous", "okay", "sad", "scared", "shy", "sleepy", "active", "surprised", "tired", "upset", "worried"]; export type KnownFeeling = "relaxed" | "afraid" | "angry" | "bad" | "bored" | "confused" | "excited" | "fine" | "happy" | "hurt" | "in love" | "mad" | "nervous" | "okay" | "sad" | "scared" | "shy" | "sleepy" | "active" | "surprised" | "tired" | "upset" | "worried"; -export type Entry = { - id: string, +export type IdlessEntry = { base: SongEntry | AlbumEntry | EventEntry | MemoryEntry | FeelingEntry | EnvironmentEntry | DateEntry, - creationDate: Date, + creationDate: string, feelings: (KnownFeeling | { identifier: string, description: string, backgroundColor: string, textColor: string, })[], + assets: string[], + title?: string, + description?: string, +}; + +export type Entry = { + id: string, + base: SongEntry | AlbumEntry | EventEntry | MemoryEntry | FeelingEntry | EnvironmentEntry | DateEntry, + creationDate: string, + feelings: (KnownFeeling | { + identifier: string, + description: string, + backgroundColor: string, + textColor: string, + })[], + assets: string[], title?: string, description?: string, }; @@ -59,5 +75,5 @@ export type EnvironmentEntry = { export type DateEntry = { kind: "date", - referencedDate: Date, + referencedDate: string, } \ No newline at end of file diff --git a/identity-web/src/lib/stores.ts b/identity-web/src/lib/stores.ts index 9c90f53..14817c0 100644 --- a/identity-web/src/lib/stores.ts +++ b/identity-web/src/lib/stores.ts @@ -1,5 +1,5 @@ import { writable } from "svelte/store"; -import { accountData, type Account, type Credentials } from "./api"; +import { accountData, assetEndpoint, genSessionKey, type Account, type Credentials } from "./api"; const CREDENTIALS_KEY = 'v0:credentials' @@ -11,6 +11,8 @@ credentials.subscribe((value) => { }) export const account = writable() +export const session_key = writable() +export const asset_endpoint = writable() export async function initializeStores() { let rawCredentials = localStorage.getItem(CREDENTIALS_KEY) @@ -31,5 +33,15 @@ export async function initializeStores() { } else { account.set(data) } + + let key_result = await genSessionKey(parsedCredentials) + if ('error' in key_result) { + console.warn('Couldn\'t generate a session key!') + } else { + session_key.set(key_result.session_key) + } + + let asset_result = await assetEndpoint() + asset_endpoint.set(asset_result) } } \ No newline at end of file diff --git a/identity-web/src/routes/dashboard/+page.svelte b/identity-web/src/routes/dashboard/+page.svelte index 00031d6..a080944 100644 --- a/identity-web/src/routes/dashboard/+page.svelte +++ b/identity-web/src/routes/dashboard/+page.svelte @@ -1,122 +1,52 @@ -
-
-

Welcome back, {$account?.name}.

-
-
-

Latest activity

-
-

New song: TV Girl ‐ Taking what's not yours

-

New album: femtanyl ‐ CHASER

-

New memory: § At the sunflower field with Ms. Violet

-
-
-
-

Memories from the past

-
-

§ 2024 Birthday

-

New song: Hayes Carll ‐ KMAG YOYO

-

§ A new era

-
+{#if $account != null} + {#await entries} +
+
+ Loading entries... +
+ {:then entries} +
+
+ {#if entries.length === 0} + + + +

Add an entry

+
+ {:else} +

Welcome back, {$account?.name}.

+
+ {#await overview} + Loading... + {:then overview} + !["feeling"].includes(v.base.kind))} past={overview[1].value}/> + {/await} +
-
-

Memories

- + Add a memory +
+

Memories

+ + Add an entry +
+
+ +
+ {/if} +
-
- - -
-
-
\ No newline at end of file + {/await} +{/if} \ No newline at end of file diff --git a/identity-web/src/routes/dashboard/Entries.svelte b/identity-web/src/routes/dashboard/Entries.svelte index 8b4b3f5..bb7b963 100644 --- a/identity-web/src/routes/dashboard/Entries.svelte +++ b/identity-web/src/routes/dashboard/Entries.svelte @@ -4,12 +4,13 @@ import FeelingPill from "./utils/FeelingPill.svelte"; import Entry from "./utils/Entry.svelte"; import EntryDescription from "./utils/EntryDescription.svelte"; + import AssetPreview from "./utils/AssetPreview.svelte"; export let entries: EntryType[] {#each entries as entry (entry.id)} - +
{#if entry.base.kind === "song" || entry.base.kind === "album"} {entry.base.artist} ‐ {entry.base.title} @@ -46,6 +47,12 @@ {#if entry.description != null} {entry.description} {/if} + +
+ {#each entry.assets as asset} + + {/each} +
{/each} \ No newline at end of file diff --git a/identity-web/src/routes/dashboard/Overview.svelte b/identity-web/src/routes/dashboard/Overview.svelte new file mode 100644 index 0000000..980743f --- /dev/null +++ b/identity-web/src/routes/dashboard/Overview.svelte @@ -0,0 +1,26 @@ + + +
+

Latest activity

+
+ {#each latest as entry (entry.id)} + + {/each} +
+
+{#if past.length > 0} +
+

Memories from the past

+
+ {#each past as entry (entry.id)} + + {/each} +
+
+{/if} \ No newline at end of file diff --git a/identity-web/src/routes/dashboard/utils/AssetPreview.svelte b/identity-web/src/routes/dashboard/utils/AssetPreview.svelte new file mode 100644 index 0000000..07aa22f --- /dev/null +++ b/identity-web/src/routes/dashboard/utils/AssetPreview.svelte @@ -0,0 +1,32 @@ + + + + {#if kind == null} + + {:else if kind === "image"} + + {:else if kind === "audio"} + + {:else if kind === "video"} + + {:else} + + {/if} + + {#if kind != null && kind !== "application"} + {kind} + {:else} + Asset + {/if} + \ No newline at end of file diff --git a/identity-web/src/routes/dashboard/utils/Entry.svelte b/identity-web/src/routes/dashboard/utils/Entry.svelte index 3972978..192578f 100644 --- a/identity-web/src/routes/dashboard/utils/Entry.svelte +++ b/identity-web/src/routes/dashboard/utils/Entry.svelte @@ -2,6 +2,7 @@ import { TITLED_ENTRIES } from "$lib/entry"; import EntryKind from "./EntryKind.svelte"; + export let id: string; export let creationDate: Date; export let kind: "song" | "album" | "event" | "feeling" | "environment" | "date" | "memory"; export let title: string | undefined; @@ -21,12 +22,11 @@ } } - console.log(cardClass) return cardClass }; -
+
+ + {#if chosenFeelings.length > 0} +
+ Chosen: + {#each chosenFeelings as feeling (feeling)} +
+ + +
+ {/each} +
+ {:else} + No feelings chosen. + {#if kind === "feeling"} + You need to choose at least one feeling. + {/if} + {/if} +
+
+ {#each feelingsToChoose as feeling (feeling)} + + {/each} +
+ {#if $errors.feelings != null} +

{$errors.feelings[0]}

+ {/if} +
+ + + {/if} + +
+
\ No newline at end of file diff --git a/identity-web/yarn.lock b/identity-web/yarn.lock index ee4726a..9615197 100644 --- a/identity-web/yarn.lock +++ b/identity-web/yarn.lock @@ -275,6 +275,15 @@ __metadata: languageName: node linkType: hard +"@fortawesome/free-brands-svg-icons@npm:^6.5.2": + version: 6.5.2 + resolution: "@fortawesome/free-brands-svg-icons@npm:6.5.2" + dependencies: + "@fortawesome/fontawesome-common-types": "npm:6.5.2" + checksum: 10c0/4c1798930547a73bf9dfd04064288d4ce02fbf42fece056c1f5dd6596363d6a34bb86bc2e0f400b78f938f48773aceb375bc6977e55cbaec9b7bd07996ae1e8c + languageName: node + linkType: hard + "@fortawesome/free-solid-svg-icons@npm:^6.5.2": version: 6.5.2 resolution: "@fortawesome/free-solid-svg-icons@npm:6.5.2" @@ -1899,6 +1908,7 @@ __metadata: resolution: "identity-web@workspace:." dependencies: "@fortawesome/fontawesome-svg-core": "npm:^6.5.2" + "@fortawesome/free-brands-svg-icons": "npm:^6.5.2" "@fortawesome/free-solid-svg-icons": "npm:^6.5.2" "@fortawesome/svelte-fontawesome": "npm:^0.2.2" "@sveltejs/adapter-auto": "npm:^3.0.0" @@ -1911,6 +1921,7 @@ __metadata: eslint-plugin-svelte: "npm:^2.36.0" felte: "npm:^1.2.14" globals: "npm:^15.0.0" + mime: "npm:^4.0.3" postcss: "npm:^8.4.38" prettier: "npm:^3.1.1" prettier-plugin-svelte: "npm:^3.1.2" @@ -2272,6 +2283,15 @@ __metadata: languageName: node linkType: hard +"mime@npm:^4.0.3": + version: 4.0.3 + resolution: "mime@npm:4.0.3" + bin: + mime: bin/cli.js + checksum: 10c0/4be1d06813a581eb9634748919eadab9785857dcfe2af4acca8e4bc340b4b74ff7452c7d3cd76169d0f6b77d7f1ab3434bde8a72ca4291fd150b4205c756c36b + languageName: node + linkType: hard + "min-indent@npm:^1.0.0": version: 1.0.1 resolution: "min-indent@npm:1.0.1"