mirror of
https://github.com/fosrl/pangolin.git
synced 2025-07-07 20:45:00 +02:00
remove server admin from config and add onboarding ui
This commit is contained in:
parent
f300838f8e
commit
d03f45279c
15 changed files with 345 additions and 190 deletions
|
@ -41,11 +41,6 @@ rate_limits:
|
||||||
window_minutes: 1
|
window_minutes: 1
|
||||||
max_requests: 500
|
max_requests: 500
|
||||||
|
|
||||||
users:
|
|
||||||
server_admin:
|
|
||||||
email: "admin@example.com"
|
|
||||||
password: "Password123!"
|
|
||||||
|
|
||||||
flags:
|
flags:
|
||||||
require_email_verification: false
|
require_email_verification: false
|
||||||
disable_signup_without_invite: true
|
disable_signup_without_invite: true
|
||||||
|
|
|
@ -54,10 +54,6 @@ email:
|
||||||
smtp_pass: "{{.EmailSMTPPass}}"
|
smtp_pass: "{{.EmailSMTPPass}}"
|
||||||
no_reply: "{{.EmailNoReply}}"
|
no_reply: "{{.EmailNoReply}}"
|
||||||
{{end}}
|
{{end}}
|
||||||
users:
|
|
||||||
server_admin:
|
|
||||||
email: "{{.AdminUserEmail}}"
|
|
||||||
password: "{{.AdminUserPassword}}"
|
|
||||||
|
|
||||||
flags:
|
flags:
|
||||||
require_email_verification: {{.EnableEmail}}
|
require_email_verification: {{.EnableEmail}}
|
||||||
|
|
|
@ -16,7 +16,6 @@ import (
|
||||||
"syscall"
|
"syscall"
|
||||||
"text/template"
|
"text/template"
|
||||||
"time"
|
"time"
|
||||||
"unicode"
|
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
|
@ -40,8 +39,6 @@ type Config struct {
|
||||||
BaseDomain string
|
BaseDomain string
|
||||||
DashboardDomain string
|
DashboardDomain string
|
||||||
LetsEncryptEmail string
|
LetsEncryptEmail string
|
||||||
AdminUserEmail string
|
|
||||||
AdminUserPassword string
|
|
||||||
DisableSignupWithoutInvite bool
|
DisableSignupWithoutInvite bool
|
||||||
DisableUserCreateOrg bool
|
DisableUserCreateOrg bool
|
||||||
EnableEmail bool
|
EnableEmail bool
|
||||||
|
@ -171,6 +168,7 @@ func main() {
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println("Installation complete!")
|
fmt.Println("Installation complete!")
|
||||||
|
fmt.Printf("\nTo complete the initial setup, please visit:\nhttps://%s/auth/initial-setup\n", config.DashboardDomain)
|
||||||
}
|
}
|
||||||
|
|
||||||
func readString(reader *bufio.Reader, prompt string, defaultValue string) string {
|
func readString(reader *bufio.Reader, prompt string, defaultValue string) string {
|
||||||
|
@ -236,30 +234,6 @@ func collectUserInput(reader *bufio.Reader) Config {
|
||||||
config.LetsEncryptEmail = readString(reader, "Enter email for Let's Encrypt certificates", "")
|
config.LetsEncryptEmail = readString(reader, "Enter email for Let's Encrypt certificates", "")
|
||||||
config.InstallGerbil = readBool(reader, "Do you want to use Gerbil to allow tunneled connections", true)
|
config.InstallGerbil = readBool(reader, "Do you want to use Gerbil to allow tunneled connections", true)
|
||||||
|
|
||||||
// Admin user configuration
|
|
||||||
fmt.Println("\n=== Admin User Configuration ===")
|
|
||||||
config.AdminUserEmail = readString(reader, "Enter admin user email", "admin@"+config.BaseDomain)
|
|
||||||
for {
|
|
||||||
pass1 := readPassword("Create admin user password", reader)
|
|
||||||
pass2 := readPassword("Confirm admin user password", reader)
|
|
||||||
|
|
||||||
if pass1 != pass2 {
|
|
||||||
fmt.Println("Passwords do not match")
|
|
||||||
} else {
|
|
||||||
config.AdminUserPassword = pass1
|
|
||||||
if valid, message := validatePassword(config.AdminUserPassword); valid {
|
|
||||||
break
|
|
||||||
} else {
|
|
||||||
fmt.Println("Invalid password:", message)
|
|
||||||
fmt.Println("Password requirements:")
|
|
||||||
fmt.Println("- At least one uppercase English letter")
|
|
||||||
fmt.Println("- At least one lowercase English letter")
|
|
||||||
fmt.Println("- At least one digit")
|
|
||||||
fmt.Println("- At least one special character")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Security settings
|
// Security settings
|
||||||
fmt.Println("\n=== Security Settings ===")
|
fmt.Println("\n=== Security Settings ===")
|
||||||
config.DisableSignupWithoutInvite = readBool(reader, "Disable signup without invite", true)
|
config.DisableSignupWithoutInvite = readBool(reader, "Disable signup without invite", true)
|
||||||
|
@ -290,60 +264,10 @@ func collectUserInput(reader *bufio.Reader) Config {
|
||||||
fmt.Println("Error: Let's Encrypt email is required")
|
fmt.Println("Error: Let's Encrypt email is required")
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
if config.AdminUserEmail == "" || config.AdminUserPassword == "" {
|
|
||||||
fmt.Println("Error: Admin user email and password are required")
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
return config
|
return config
|
||||||
}
|
}
|
||||||
|
|
||||||
func validatePassword(password string) (bool, string) {
|
|
||||||
if len(password) == 0 {
|
|
||||||
return false, "Password cannot be empty"
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
hasUpper bool
|
|
||||||
hasLower bool
|
|
||||||
hasDigit bool
|
|
||||||
hasSpecial bool
|
|
||||||
)
|
|
||||||
|
|
||||||
for _, char := range password {
|
|
||||||
switch {
|
|
||||||
case unicode.IsUpper(char):
|
|
||||||
hasUpper = true
|
|
||||||
case unicode.IsLower(char):
|
|
||||||
hasLower = true
|
|
||||||
case unicode.IsDigit(char):
|
|
||||||
hasDigit = true
|
|
||||||
case unicode.IsPunct(char) || unicode.IsSymbol(char):
|
|
||||||
hasSpecial = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var missing []string
|
|
||||||
if !hasUpper {
|
|
||||||
missing = append(missing, "an uppercase letter")
|
|
||||||
}
|
|
||||||
if !hasLower {
|
|
||||||
missing = append(missing, "a lowercase letter")
|
|
||||||
}
|
|
||||||
if !hasDigit {
|
|
||||||
missing = append(missing, "a digit")
|
|
||||||
}
|
|
||||||
if !hasSpecial {
|
|
||||||
missing = append(missing, "a special character")
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(missing) > 0 {
|
|
||||||
return false, fmt.Sprintf("Password must contain %s", strings.Join(missing, ", "))
|
|
||||||
}
|
|
||||||
|
|
||||||
return true, ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func createConfigFiles(config Config) error {
|
func createConfigFiles(config Config) error {
|
||||||
os.MkdirAll("config", 0755)
|
os.MkdirAll("config", 0755)
|
||||||
os.MkdirAll("config/letsencrypt", 0755)
|
os.MkdirAll("config/letsencrypt", 0755)
|
||||||
|
|
|
@ -1128,5 +1128,9 @@
|
||||||
"light": "light",
|
"light": "light",
|
||||||
"dark": "dark",
|
"dark": "dark",
|
||||||
"system": "system",
|
"system": "system",
|
||||||
"theme": "Theme"
|
"theme": "Theme",
|
||||||
|
"initialSetupTitle": "Initial Server Setup",
|
||||||
|
"initialSetupDescription": "Create the intial server admin account. Only one server admin can exist. You can always change these credentials later.",
|
||||||
|
"createAdminAccount": "Create Admin Account",
|
||||||
|
"setupErrorCreateAdmin": "An error occurred while creating the server admin account."
|
||||||
}
|
}
|
|
@ -197,21 +197,6 @@ export const configSchema = z.object({
|
||||||
no_reply: z.string().email().optional()
|
no_reply: z.string().email().optional()
|
||||||
})
|
})
|
||||||
.optional(),
|
.optional(),
|
||||||
users: z.object({
|
|
||||||
server_admin: z.object({
|
|
||||||
email: z
|
|
||||||
.string()
|
|
||||||
.email()
|
|
||||||
.optional()
|
|
||||||
.transform(getEnvOrYaml("USERS_SERVERADMIN_EMAIL"))
|
|
||||||
.pipe(z.string().email())
|
|
||||||
.transform((v) => v.toLowerCase()),
|
|
||||||
password: passwordSchema
|
|
||||||
.optional()
|
|
||||||
.transform(getEnvOrYaml("USERS_SERVERADMIN_PASSWORD"))
|
|
||||||
.pipe(passwordSchema)
|
|
||||||
})
|
|
||||||
}),
|
|
||||||
flags: z
|
flags: z
|
||||||
.object({
|
.object({
|
||||||
require_email_verification: z.boolean().optional(),
|
require_email_verification: z.boolean().optional(),
|
||||||
|
|
|
@ -10,3 +10,5 @@ export * from "./changePassword";
|
||||||
export * from "./requestPasswordReset";
|
export * from "./requestPasswordReset";
|
||||||
export * from "./resetPassword";
|
export * from "./resetPassword";
|
||||||
export * from "./checkResourceSession";
|
export * from "./checkResourceSession";
|
||||||
|
export * from "./setServerAdmin";
|
||||||
|
export * from "./initialSetupComplete";
|
||||||
|
|
42
server/routers/auth/initialSetupComplete.ts
Normal file
42
server/routers/auth/initialSetupComplete.ts
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
import { NextFunction, Request, Response } from "express";
|
||||||
|
import HttpCode from "@server/types/HttpCode";
|
||||||
|
import createHttpError from "http-errors";
|
||||||
|
import logger from "@server/logger";
|
||||||
|
import { response } from "@server/lib";
|
||||||
|
import { db, users } from "@server/db";
|
||||||
|
import { eq } from "drizzle-orm";
|
||||||
|
|
||||||
|
export type InitialSetupCompleteResponse = {
|
||||||
|
complete: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function initialSetupComplete(
|
||||||
|
req: Request,
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction
|
||||||
|
): Promise<any> {
|
||||||
|
try {
|
||||||
|
const [existing] = await db
|
||||||
|
.select()
|
||||||
|
.from(users)
|
||||||
|
.where(eq(users.serverAdmin, true));
|
||||||
|
|
||||||
|
return response<InitialSetupCompleteResponse>(res, {
|
||||||
|
data: {
|
||||||
|
complete: !!existing
|
||||||
|
},
|
||||||
|
success: true,
|
||||||
|
error: false,
|
||||||
|
message: "Initial setup check completed",
|
||||||
|
status: HttpCode.OK
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
logger.error(e);
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.INTERNAL_SERVER_ERROR,
|
||||||
|
"Failed to check initial setup completion"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
88
server/routers/auth/setServerAdmin.ts
Normal file
88
server/routers/auth/setServerAdmin.ts
Normal file
|
@ -0,0 +1,88 @@
|
||||||
|
import { NextFunction, Request, Response } from "express";
|
||||||
|
import HttpCode from "@server/types/HttpCode";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { fromError } from "zod-validation-error";
|
||||||
|
import createHttpError from "http-errors";
|
||||||
|
import { generateId } from "@server/auth/sessions/app";
|
||||||
|
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 { UserType } from "@server/types/UserTypes";
|
||||||
|
import moment from "moment";
|
||||||
|
|
||||||
|
export const bodySchema = z.object({
|
||||||
|
email: z.string().toLowerCase().email(),
|
||||||
|
password: passwordSchema
|
||||||
|
});
|
||||||
|
|
||||||
|
export type SetServerAdminBody = z.infer<typeof bodySchema>;
|
||||||
|
|
||||||
|
export type SetServerAdminResponse = null;
|
||||||
|
|
||||||
|
export async function setServerAdmin(
|
||||||
|
req: Request,
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction
|
||||||
|
): Promise<any> {
|
||||||
|
try {
|
||||||
|
const parsedBody = bodySchema.safeParse(req.body);
|
||||||
|
|
||||||
|
if (!parsedBody.success) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
fromError(parsedBody.error).toString()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { email, password } = parsedBody.data;
|
||||||
|
|
||||||
|
const [existing] = await db
|
||||||
|
.select()
|
||||||
|
.from(users)
|
||||||
|
.where(eq(users.serverAdmin, true));
|
||||||
|
|
||||||
|
if (existing) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
"Server admin already exists"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
});
|
||||||
|
|
||||||
|
return response<SetServerAdminResponse>(res, {
|
||||||
|
data: null,
|
||||||
|
success: true,
|
||||||
|
error: false,
|
||||||
|
message: "Server admin set successfully",
|
||||||
|
status: HttpCode.OK
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
logger.error(e);
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.INTERNAL_SERVER_ERROR,
|
||||||
|
"Failed to set server admin"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -785,3 +785,6 @@ authRouter.post("/access-token", resource.authWithAccessToken);
|
||||||
authRouter.post("/idp/:idpId/oidc/generate-url", idp.generateOidcUrl);
|
authRouter.post("/idp/:idpId/oidc/generate-url", idp.generateOidcUrl);
|
||||||
|
|
||||||
authRouter.post("/idp/:idpId/oidc/validate-callback", idp.validateOidcCallback);
|
authRouter.post("/idp/:idpId/oidc/validate-callback", idp.validateOidcCallback);
|
||||||
|
|
||||||
|
authRouter.put("/set-server-admin", auth.setServerAdmin);
|
||||||
|
authRouter.get("/initial-setup-complete", auth.initialSetupComplete);
|
||||||
|
|
|
@ -1,13 +1,11 @@
|
||||||
import { ensureActions } from "./ensureActions";
|
import { ensureActions } from "./ensureActions";
|
||||||
import { copyInConfig } from "./copyInConfig";
|
import { copyInConfig } from "./copyInConfig";
|
||||||
import { setupServerAdmin } from "./setupServerAdmin";
|
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import { clearStaleData } from "./clearStaleData";
|
import { clearStaleData } from "./clearStaleData";
|
||||||
|
|
||||||
export async function runSetupFunctions() {
|
export async function runSetupFunctions() {
|
||||||
try {
|
try {
|
||||||
await copyInConfig(); // copy in the config to the db as needed
|
await copyInConfig(); // copy in the config to the db as needed
|
||||||
await setupServerAdmin();
|
|
||||||
await ensureActions(); // make sure all of the actions are in the db and the roles
|
await ensureActions(); // make sure all of the actions are in the db and the roles
|
||||||
await clearStaleData();
|
await clearStaleData();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
|
@ -1,85 +0,0 @@
|
||||||
import { generateId, invalidateAllSessions } from "@server/auth/sessions/app";
|
|
||||||
import { hashPassword, verifyPassword } from "@server/auth/password";
|
|
||||||
import config from "@server/lib/config";
|
|
||||||
import { db } from "@server/db";
|
|
||||||
import { users } from "@server/db";
|
|
||||||
import logger from "@server/logger";
|
|
||||||
import { eq } from "drizzle-orm";
|
|
||||||
import moment from "moment";
|
|
||||||
import { fromError } from "zod-validation-error";
|
|
||||||
import { passwordSchema } from "@server/auth/passwordSchema";
|
|
||||||
import { UserType } from "@server/types/UserTypes";
|
|
||||||
|
|
||||||
export async function setupServerAdmin() {
|
|
||||||
const {
|
|
||||||
server_admin: { email, password }
|
|
||||||
} = config.getRawConfig().users;
|
|
||||||
|
|
||||||
const parsed = passwordSchema.safeParse(password);
|
|
||||||
|
|
||||||
if (!parsed.success) {
|
|
||||||
throw Error(
|
|
||||||
`Invalid server admin password: ${fromError(parsed.error).toString()}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const passwordHash = await hashPassword(password);
|
|
||||||
|
|
||||||
await db.transaction(async (trx) => {
|
|
||||||
try {
|
|
||||||
const [existing] = await trx
|
|
||||||
.select()
|
|
||||||
.from(users)
|
|
||||||
.where(eq(users.serverAdmin, true));
|
|
||||||
|
|
||||||
if (existing) {
|
|
||||||
const passwordChanged = !(await verifyPassword(
|
|
||||||
password,
|
|
||||||
existing.passwordHash!
|
|
||||||
));
|
|
||||||
|
|
||||||
if (passwordChanged) {
|
|
||||||
await trx
|
|
||||||
.update(users)
|
|
||||||
.set({ passwordHash })
|
|
||||||
.where(eq(users.userId, existing.userId));
|
|
||||||
|
|
||||||
// this isn't using the transaction, but it's probably fine
|
|
||||||
await invalidateAllSessions(existing.userId);
|
|
||||||
|
|
||||||
logger.info(`Server admin password updated`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (existing.email !== email) {
|
|
||||||
await trx
|
|
||||||
.update(users)
|
|
||||||
.set({ email, username: email })
|
|
||||||
.where(eq(users.userId, existing.userId));
|
|
||||||
|
|
||||||
logger.info(`Server admin email updated`);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const userId = generateId(15);
|
|
||||||
|
|
||||||
await trx.update(users).set({ serverAdmin: false });
|
|
||||||
|
|
||||||
await db.insert(users).values({
|
|
||||||
userId: userId,
|
|
||||||
email: email,
|
|
||||||
type: UserType.Internal,
|
|
||||||
username: email,
|
|
||||||
passwordHash,
|
|
||||||
dateCreated: moment().toISOString(),
|
|
||||||
serverAdmin: true,
|
|
||||||
emailVerified: true
|
|
||||||
});
|
|
||||||
|
|
||||||
logger.info(`Server admin created`);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error("Failed to setup server admin");
|
|
||||||
logger.error(e);
|
|
||||||
trx.rollback();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
17
src/app/auth/initial-setup/layout.tsx
Normal file
17
src/app/auth/initial-setup/layout.tsx
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
import { internal } from "@app/lib/api";
|
||||||
|
import { authCookieHeader } from "@app/lib/api/cookies";
|
||||||
|
import { InitialSetupCompleteResponse } from "@server/routers/auth";
|
||||||
|
import { AxiosResponse } from "axios";
|
||||||
|
import { redirect } from "next/navigation";
|
||||||
|
|
||||||
|
export default async function Layout(props: { children: React.ReactNode }) {
|
||||||
|
const setupRes = await internal.get<
|
||||||
|
AxiosResponse<InitialSetupCompleteResponse>
|
||||||
|
>(`/auth/initial-setup-complete`, await authCookieHeader());
|
||||||
|
const complete = setupRes.data.data.complete;
|
||||||
|
if (complete) {
|
||||||
|
redirect("/");
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div>{props.children}</div>;
|
||||||
|
}
|
174
src/app/auth/initial-setup/page.tsx
Normal file
174
src/app/auth/initial-setup/page.tsx
Normal file
|
@ -0,0 +1,174 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import * as z from "zod";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage
|
||||||
|
} from "@/components/ui/form";
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle
|
||||||
|
} from "@/components/ui/card";
|
||||||
|
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||||
|
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
||||||
|
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
import Image from "next/image";
|
||||||
|
import { passwordSchema } from "@server/auth/passwordSchema";
|
||||||
|
|
||||||
|
const formSchema = z
|
||||||
|
.object({
|
||||||
|
email: z.string().email({ message: "Invalid email address" }),
|
||||||
|
password: passwordSchema,
|
||||||
|
confirmPassword: z.string()
|
||||||
|
})
|
||||||
|
.refine((data) => data.password === data.confirmPassword, {
|
||||||
|
path: ["confirmPassword"],
|
||||||
|
message: "Passwords do not match"
|
||||||
|
});
|
||||||
|
|
||||||
|
export default function InitialSetupPage() {
|
||||||
|
const router = useRouter();
|
||||||
|
const api = createApiClient(useEnvContext());
|
||||||
|
const t = useTranslations();
|
||||||
|
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [checking, setChecking] = useState(true);
|
||||||
|
|
||||||
|
const form = useForm<z.infer<typeof formSchema>>({
|
||||||
|
resolver: zodResolver(formSchema),
|
||||||
|
defaultValues: {
|
||||||
|
email: "",
|
||||||
|
password: "",
|
||||||
|
confirmPassword: ""
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
async function onSubmit(values: z.infer<typeof formSchema>) {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
|
const res = await api.put("/auth/set-server-admin", {
|
||||||
|
email: values.email,
|
||||||
|
password: values.password
|
||||||
|
});
|
||||||
|
if (res && res.status === 200) {
|
||||||
|
router.replace("/");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
setError(formatAxiosError(e, t("setupErrorCreateAdmin")));
|
||||||
|
}
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="w-full max-w-md mx-auto mt-12">
|
||||||
|
<CardHeader>
|
||||||
|
<div className="flex flex-row items-center justify-center">
|
||||||
|
<Image
|
||||||
|
src="/logo/pangolin_orange.svg"
|
||||||
|
alt={t("pangolinLogoAlt")}
|
||||||
|
width={100}
|
||||||
|
height={100}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="text-center space-y-1">
|
||||||
|
<h1 className="text-2xl font-bold mt-1">
|
||||||
|
{t("initialSetupTitle")}
|
||||||
|
</h1>
|
||||||
|
<CardDescription>
|
||||||
|
{t("initialSetupDescription")}
|
||||||
|
</CardDescription>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<Form {...form}>
|
||||||
|
<form
|
||||||
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
|
className="space-y-4"
|
||||||
|
>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="email"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>{t("email")}</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
autoComplete="username"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="password"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>{t("password")}</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
type="password"
|
||||||
|
{...field}
|
||||||
|
autoComplete="new-password"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="confirmPassword"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>
|
||||||
|
{t("confirmPassword")}
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
type="password"
|
||||||
|
{...field}
|
||||||
|
autoComplete="new-password"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{error && (
|
||||||
|
<Alert variant="destructive">
|
||||||
|
<AlertDescription>{error}</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
className="w-full"
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
{t("createAdminAccount")}
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
|
@ -11,6 +11,7 @@ import { pullEnv } from "@app/lib/pullEnv";
|
||||||
import { cleanRedirect } from "@app/lib/cleanRedirect";
|
import { cleanRedirect } from "@app/lib/cleanRedirect";
|
||||||
import { Layout } from "@app/components/Layout";
|
import { Layout } from "@app/components/Layout";
|
||||||
import { rootNavItems } from "./navigation";
|
import { rootNavItems } from "./navigation";
|
||||||
|
import { InitialSetupCompleteResponse } from "@server/routers/auth";
|
||||||
|
|
||||||
export const dynamic = "force-dynamic";
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
|
@ -27,6 +28,15 @@ export default async function Page(props: {
|
||||||
const getUser = cache(verifySession);
|
const getUser = cache(verifySession);
|
||||||
const user = await getUser({ skipCheckVerifyEmail: true });
|
const user = await getUser({ skipCheckVerifyEmail: true });
|
||||||
|
|
||||||
|
const setupRes = await internal.get<
|
||||||
|
AxiosResponse<InitialSetupCompleteResponse>
|
||||||
|
>(`/auth/initial-setup-complete`, await authCookieHeader());
|
||||||
|
const complete = setupRes.data.data.complete;
|
||||||
|
if (!complete) {
|
||||||
|
console.log("compelte", complete);
|
||||||
|
redirect("/auth/initial-setup");
|
||||||
|
}
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
if (params.redirect) {
|
if (params.redirect) {
|
||||||
const safe = cleanRedirect(params.redirect);
|
const safe = cleanRedirect(params.redirect);
|
||||||
|
|
|
@ -14,6 +14,8 @@ const alertVariants = cva(
|
||||||
"border-destructive/50 border bg-destructive/10 text-destructive dark:border-destructive [&>svg]:text-destructive",
|
"border-destructive/50 border bg-destructive/10 text-destructive dark:border-destructive [&>svg]:text-destructive",
|
||||||
success:
|
success:
|
||||||
"border-green-500/50 border bg-green-500/10 text-green-500 dark:border-success [&>svg]:text-green-500",
|
"border-green-500/50 border bg-green-500/10 text-green-500 dark:border-success [&>svg]:text-green-500",
|
||||||
|
info:
|
||||||
|
"border-blue-500/50 border bg-blue-500/10 text-blue-500 dark:border-blue-400 [&>svg]:text-blue-500",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue