feat: Add initial admin password setup flow
All checks were successful
Automated Container Build / build-and-push (push) Successful in 54s
All checks were successful
Automated Container Build / build-and-push (push) Successful in 54s
This commit is contained in:
parent
f8de1a4380
commit
577a189e0b
3 changed files with 65 additions and 12 deletions
|
|
@ -52,19 +52,30 @@ export async function POST(request: NextRequest) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify password
|
// Verify password
|
||||||
const passwordHash = process.env.ADMIN_PASSWORD_HASH;
|
const setting = await prisma.setting.findUnique({
|
||||||
|
where: { key: "admin_password_hash" },
|
||||||
|
});
|
||||||
|
|
||||||
// For development: if no hash is set, accept any non-empty password
|
|
||||||
let isValid = false;
|
let isValid = false;
|
||||||
if (!passwordHash) {
|
|
||||||
isValid = body.password.length > 0;
|
if (!setting) {
|
||||||
|
// Initial setup: hash and save the new password
|
||||||
|
const argon2 = await import("argon2");
|
||||||
|
const newHash = await argon2.hash(body.password);
|
||||||
|
await prisma.setting.create({
|
||||||
|
data: {
|
||||||
|
key: "admin_password_hash",
|
||||||
|
value: newHash,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
isValid = true;
|
||||||
} else {
|
} else {
|
||||||
// Dynamic import to avoid issues when argon2 isn't installed
|
// Verify existing password
|
||||||
|
const passwordHash = setting.value;
|
||||||
try {
|
try {
|
||||||
const argon2 = await import("argon2");
|
const argon2 = await import("argon2");
|
||||||
isValid = await argon2.verify(passwordHash, body.password);
|
isValid = await argon2.verify(passwordHash, body.password);
|
||||||
} catch {
|
} catch {
|
||||||
// Fallback: direct comparison (only for development)
|
|
||||||
isValid = body.password === passwordHash;
|
isValid = body.password === passwordHash;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
18
src/app/api/auth/setup-status/route.ts
Normal file
18
src/app/api/auth/setup-status/route.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
import { NextResponse } from "next/server";
|
||||||
|
import { prisma } from "@/lib/db";
|
||||||
|
|
||||||
|
export async function GET() {
|
||||||
|
try {
|
||||||
|
const setting = await prisma.setting.findUnique({
|
||||||
|
where: { key: "admin_password_hash" },
|
||||||
|
});
|
||||||
|
|
||||||
|
return NextResponse.json({ setupRequired: !setting });
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to check setup status:", error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Failed to check setup status" },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,14 +1,22 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
|
|
||||||
export default function LoginPage() {
|
export default function LoginPage() {
|
||||||
const [password, setPassword] = useState("");
|
const [password, setPassword] = useState("");
|
||||||
const [error, setError] = useState("");
|
const [error, setError] = useState("");
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [isSetup, setIsSetup] = useState<boolean | null>(null);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetch("/api/auth/setup-status")
|
||||||
|
.then((res) => res.json())
|
||||||
|
.then((data) => setIsSetup(data.setupRequired))
|
||||||
|
.catch(() => setIsSetup(false)); // fallback
|
||||||
|
}, []);
|
||||||
|
|
||||||
async function handleSubmit(e: React.FormEvent) {
|
async function handleSubmit(e: React.FormEvent) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setError("");
|
setError("");
|
||||||
|
|
@ -37,9 +45,19 @@ export default function LoginPage() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isSetup === null) {
|
||||||
|
return (
|
||||||
|
<div className="flex min-h-screen items-center justify-center p-4">
|
||||||
|
<div className="animate-pulse flex items-center justify-center">
|
||||||
|
<div className="w-8 h-8 border-4 border-primary border-t-transparent rounded-full animate-spin"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-screen items-center justify-center p-4">
|
<div className="flex min-h-screen items-center justify-center p-4">
|
||||||
<div className="w-full max-w-sm">
|
<div className="w-full max-w-sm animate-fade-in">
|
||||||
{/* Logo / Title area */}
|
{/* Logo / Title area */}
|
||||||
<div className="text-center mb-8">
|
<div className="text-center mb-8">
|
||||||
<div className="inline-flex items-center justify-center w-16 h-16 rounded-2xl bg-primary/10 mb-4">
|
<div className="inline-flex items-center justify-center w-16 h-16 rounded-2xl bg-primary/10 mb-4">
|
||||||
|
|
@ -57,9 +75,13 @@ export default function LoginPage() {
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<h1 className="text-2xl font-bold text-text-heading">Study App</h1>
|
<h1 className="text-2xl font-bold text-text-heading">
|
||||||
|
{isSetup ? "Welcome to Study App!" : "Study App"}
|
||||||
|
</h1>
|
||||||
<p className="text-text-muted mt-1 text-sm">
|
<p className="text-text-muted mt-1 text-sm">
|
||||||
Enter your password to continue
|
{isSetup
|
||||||
|
? "Please set your admin password for the first time."
|
||||||
|
: "Enter your password to continue"}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -78,7 +100,7 @@ export default function LoginPage() {
|
||||||
type="password"
|
type="password"
|
||||||
value={password}
|
value={password}
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
placeholder="Enter your password"
|
placeholder={isSetup ? "Create a new password" : "Enter your password"}
|
||||||
autoFocus
|
autoFocus
|
||||||
className="w-full px-3.5 py-2.5 rounded-lg border border-border bg-bg-surface-alt/50 text-text-heading placeholder:text-text-muted focus:outline-none focus:ring-2 focus:ring-primary/30 focus:border-primary transition-all duration-200"
|
className="w-full px-3.5 py-2.5 rounded-lg border border-border bg-bg-surface-alt/50 text-text-heading placeholder:text-text-muted focus:outline-none focus:ring-2 focus:ring-primary/30 focus:border-primary transition-all duration-200"
|
||||||
/>
|
/>
|
||||||
|
|
@ -129,8 +151,10 @@ export default function LoginPage() {
|
||||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
Signing in...
|
{isSetup ? "Saving..." : "Signing in..."}
|
||||||
</span>
|
</span>
|
||||||
|
) : isSetup ? (
|
||||||
|
"Save Password & Login"
|
||||||
) : (
|
) : (
|
||||||
"Sign In"
|
"Sign In"
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue