diff --git a/messages/en-US.json b/messages/en-US.json index 9986c5fd..0389a0dc 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -967,6 +967,9 @@ "actionDeleteSite": "Delete Site", "actionGetSite": "Get Site", "actionListSites": "List Sites", + "setupToken": "Setup Token", + "setupTokenPlaceholder": "Enter the setup token from the server console", + "setupTokenRequired": "Setup token is required", "actionUpdateSite": "Update Site", "actionListSiteRoles": "List Allowed Site Roles", "actionCreateResource": "Create Resource", diff --git a/package-lock.json b/package-lock.json index baec0b2b..7eb58d2c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -61,7 +61,6 @@ "http-errors": "2.0.0", "i": "^0.3.7", "input-otp": "1.4.2", - "ioredis": "^5.6.1", "jmespath": "^0.16.0", "js-yaml": "4.1.0", "jsonwebtoken": "^9.0.2", @@ -77,7 +76,6 @@ "oslo": "1.2.1", "pg": "^8.16.2", "qrcode.react": "4.2.0", - "rate-limit-redis": "^4.2.1", "react": "19.1.0", "react-dom": "19.1.0", "react-easy-sort": "^1.6.0", @@ -2010,12 +2008,6 @@ "url": "https://opencollective.com/libvips" } }, - "node_modules/@ioredis/commands": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.2.0.tgz", - "integrity": "sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==", - "license": "MIT" - }, "node_modules/@isaacs/balanced-match": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", @@ -2058,6 +2050,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "dev": true, "license": "ISC", "dependencies": { "minipass": "^7.0.4" @@ -6309,6 +6302,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "dev": true, "license": "BlueOak-1.0.0", "engines": { "node": ">=18" @@ -6455,15 +6449,6 @@ "node": ">=6" } }, - "node_modules/cluster-key-slot": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", - "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==", - "license": "Apache-2.0", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/cmdk": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/cmdk/-/cmdk-1.1.1.tgz", @@ -6947,15 +6932,6 @@ "node": ">=0.4.0" } }, - "node_modules/denque": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", - "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", - "license": "Apache-2.0", - "engines": { - "node": ">=0.10" - } - }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -8835,6 +8811,7 @@ "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, "license": "ISC" }, "node_modules/graphemer": { @@ -9122,30 +9099,6 @@ "tslib": "^2.8.0" } }, - "node_modules/ioredis": { - "version": "5.6.1", - "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.6.1.tgz", - "integrity": "sha512-UxC0Yv1Y4WRJiGQxQkP0hfdL0/5/6YvdfOOClRgJ0qppSarkhneSa6UvkMkms0AkdGimSH3Ikqm+6mkMmX7vGA==", - "license": "MIT", - "dependencies": { - "@ioredis/commands": "^1.1.1", - "cluster-key-slot": "^1.1.0", - "debug": "^4.3.4", - "denque": "^2.1.0", - "lodash.defaults": "^4.2.0", - "lodash.isarguments": "^3.1.0", - "redis-errors": "^1.2.0", - "redis-parser": "^3.0.0", - "standard-as-callback": "^2.1.0" - }, - "engines": { - "node": ">=12.22.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/ioredis" - } - }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -9606,6 +9559,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", + "dev": true, "license": "ISC", "engines": { "node": ">=16" @@ -10112,24 +10066,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/lodash.defaults": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", - "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", - "license": "MIT" - }, "node_modules/lodash.includes": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", "license": "MIT" }, - "node_modules/lodash.isarguments": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", - "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==", - "license": "MIT" - }, "node_modules/lodash.isboolean": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", @@ -10481,6 +10423,7 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.2.tgz", "integrity": "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==", + "dev": true, "license": "MIT", "dependencies": { "minipass": "^7.1.2" @@ -10493,6 +10436,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", + "dev": true, "license": "MIT", "bin": { "mkdirp": "dist/cjs/src/bin.js" @@ -14470,18 +14414,6 @@ "node": ">= 0.6" } }, - "node_modules/rate-limit-redis": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/rate-limit-redis/-/rate-limit-redis-4.2.1.tgz", - "integrity": "sha512-JsUsVmRVI6G/XrlYtfGV1NMCbGS/CVYayHkxD5Ism5FaL8qpFHCXbFkUeIi5WJ/onJOKWCgtB/xtCLa6qSXb4g==", - "license": "MIT", - "engines": { - "node": ">= 16" - }, - "peerDependencies": { - "express-rate-limit": ">= 6" - } - }, "node_modules/raw-body": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", @@ -14828,27 +14760,6 @@ "node": ">=0.8.8" } }, - "node_modules/redis-errors": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", - "integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/redis-parser": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz", - "integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==", - "license": "MIT", - "dependencies": { - "redis-errors": "^1.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -15628,12 +15539,6 @@ "node": "*" } }, - "node_modules/standard-as-callback": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", - "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==", - "license": "MIT" - }, "node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", @@ -16021,6 +15926,7 @@ "version": "7.4.3", "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz", "integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==", + "dev": true, "license": "ISC", "dependencies": { "@isaacs/fs-minipass": "^4.0.0", @@ -16667,6 +16573,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", + "dev": true, "license": "ISC", "dependencies": { "isexe": "^3.1.1" @@ -16972,6 +16879,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "dev": true, "license": "BlueOak-1.0.0", "engines": { "node": ">=18" diff --git a/server/db/pg/schema.ts b/server/db/pg/schema.ts index be4e58e2..afc9d6e7 100644 --- a/server/db/pg/schema.ts +++ b/server/db/pg/schema.ts @@ -592,6 +592,14 @@ export const webauthnChallenge = pgTable("webauthnChallenge", { expiresAt: bigint("expiresAt", { mode: "number" }).notNull() // Unix timestamp }); +export const setupTokens = pgTable("setupTokens", { + tokenId: varchar("tokenId").primaryKey(), + token: varchar("token").notNull(), + used: boolean("used").notNull().default(false), + dateCreated: varchar("dateCreated").notNull(), + dateUsed: varchar("dateUsed") +}); + export type Org = InferSelectModel; export type User = InferSelectModel; export type Site = InferSelectModel; @@ -637,3 +645,4 @@ export type OlmSession = InferSelectModel; export type UserClient = InferSelectModel; export type RoleClient = InferSelectModel; export type OrgDomains = InferSelectModel; +export type SetupToken = InferSelectModel; diff --git a/server/db/sqlite/schema.ts b/server/db/sqlite/schema.ts index 5773a5f3..cc0fb6d0 100644 --- a/server/db/sqlite/schema.ts +++ b/server/db/sqlite/schema.ts @@ -187,6 +187,14 @@ export const webauthnChallenge = sqliteTable("webauthnChallenge", { expiresAt: integer("expiresAt").notNull() // Unix timestamp }); +export const setupTokens = sqliteTable("setupTokens", { + tokenId: text("tokenId").primaryKey(), + token: text("token").notNull(), + used: integer("used", { mode: "boolean" }).notNull().default(false), + dateCreated: text("dateCreated").notNull(), + dateUsed: text("dateUsed") +}); + export const newts = sqliteTable("newt", { newtId: text("id").primaryKey(), secretHash: text("secretHash").notNull(), @@ -679,3 +687,4 @@ export type ApiKey = InferSelectModel; export type ApiKeyAction = InferSelectModel; export type ApiKeyOrg = InferSelectModel; export type OrgDomains = InferSelectModel; +export type SetupToken = InferSelectModel; diff --git a/server/routers/auth/index.ts b/server/routers/auth/index.ts index cc8fd630..505d12c2 100644 --- a/server/routers/auth/index.ts +++ b/server/routers/auth/index.ts @@ -10,6 +10,7 @@ export * from "./resetPassword"; export * from "./requestPasswordReset"; export * from "./setServerAdmin"; export * from "./initialSetupComplete"; +export * from "./validateSetupToken"; export * from "./changePassword"; export * from "./checkResourceSession"; export * from "./securityKey"; diff --git a/server/routers/auth/setServerAdmin.ts b/server/routers/auth/setServerAdmin.ts index 7c49753e..ebb95359 100644 --- a/server/routers/auth/setServerAdmin.ts +++ b/server/routers/auth/setServerAdmin.ts @@ -8,14 +8,15 @@ import logger from "@server/logger"; import { hashPassword } from "@server/auth/password"; import { passwordSchema } from "@server/auth/passwordSchema"; import { response } from "@server/lib"; -import { db, users } from "@server/db"; -import { eq } from "drizzle-orm"; +import { db, users, setupTokens } from "@server/db"; +import { eq, and } from "drizzle-orm"; import { UserType } from "@server/types/UserTypes"; import moment from "moment"; export const bodySchema = z.object({ email: z.string().toLowerCase().email(), - password: passwordSchema + password: passwordSchema, + setupToken: z.string().min(1, "Setup token is required") }); export type SetServerAdminBody = z.infer; @@ -39,7 +40,27 @@ export async function setServerAdmin( ); } - const { email, password } = parsedBody.data; + const { email, password, setupToken } = parsedBody.data; + + // Validate setup token + const [validToken] = await db + .select() + .from(setupTokens) + .where( + and( + eq(setupTokens.token, setupToken), + eq(setupTokens.used, false) + ) + ); + + if (!validToken) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + "Invalid or expired setup token" + ) + ); + } const [existing] = await db .select() @@ -58,15 +79,27 @@ export async function setServerAdmin( const passwordHash = await hashPassword(password); const userId = generateId(15); - await db.insert(users).values({ - userId: userId, - email: email, - type: UserType.Internal, - username: email, - passwordHash, - dateCreated: moment().toISOString(), - serverAdmin: true, - emailVerified: true + await db.transaction(async (trx) => { + // Mark the token as used + await trx + .update(setupTokens) + .set({ + used: true, + dateUsed: moment().toISOString() + }) + .where(eq(setupTokens.tokenId, validToken.tokenId)); + + // Create the server admin user + await trx.insert(users).values({ + userId: userId, + email: email, + type: UserType.Internal, + username: email, + passwordHash, + dateCreated: moment().toISOString(), + serverAdmin: true, + emailVerified: true + }); }); return response(res, { diff --git a/server/routers/auth/validateSetupToken.ts b/server/routers/auth/validateSetupToken.ts new file mode 100644 index 00000000..e3c29833 --- /dev/null +++ b/server/routers/auth/validateSetupToken.ts @@ -0,0 +1,84 @@ +import { Request, Response, NextFunction } from "express"; +import { z } from "zod"; +import { db, setupTokens } from "@server/db"; +import { eq, and } from "drizzle-orm"; +import response from "@server/lib/response"; +import HttpCode from "@server/types/HttpCode"; +import createHttpError from "http-errors"; +import logger from "@server/logger"; +import { fromError } from "zod-validation-error"; + +const validateSetupTokenSchema = z + .object({ + token: z.string().min(1, "Token is required") + }) + .strict(); + +export type ValidateSetupTokenResponse = { + valid: boolean; + message: string; +}; + +export async function validateSetupToken( + req: Request, + res: Response, + next: NextFunction +): Promise { + try { + const parsedBody = validateSetupTokenSchema.safeParse(req.body); + + if (!parsedBody.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedBody.error).toString() + ) + ); + } + + const { token } = parsedBody.data; + + // Find the token in the database + const [setupToken] = await db + .select() + .from(setupTokens) + .where( + and( + eq(setupTokens.token, token), + eq(setupTokens.used, false) + ) + ); + + if (!setupToken) { + return response(res, { + data: { + valid: false, + message: "Invalid or expired setup token" + }, + success: true, + error: false, + message: "Token validation completed", + status: HttpCode.OK + }); + } + + return response(res, { + data: { + valid: true, + message: "Setup token is valid" + }, + success: true, + error: false, + message: "Token validation completed", + status: HttpCode.OK + }); + } catch (error) { + logger.error(error); + return next( + createHttpError( + HttpCode.INTERNAL_SERVER_ERROR, + "Failed to validate setup token" + ) + ); + } +} \ No newline at end of file diff --git a/server/routers/external.ts b/server/routers/external.ts index 5bae553e..f9ff7377 100644 --- a/server/routers/external.ts +++ b/server/routers/external.ts @@ -1033,6 +1033,7 @@ authRouter.post("/idp/:idpId/oidc/validate-callback", idp.validateOidcCallback); authRouter.put("/set-server-admin", auth.setServerAdmin); authRouter.get("/initial-setup-complete", auth.initialSetupComplete); +authRouter.post("/validate-setup-token", auth.validateSetupToken); // Security Key routes authRouter.post( diff --git a/server/setup/ensureSetupToken.ts b/server/setup/ensureSetupToken.ts new file mode 100644 index 00000000..1734b5e6 --- /dev/null +++ b/server/setup/ensureSetupToken.ts @@ -0,0 +1,73 @@ +import { db, setupTokens, users } from "@server/db"; +import { eq } from "drizzle-orm"; +import { generateRandomString, RandomReader } from "@oslojs/crypto/random"; +import moment from "moment"; +import logger from "@server/logger"; + +const random: RandomReader = { + read(bytes: Uint8Array): void { + crypto.getRandomValues(bytes); + } +}; + +function generateToken(): string { + // Generate a 32-character alphanumeric token + const alphabet = "abcdefghijklmnopqrstuvwxyz0123456789"; + return generateRandomString(random, alphabet, 32); +} + +function generateId(length: number): string { + const alphabet = "abcdefghijklmnopqrstuvwxyz0123456789"; + return generateRandomString(random, alphabet, length); +} + +export async function ensureSetupToken() { + try { + // Check if a server admin already exists + const [existingAdmin] = await db + .select() + .from(users) + .where(eq(users.serverAdmin, true)); + + // If admin exists, no need for setup token + if (existingAdmin) { + logger.warn("Server admin exists. Setup token generation skipped."); + return; + } + + // Check if a setup token already exists + const existingTokens = await db + .select() + .from(setupTokens) + .where(eq(setupTokens.used, false)); + + // If unused token exists, display it instead of creating a new one + if (existingTokens.length > 0) { + console.log("=== SETUP TOKEN EXISTS ==="); + console.log("Token:", existingTokens[0].token); + console.log("Use this token on the initial setup page"); + console.log("================================"); + return; + } + + // Generate a new setup token + const token = generateToken(); + const tokenId = generateId(15); + + await db.insert(setupTokens).values({ + tokenId: tokenId, + token: token, + used: false, + dateCreated: moment().toISOString(), + dateUsed: null + }); + + console.log("=== SETUP TOKEN GENERATED ==="); + console.log("Token:", token); + console.log("Use this token on the initial setup page"); + console.log("================================"); + } catch (error) { + console.error("Failed to ensure setup token:", error); + throw error; + } +} \ No newline at end of file diff --git a/server/setup/index.ts b/server/setup/index.ts index d126869a..2dfb633e 100644 --- a/server/setup/index.ts +++ b/server/setup/index.ts @@ -1,9 +1,11 @@ import { ensureActions } from "./ensureActions"; import { copyInConfig } from "./copyInConfig"; import { clearStaleData } from "./clearStaleData"; +import { ensureSetupToken } from "./ensureSetupToken"; export async function runSetupFunctions() { await copyInConfig(); // copy in the config to the db as needed await ensureActions(); // make sure all of the actions are in the db and the roles await clearStaleData(); + await ensureSetupToken(); // ensure setup token exists for initial setup } diff --git a/server/setup/migrationsPg.ts b/server/setup/migrationsPg.ts index 07ece65b..6b3f20b9 100644 --- a/server/setup/migrationsPg.ts +++ b/server/setup/migrationsPg.ts @@ -8,6 +8,7 @@ import path from "path"; import m1 from "./scriptsPg/1.6.0"; import m2 from "./scriptsPg/1.7.0"; import m3 from "./scriptsPg/1.8.0"; +import m4 from "./scriptsPg/1.9.0"; // THIS CANNOT IMPORT ANYTHING FROM THE SERVER // EXCEPT FOR THE DATABASE AND THE SCHEMA @@ -16,7 +17,8 @@ import m3 from "./scriptsPg/1.8.0"; const migrations = [ { version: "1.6.0", run: m1 }, { version: "1.7.0", run: m2 }, - { version: "1.8.0", run: m3 } + { version: "1.8.0", run: m3 }, + { version: "1.9.0", run: m4 } // Add new migrations here as they are created ] as { version: string; diff --git a/server/setup/migrationsSqlite.ts b/server/setup/migrationsSqlite.ts index 15dd28d2..5b0850c8 100644 --- a/server/setup/migrationsSqlite.ts +++ b/server/setup/migrationsSqlite.ts @@ -25,6 +25,7 @@ import m20 from "./scriptsSqlite/1.5.0"; import m21 from "./scriptsSqlite/1.6.0"; import m22 from "./scriptsSqlite/1.7.0"; import m23 from "./scriptsSqlite/1.8.0"; +import m24 from "./scriptsSqlite/1.9.0"; // THIS CANNOT IMPORT ANYTHING FROM THE SERVER // EXCEPT FOR THE DATABASE AND THE SCHEMA @@ -49,6 +50,7 @@ const migrations = [ { version: "1.6.0", run: m21 }, { version: "1.7.0", run: m22 }, { version: "1.8.0", run: m23 }, + { version: "1.9.0", run: m24 }, // Add new migrations here as they are created ] as const; diff --git a/server/setup/scriptsPg/1.9.0.ts b/server/setup/scriptsPg/1.9.0.ts new file mode 100644 index 00000000..22259cae --- /dev/null +++ b/server/setup/scriptsPg/1.9.0.ts @@ -0,0 +1,25 @@ +import { db } from "@server/db/pg/driver"; +import { sql } from "drizzle-orm"; + +const version = "1.9.0"; + +export default async function migration() { + console.log(`Running setup script ${version}...`); + + try { + await db.execute(sql` + CREATE TABLE "setupTokens" ( + "tokenId" varchar PRIMARY KEY NOT NULL, + "token" varchar NOT NULL, + "used" boolean DEFAULT false NOT NULL, + "dateCreated" varchar NOT NULL, + "dateUsed" varchar + ); + `); + + console.log(`Added setupTokens table`); + } catch (e) { + console.log("Unable to add setupTokens table:", e); + throw e; + } +} \ No newline at end of file diff --git a/server/setup/scriptsSqlite/1.9.0.ts b/server/setup/scriptsSqlite/1.9.0.ts new file mode 100644 index 00000000..a4a20dda --- /dev/null +++ b/server/setup/scriptsSqlite/1.9.0.ts @@ -0,0 +1,35 @@ +import { APP_PATH } from "@server/lib/consts"; +import Database from "better-sqlite3"; +import path from "path"; + +const version = "1.9.0"; + +export default async function migration() { + console.log(`Running setup script ${version}...`); + + const location = path.join(APP_PATH, "db", "db.sqlite"); + const db = new Database(location); + + try { + db.pragma("foreign_keys = OFF"); + + db.transaction(() => { + db.exec(` + CREATE TABLE 'setupTokens' ( + 'tokenId' text PRIMARY KEY NOT NULL, + 'token' text NOT NULL, + 'used' integer DEFAULT 0 NOT NULL, + 'dateCreated' text NOT NULL, + 'dateUsed' text + ); + `); + })(); + + db.pragma("foreign_keys = ON"); + + console.log(`Added setupTokens table`); + } catch (e) { + console.log("Unable to add setupTokens table:", e); + throw e; + } +} \ No newline at end of file diff --git a/src/app/auth/initial-setup/page.tsx b/src/app/auth/initial-setup/page.tsx index 17e6c2ec..518c5370 100644 --- a/src/app/auth/initial-setup/page.tsx +++ b/src/app/auth/initial-setup/page.tsx @@ -31,6 +31,7 @@ import { passwordSchema } from "@server/auth/passwordSchema"; const formSchema = z .object({ + setupToken: z.string().min(1, "Setup token is required"), email: z.string().email({ message: "Invalid email address" }), password: passwordSchema, confirmPassword: z.string() @@ -52,6 +53,7 @@ export default function InitialSetupPage() { const form = useForm>({ resolver: zodResolver(formSchema), defaultValues: { + setupToken: "", email: "", password: "", confirmPassword: "" @@ -63,6 +65,7 @@ export default function InitialSetupPage() { setError(null); try { const res = await api.put("/auth/set-server-admin", { + setupToken: values.setupToken, email: values.email, password: values.password }); @@ -102,6 +105,23 @@ export default function InitialSetupPage() { onSubmit={form.handleSubmit(onSubmit)} className="space-y-4" > + ( + + {t("setupToken")} + + + + + + )} + />