filesystem routing

This commit is contained in:
Sofía Aritz 2024-06-29 20:11:53 +02:00
parent 10e31e2c2f
commit 9157d247ea
Signed by: sofia
GPG key ID: 90B5116E3542B28F
19 changed files with 348 additions and 310 deletions

View file

@ -1,16 +1,16 @@
// Identity. Store your memories and mental belongings // Identity. Store your memories and mental belongings
// Copyright (C) 2024 Sofía Aritz // Copyright (C) 2024 Sofía Aritz
// //
// This program is free software: you can redistribute it and/or modify // 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 // 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 // by the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version. // (at your option) any later version.
// //
// This program is distributed in the hope that it will be useful, // This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of // but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details. // GNU Affero General Public License for more details.
// //
// 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/>.
@ -18,4 +18,4 @@ import Fastify from "fastify";
export default Fastify({ export default Fastify({
logger: true, logger: true,
}); });

File diff suppressed because one or more lines are too long

View file

@ -1,28 +1,31 @@
// Identity. Store your memories and mental belongings // Identity. Store your memories and mental belongings
// Copyright (C) 2024 Sofía Aritz // Copyright (C) 2024 Sofía Aritz
// //
// This program is free software: you can redistribute it and/or modify // 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 // 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 // by the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version. // (at your option) any later version.
// //
// This program is distributed in the hope that it will be useful, // This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of // but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details. // GNU Affero General Public License for more details.
// //
// 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 "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"];
REQUIRED_VARS.forEach(element => { REQUIRED_VARS.forEach((element) => {
if (process.env[element] == null || (typeof process.env[element] === "string" && process.env[element].length === 0)) { 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.log.error(`Required environment variable was not set: ${element}`);
app.close().then(() => process.exit(1)) app.close().then(() => process.exit(1));
} }
}); });
@ -31,4 +34,5 @@ export const JWT_SECRET = new TextEncoder().encode(process.env["IDENTITY_API_JWT
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 = 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 = process.env["IDENTITY_API_ASSET_API_M2M_REFRESH_INTERVAL_MS"] || 60 * 1000 export const ASSET_API_M2M_REFRESH_INTERVAL =
process.env["IDENTITY_API_ASSET_API_M2M_REFRESH_INTERVAL_MS"] || 60 * 1000;

View file

@ -1,304 +1,45 @@
// Identity. Store your memories and mental belongings // Identity. Store your memories and mental belongings
// Copyright (C) 2024 Sofía Aritz // Copyright (C) 2024 Sofía Aritz
// //
// This program is free software: you can redistribute it and/or modify // 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 // 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 // by the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version. // (at your option) any later version.
// //
// This program is distributed in the hope that it will be useful, // This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of // but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details. // GNU Affero General Public License for more details.
// //
// 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 app from "./app.js" import app from "./app.js";
import { ASSET_API_ENDPOINT, IDENTITY_API_LANDING_MESSAGE, LISTEN_PORT } from "./consts.js" import { IDENTITY_API_LANDING_MESSAGE, LISTEN_PORT } from "./consts.js";
import { contentFromSigned, verifySignature } from "./m2m.js";
import { startAuth } from "./auth.js"; import { startAuth } from "./auth.js";
import { randomUUID } from "node:crypto";
import cors from "@fastify/cors"; import cors from "@fastify/cors";
import { registerRoutes } from "./routes/index.js";
let auth = await startAuth(); let auth = await startAuth();
app.addSchema({
$id: "schema://identity/authorization",
type: "object",
properties: {
authorization: { type: "string" },
},
required: ["authorization"],
});
app.register(cors, { app.register(cors, {
origin: true, origin: true,
}) });
registerRoutes(app, auth);
app.get("/", async () => { app.get("/", async () => {
return IDENTITY_API_LANDING_MESSAGE; 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.delete("/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);
user.entries = user.entries.filter(v => v.id !== request.query.entry_id);
await auth.updateUser(payload.uid, user);
},
schema: {
headers: {
type: "object",
properties: {
authorization: { type: "string" },
},
required: ["authorization"],
},
query: {
type: "object",
properties: {
entry_id: { type: "string" },
},
required: ["entry_id"],
},
},
})
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.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: {
type: "object",
properties: {
authorization: { type: "string" },
},
required: ["authorization"],
},
body: {
type: "array",
}
}
})
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 }); app.listen({ port: LISTEN_PORT });

View file

@ -1,26 +1,26 @@
// Identity. Store your memories and mental belongings // Identity. Store your memories and mental belongings
// Copyright (C) 2024 Sofía Aritz // Copyright (C) 2024 Sofía Aritz
// //
// This program is free software: you can redistribute it and/or modify // 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 // 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 // by the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version. // (at your option) any later version.
// //
// This program is distributed in the hope that it will be useful, // This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of // but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details. // GNU Affero General Public License for more details.
// //
// 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 { createVerify } from "node:crypto"; import { createVerify } from "node:crypto";
import assert from "node:assert" import assert from "node:assert";
import app from "./app.js"; import app from "./app.js";
import { ASSET_API_ENDPOINT, ASSET_API_M2M_REFRESH_INTERVAL } from "./consts.js"; import { ASSET_API_ENDPOINT, ASSET_API_M2M_REFRESH_INTERVAL } from "./consts.js";
let assetPubKey = await fetchAssetPubkey() let assetPubKey = await fetchAssetPubkey();
let assetAlgorithm = await fetchAssetAlgorithm() let assetAlgorithm = await fetchAssetAlgorithm();
setInterval(async () => { setInterval(async () => {
try { try {
@ -29,7 +29,7 @@ setInterval(async () => {
if (pubkey != null && algo != null) { if (pubkey != null && algo != null) {
if (assetPubKey !== pubkey) { if (assetPubKey !== pubkey) {
app.log.warn("The M2M public key has changed!") app.log.warn("The M2M public key has changed!");
} }
if (assetAlgorithm !== algo) { if (assetAlgorithm !== algo) {
@ -46,7 +46,7 @@ setInterval(async () => {
app.log.warn("Failed to update the M2M credentials"); app.log.warn("Failed to update the M2M credentials");
app.log.warn(e); app.log.warn(e);
} }
}, ASSET_API_M2M_REFRESH_INTERVAL) }, ASSET_API_M2M_REFRESH_INTERVAL);
async function fetchAssetPubkey() { async function fetchAssetPubkey() {
let url = new URL(ASSET_API_ENDPOINT); let url = new URL(ASSET_API_ENDPOINT);
@ -72,11 +72,11 @@ function partsFromSigned(content) {
assert(parts.length === 2); assert(parts.length === 2);
return parts return parts;
} }
export function verifySignature(content) { export function verifySignature(content) {
let parts = partsFromSigned(content) let parts = partsFromSigned(content);
let verify = createVerify(assetAlgorithm); let verify = createVerify(assetAlgorithm);
verify.update(parts[0]); verify.update(parts[0]);
@ -89,4 +89,4 @@ export function verifySignature(content) {
export function contentFromSigned(content) { export function contentFromSigned(content) {
return partsFromSigned(content)[0]; return partsFromSigned(content)[0];
} }

View file

@ -0,0 +1,9 @@
import { ASSET_API_ENDPOINT } from "../../consts.js";
export default function register(app) {
app.get("/asset/endpoint", {
async handler() {
return ASSET_API_ENDPOINT;
},
});
}

View file

@ -0,0 +1,5 @@
import endpoint from "./endpoint.js";
export default function registerRoutes(app, auth) {
endpoint(app, auth);
}

View file

@ -0,0 +1,21 @@
export default function register(app, auth) {
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: { $ref: "schema://identity/authorization" },
},
});
}

View file

@ -0,0 +1,17 @@
export default function register(app, auth) {
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: { $ref: "schema://identity/authorization" },
},
});
}

View file

@ -0,0 +1,24 @@
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",
},
},
});
}

View file

@ -0,0 +1,13 @@
import account from "./account.js";
import genkey from "./genkey.js";
import heirs from "./heirs.js";
import login from "./login.js";
import register from "./register.js";
export default function registerRoutes(app, auth) {
account(app, auth);
genkey(app, auth);
heirs(app, auth);
login(app, auth);
register(app, auth);
}

View file

@ -0,0 +1,30 @@
export default function register(app, auth) {
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"],
},
});
}

View file

@ -0,0 +1,36 @@
export default function register(app, auth) {
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"],
},
},
});
}

View file

@ -0,0 +1,51 @@
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"],
},
},
});
}

View file

@ -0,0 +1,27 @@
export default function register(app, auth) {
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: { $ref: "schema://identity/authorization" },
query: {
type: "object",
properties: {
limit: { type: "number" },
offset: { type: "number" },
},
required: ["limit", "offset"],
},
},
});
}

View file

@ -0,0 +1,11 @@
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);
}

View file

@ -0,0 +1,20 @@
import { contentFromSigned, verifySignature } from "../../m2m.js";
export default function register(app, auth) {
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;
},
});
}

View file

@ -0,0 +1,20 @@
import { contentFromSigned, verifySignature } from "../../m2m.js";
export default function register(app, auth) {
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);
},
});
}

View file

@ -0,0 +1,7 @@
import asset from "./asset.js";
import account from "./account.js";
export default function registerRoutes(app, auth) {
asset(app, auth);
account(app, auth);
}