diff --git a/src/database/migration.ts b/src/database/migration.ts index 2068916..84826bd 100644 --- a/src/database/migration.ts +++ b/src/database/migration.ts @@ -67,25 +67,6 @@ export const migrations: Migration[] = [ database.user.update("id", userRootId, "token", userRootToken); } } - - const userTokens = database.user.getAll(["token"]); - - let userTokenUnhashed = false; - for (const entry of userTokens) { - // combo separator - if (!entry.token.includes(" ")) { - userTokenUnhashed = true; - break; - } - } - - if (userTokenUnhashed) { - log.warn( - "Users with plain tokens found!", - "New users in the instance will have their token hashed,", - "In the future we will enforce that every user token is hashed." - ); - } }, sql: (await import("./migrations/0002.sql", { with: { type: "text" } })).default } diff --git a/src/endpoints/document/v1/delete.ts b/src/endpoints/document/v1/drop.ts similarity index 94% rename from src/endpoints/document/v1/delete.ts rename to src/endpoints/document/v1/drop.ts index bfabeb8..ee892ef 100644 --- a/src/endpoints/document/v1/delete.ts +++ b/src/endpoints/document/v1/drop.ts @@ -19,8 +19,8 @@ export default new Hono().delete( "/:name", describeRoute({ tags: ["DOCUMENT (v1)"], - summary: "Delete document", - description: "Deletes a published document in the instance", + summary: "Drop document", + description: "Deletes a document in the instance", security: [{}, { bearer: [] }], responses: { 200: { diff --git a/src/endpoints/document/v1/get.ts b/src/endpoints/document/v1/get.ts index 9f0f65e..bb2744e 100644 --- a/src/endpoints/document/v1/get.ts +++ b/src/endpoints/document/v1/get.ts @@ -42,7 +42,7 @@ export default new Hono().get( describeRoute({ tags: ["DOCUMENT (v1)"], summary: "Get document", - description: `Get the content/metadata of a published document in the instance + description: `Fetch the content/metadata of a document in the instance Note: If you only need to query the document metadata, you should use HEAD method instead`, responses: { diff --git a/src/endpoints/document/v1/index.ts b/src/endpoints/document/v1/index.ts index 768aee1..370d41b 100644 --- a/src/endpoints/document/v1/index.ts +++ b/src/endpoints/document/v1/index.ts @@ -2,7 +2,7 @@ import { Hono } from "hono/tiny"; import type { Env } from "#http/handler.ts"; -import delete_ from "./delete.ts"; +import drop from "./drop.ts"; import get from "./get.ts"; import list from "./list.ts"; import patch from "./patch.ts"; @@ -10,7 +10,7 @@ import post from "./post.ts"; export const v1DocumentHandler = new Hono(); -v1DocumentHandler.route("/", delete_); +v1DocumentHandler.route("/", drop); v1DocumentHandler.route("/", get); v1DocumentHandler.route("/", list); v1DocumentHandler.route("/", patch); diff --git a/src/endpoints/document/v1/patch.ts b/src/endpoints/document/v1/patch.ts index 4e8e385..5e7a554 100644 --- a/src/endpoints/document/v1/patch.ts +++ b/src/endpoints/document/v1/patch.ts @@ -39,7 +39,7 @@ export default new Hono().patch( describeRoute({ tags: ["DOCUMENT (v1)"], summary: "Alter document", - description: `Edit the content/metadata of a published document in the instance + description: `Edit the content/metadata of a document in the instance Note: You can't move the ownership of a document, duplicate the document instead diff --git a/src/endpoints/user/v1/drop.ts b/src/endpoints/user/v1/drop.ts new file mode 100644 index 0000000..6f33b84 --- /dev/null +++ b/src/endpoints/user/v1/drop.ts @@ -0,0 +1,40 @@ +import { describeRoute } from "@hono/openapi"; +import { Hono } from "hono/tiny"; + +import { constantHttpStatusCodes, mutable } from "#/global.ts"; +import type { Env } from "#http/handler.ts"; +import { authMiddleware } from "#http/middleware/authorization.ts"; +import { ErrorCode, errorThrow, genericErrorResponse } from "#util/error.ts"; + +export default new Hono().delete( + "/", + describeRoute({ + tags: ["USER (v1)"], + summary: "Drop user", + description: `Deletes a user in the instance + +Note: All documents owned by the user will also be deleted`, + security: [{ bearer: [] }], + responses: { + 200: { + description: constantHttpStatusCodes[200] + }, + 400: { ...genericErrorResponse, description: constantHttpStatusCodes[400] }, + 404: { ...genericErrorResponse, description: constantHttpStatusCodes[404] }, + + // auth middleware + 401: { ...genericErrorResponse, description: constantHttpStatusCodes[401] } + } + }), + authMiddleware, + (ctx) => { + const userId = ctx.get("userId"); + if (!userId) { + return errorThrow(ErrorCode.UserInvalidToken); + } + + mutable.database.user.delete("id", userId); + + return ctx.body(null); + } +); diff --git a/src/endpoints/user/v1/index.ts b/src/endpoints/user/v1/index.ts index 8e29ad6..26a03ab 100644 --- a/src/endpoints/user/v1/index.ts +++ b/src/endpoints/user/v1/index.ts @@ -3,7 +3,11 @@ import { Hono } from "hono/tiny"; import type { Env } from "#http/handler.ts"; import create from "./create.ts"; +import drop from "./drop.ts"; +import rotateToken from "./rotateToken.ts"; export const v1UserHandler = new Hono(); v1UserHandler.route("/", create); +v1UserHandler.route("/", drop); +v1UserHandler.route("/", rotateToken); diff --git a/src/endpoints/user/v1/rotateToken.ts b/src/endpoints/user/v1/rotateToken.ts new file mode 100644 index 0000000..bc35753 --- /dev/null +++ b/src/endpoints/user/v1/rotateToken.ts @@ -0,0 +1,58 @@ +import { describeRoute, resolver } from "@hono/openapi"; +import { type } from "arktype"; +import { Hono } from "hono/tiny"; + +import { constantHttpStatusCodes, mutable } from "#/global.ts"; +import type { Env } from "#http/handler.ts"; +import { authMiddleware } from "#http/middleware/authorization.ts"; +import { generateHash } from "#util/crypto.ts"; +import { ErrorCode, errorThrow, genericErrorResponse } from "#util/error.ts"; +import { generateToken } from "#util/user.ts"; +import { validatorUserToken } from "#util/validator/user.ts"; + +const schemaBodyResponse = resolver( + type({ + token: validatorUserToken + }) +); + +export default new Hono().post( + "/token", + describeRoute({ + tags: ["USER (v1)"], + summary: "Rotate user token", + description: "Rotates a user token in the instance", + security: [{ bearer: [] }], + responses: { + 200: { + content: { + "application/json": { + schema: schemaBodyResponse + } + }, + description: constantHttpStatusCodes[200] + }, + 400: { ...genericErrorResponse, description: constantHttpStatusCodes[400] }, + 404: { ...genericErrorResponse, description: constantHttpStatusCodes[404] }, + + // auth middleware + 401: { ...genericErrorResponse, description: constantHttpStatusCodes[401] } + } + }), + authMiddleware, + (ctx) => { + const userId = ctx.get("userId"); + if (!userId) { + return errorThrow(ErrorCode.UserInvalidToken); + } + + const token = generateToken(userId); + const hash = generateHash(token); + + mutable.database.user.update("id", userId, "token", hash.combo); + + return ctx.json({ + token: token + }); + } +); diff --git a/src/index.ts b/src/index.ts index 74ae5e0..8b64543 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,7 @@ import { abortable } from "@std/async"; import { constantStoreDispose } from "#/global.ts"; -import { initDatabase, initDirStruct, initHTTPServer, initTask } from "#/init.ts"; +import { initDatabase, initDirStruct, initHTTPServer, initUnhashedTokenCheck, initTask } from "#/init.ts"; import { handler } from "#http/handler.ts"; import { Logger } from "#util/console.ts"; @@ -59,6 +59,7 @@ for (const signal of ["SIGINT", "SIGTERM", "SIGHUP", "SIGUSR1", "SIGUSR2"] satis try { await Promise.all([initDirStruct(), initHTTPServer()]); await initDatabase(); + initUnhashedTokenCheck(); initTask(); await initHTTPServer(handler().fetch); } catch (error) { diff --git a/src/init.ts b/src/init.ts index 4f8f630..4b9cf16 100644 --- a/src/init.ts +++ b/src/init.ts @@ -4,10 +4,13 @@ import { Database } from "#db/index.ts"; import { http } from "#http/index.ts"; import { taskRegister } from "#task/index.ts"; import { sweeper } from "#task/list/sweeper.ts"; +import { Logger } from "#util/console.ts"; import { env } from "#util/env.ts"; import { constantPathStructStorage, constantPathStructStorageData, constantStoreDispose, mutable } from "./global.ts"; +const log: Logger = new Logger(); + export const initDirStruct = async (): Promise => { await Promise.all([ensureDir(constantPathStructStorage), ensureDir(constantPathStructStorageData)]); }; @@ -54,3 +57,24 @@ export const initTask = (): void => { name: "sweeper" }); }; + +export const initUnhashedTokenCheck = (): void => { + const userTokens = mutable.database.user.getAll(["token"]); + + let userUnhashedToken = false; + for (const entry of userTokens) { + // combo separator + if (!entry.token.includes(" ")) { + userUnhashedToken = true; + break; + } + } + + if (userUnhashedToken) { + log.warn( + "Users with unhashed tokens found!", + "Those users may lose access in future versions of JSPaste!", + "See: https://github.com/jspaste/backend/issues/318" + ); + } +};