mirror of
https://github.com/fosrl/pangolin.git
synced 2025-08-29 14:18:26 +02:00
feat: Add setup token security for initial server setup
- Add setupTokens database table with proper schema - Implement setup token generation on first server startup - Add token validation endpoint and modify admin creation - Update initial setup page to require setup token - Add migration scripts for both SQLite and PostgreSQL - Add internationalization support for setup token fields - Implement proper error handling and logging - Add CLI command for resetting user security keys This prevents unauthorized access during initial server setup by requiring a token that is generated and displayed in the server console.
This commit is contained in:
parent
84268e484d
commit
69baa6785f
15 changed files with 322 additions and 115 deletions
|
@ -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",
|
||||
|
|
110
package-lock.json
generated
110
package-lock.json
generated
|
@ -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"
|
||||
|
|
|
@ -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<typeof orgs>;
|
||||
export type User = InferSelectModel<typeof users>;
|
||||
export type Site = InferSelectModel<typeof sites>;
|
||||
|
@ -637,3 +645,4 @@ export type OlmSession = InferSelectModel<typeof olmSessions>;
|
|||
export type UserClient = InferSelectModel<typeof userClients>;
|
||||
export type RoleClient = InferSelectModel<typeof roleClients>;
|
||||
export type OrgDomains = InferSelectModel<typeof orgDomains>;
|
||||
export type SetupToken = InferSelectModel<typeof setupTokens>;
|
||||
|
|
|
@ -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<typeof apiKeys>;
|
|||
export type ApiKeyAction = InferSelectModel<typeof apiKeyActions>;
|
||||
export type ApiKeyOrg = InferSelectModel<typeof apiKeyOrg>;
|
||||
export type OrgDomains = InferSelectModel<typeof orgDomains>;
|
||||
export type SetupToken = InferSelectModel<typeof setupTokens>;
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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<typeof bodySchema>;
|
||||
|
@ -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,7 +79,18 @@ export async function setServerAdmin(
|
|||
const passwordHash = await hashPassword(password);
|
||||
const userId = generateId(15);
|
||||
|
||||
await db.insert(users).values({
|
||||
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,
|
||||
|
@ -68,6 +100,7 @@ export async function setServerAdmin(
|
|||
serverAdmin: true,
|
||||
emailVerified: true
|
||||
});
|
||||
});
|
||||
|
||||
return response<SetServerAdminResponse>(res, {
|
||||
data: null,
|
||||
|
|
84
server/routers/auth/validateSetupToken.ts
Normal file
84
server/routers/auth/validateSetupToken.ts
Normal file
|
@ -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<any> {
|
||||
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<ValidateSetupTokenResponse>(res, {
|
||||
data: {
|
||||
valid: false,
|
||||
message: "Invalid or expired setup token"
|
||||
},
|
||||
success: true,
|
||||
error: false,
|
||||
message: "Token validation completed",
|
||||
status: HttpCode.OK
|
||||
});
|
||||
}
|
||||
|
||||
return response<ValidateSetupTokenResponse>(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"
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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(
|
||||
|
|
73
server/setup/ensureSetupToken.ts
Normal file
73
server/setup/ensureSetupToken.ts
Normal file
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
25
server/setup/scriptsPg/1.9.0.ts
Normal file
25
server/setup/scriptsPg/1.9.0.ts
Normal file
|
@ -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;
|
||||
}
|
||||
}
|
35
server/setup/scriptsSqlite/1.9.0.ts
Normal file
35
server/setup/scriptsSqlite/1.9.0.ts
Normal file
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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<z.infer<typeof formSchema>>({
|
||||
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"
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="setupToken"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("setupToken")}</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
placeholder={t("setupTokenPlaceholder")}
|
||||
autoComplete="off"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="email"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue