refactor(auth): extract authentication logic into a dedicated service and configure docker build args
All checks were successful
Deploy Flowstate to VPS / deploy (push) Successful in 7s
All checks were successful
Deploy Flowstate to VPS / deploy (push) Successful in 7s
This commit is contained in:
parent
1b5a532510
commit
f3a526a68c
4 changed files with 125 additions and 122 deletions
|
|
@ -9,6 +9,14 @@ FROM node:20-alpine AS builder
|
|||
WORKDIR /app
|
||||
COPY --from=deps /app/node_modules ./node_modules
|
||||
COPY . .
|
||||
|
||||
# Inject NEXT_PUBLIC_ vars at build time (required for client-side embedding)
|
||||
ARG NEXT_PUBLIC_GUEST_EMAIL
|
||||
ARG NEXT_PUBLIC_GUEST_PASSWORD
|
||||
|
||||
ENV NEXT_PUBLIC_GUEST_EMAIL=$NEXT_PUBLIC_GUEST_EMAIL
|
||||
ENV NEXT_PUBLIC_GUEST_PASSWORD=$NEXT_PUBLIC_GUEST_PASSWORD
|
||||
|
||||
RUN npm run build
|
||||
|
||||
# 3. Run production image (Node.js server)
|
||||
|
|
|
|||
|
|
@ -1,15 +1,14 @@
|
|||
/**
|
||||
* @file (auth)/login/page.tsx
|
||||
* @description Client component providing a user login interface with NextAuth credentials authentication and guest login capabilities.
|
||||
* @description Client component providing a user login interface utilizing the auth service.
|
||||
*/
|
||||
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { signIn } from "next-auth/react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import Link from "next/link";
|
||||
import { getGuestCredentials } from "@/services/auth.service";
|
||||
import { getGuestCredentials, loginUser } from "@/services/auth.service";
|
||||
|
||||
/**
|
||||
* Renders the login page containing the authentication form, error handling,
|
||||
|
|
@ -25,13 +24,13 @@ export default function LoginPage() {
|
|||
>(null);
|
||||
|
||||
/**
|
||||
* Universal sign-in handler for both manual credentials and guest login.
|
||||
* Performs user authentication via email and password using the auth service.
|
||||
* Redirects to the summary page upon success or sets an error message on failure.
|
||||
*
|
||||
* @async
|
||||
* @param {string} email - The user email.
|
||||
* @param {string} password - The user password.
|
||||
* @param {"credentials" | "guest"} type - The login method type for specific loading states.
|
||||
* @returns {Promise<void>}
|
||||
* @param {string} email - The email address for login.
|
||||
* @param {string} password - The account password.
|
||||
* @param {"credentials" | "guest"} type - The authentication trigger source type.
|
||||
*/
|
||||
const handleSignIn = async (
|
||||
email: string,
|
||||
|
|
@ -41,42 +40,21 @@ export default function LoginPage() {
|
|||
setError(null);
|
||||
setLoadingType(type);
|
||||
|
||||
const defaultErrorMessage = "An unexpected error has occurred.";
|
||||
const result = await loginUser(email, password);
|
||||
|
||||
try {
|
||||
const res = await signIn("credentials", {
|
||||
email,
|
||||
password,
|
||||
redirect: false,
|
||||
});
|
||||
|
||||
if (res?.error) {
|
||||
if (res.error === "CredentialsSignin") {
|
||||
setError("Incorrect email address or password.");
|
||||
} else {
|
||||
setError(defaultErrorMessage);
|
||||
}
|
||||
} else {
|
||||
router.push("/summary");
|
||||
return;
|
||||
}
|
||||
} catch (err: any) {
|
||||
if (err?.message?.includes("fetch") || err?.name === "TypeError") {
|
||||
setError("Server error: API endpoint not available");
|
||||
} else {
|
||||
setError(defaultErrorMessage);
|
||||
}
|
||||
} finally {
|
||||
if (result.error) {
|
||||
setError(result.error);
|
||||
setLoadingType(null);
|
||||
} else {
|
||||
router.push("/summary");
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles regular form submission.
|
||||
* Handles traditional credential-based form submissions.
|
||||
*
|
||||
* @async
|
||||
* @param {React.SubmitEvent<HTMLFormElement>} e - The form submission event.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
const handleSubmit = async (e: React.SubmitEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
|
|
@ -87,48 +65,33 @@ export default function LoginPage() {
|
|||
};
|
||||
|
||||
/**
|
||||
* Handles quick guest login securely via Server Action.
|
||||
* Fetches guest user credentials and triggers guest authentication.
|
||||
*
|
||||
* @async
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
const handleGuestLogin = async () => {
|
||||
setError(null);
|
||||
setLoadingType("guest");
|
||||
|
||||
try {
|
||||
const result = await getGuestCredentials();
|
||||
const result = await getGuestCredentials();
|
||||
|
||||
if ("error" in result && result.error) {
|
||||
setError(result.error);
|
||||
setLoadingType(null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!result.email || !result.password) {
|
||||
setError("Guest login is not configured properly.");
|
||||
setLoadingType(null);
|
||||
return;
|
||||
}
|
||||
|
||||
await handleSignIn(result.email, result.password, "guest");
|
||||
} catch (err) {
|
||||
setError("Failed to initialize guest login.");
|
||||
if (result.error || !result.email || !result.password) {
|
||||
setError(result.error || "Guest login is not configured properly.");
|
||||
setLoadingType(null);
|
||||
return;
|
||||
}
|
||||
|
||||
await handleSignIn(result.email, result.password, "guest");
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-dvh w-full items-center justify-center">
|
||||
{/* Login Card Container */}
|
||||
<div className="w-full max-w-sm p-6 space-y-4 border border-border rounded-xl bg-card backdrop-blur-md">
|
||||
{/* Header Section */}
|
||||
<div className="space-y-2 text-center">
|
||||
<h1 className="text-2xl font-bold tracking-tight">Flowstate Login</h1>
|
||||
<p className="text-sm text-foreground-muted">Sign in to continue.</p>
|
||||
</div>
|
||||
|
||||
{/* Error Notification Banner */}
|
||||
{error && (
|
||||
<div
|
||||
className="p-3 text-xs font-medium text-destructive bg-destructive-bg border border-destructive-border rounded-lg text-center"
|
||||
|
|
@ -138,7 +101,6 @@ export default function LoginPage() {
|
|||
</div>
|
||||
)}
|
||||
|
||||
{/* Credentials Form */}
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<label className="block space-y-2">
|
||||
<span className="text-xs font-medium text-foreground-muted">
|
||||
|
|
@ -175,7 +137,6 @@ export default function LoginPage() {
|
|||
</button>
|
||||
</form>
|
||||
|
||||
{/* Divider */}
|
||||
<div className="relative flex items-center">
|
||||
<div className="grow border-t border-border"></div>
|
||||
<span className="shrink mx-4 text-xs text-foreground-muted uppercase">
|
||||
|
|
@ -184,7 +145,6 @@ export default function LoginPage() {
|
|||
<div className="grow border-t border-border"></div>
|
||||
</div>
|
||||
|
||||
{/* Guest Login Button */}
|
||||
<button
|
||||
type="button"
|
||||
disabled={loadingType !== null}
|
||||
|
|
@ -196,7 +156,6 @@ export default function LoginPage() {
|
|||
: "Sign in as Guest"}
|
||||
</button>
|
||||
|
||||
{/* Registration Link Footer */}
|
||||
<div className="text-center text-xs text-foreground-muted">
|
||||
Don't have an account yet?{" "}
|
||||
<Link href="/register/" className="text-foreground hover:underline">
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
/**
|
||||
* @file RegisterPage.tsx
|
||||
* @description Client component providing a user registration interface for creating a new account.
|
||||
* @file (auth)/register/page.tsx
|
||||
* @description Client component providing a user registration interface utilizing the auth service.
|
||||
*/
|
||||
|
||||
"use client";
|
||||
|
|
@ -8,10 +8,11 @@
|
|||
import Link from "next/link";
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { registerUser } from "@/services/auth.service";
|
||||
|
||||
/**
|
||||
* Renders the registration page featuring a sign-up form, error feedback,
|
||||
* and routing logic to the login page upon successful account creation.
|
||||
* and routing logic to the summary view upon successful account creation.
|
||||
*
|
||||
* @returns {JSX.Element} The rendered registration page component.
|
||||
*/
|
||||
|
|
@ -21,12 +22,11 @@ export default function RegisterPage() {
|
|||
const router = useRouter();
|
||||
|
||||
/**
|
||||
* Handles the submission of the registration form, sends user data to the API,
|
||||
* creates the account, auto-logins the user, and redirects to the main view on success.
|
||||
* Handles user registration form submission, validates form data via auth service,
|
||||
* and navigates to summary page on successful registration.
|
||||
*
|
||||
* @async
|
||||
* @param {React.SubmitEvent<HTMLFormElement>} event - The form submission event.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function handleSubmit(event: React.SubmitEvent<HTMLFormElement>) {
|
||||
event.preventDefault();
|
||||
|
|
@ -34,61 +34,21 @@ export default function RegisterPage() {
|
|||
setLoading(true);
|
||||
|
||||
const formData = new FormData(event.currentTarget);
|
||||
const firstName = formData.get("firstName");
|
||||
const lastName = formData.get("lastName");
|
||||
const email = formData.get("email");
|
||||
const password = formData.get("password");
|
||||
const confirmPassword = formData.get("confirmPassword");
|
||||
const payload = Object.fromEntries(formData.entries());
|
||||
|
||||
if (password !== confirmPassword) {
|
||||
setError("Passwords do not match");
|
||||
const result = await registerUser(payload);
|
||||
|
||||
if (result.error) {
|
||||
setError(result.error);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Send registration request to the API
|
||||
const response = await fetch("/api/auth/register", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
firstName,
|
||||
lastName,
|
||||
email,
|
||||
password,
|
||||
confirmPassword,
|
||||
}),
|
||||
});
|
||||
|
||||
// Ensure the response is valid JSON before parsing
|
||||
const contentType = response.headers.get("content-type");
|
||||
let data;
|
||||
if (contentType && contentType.includes("application/json")) {
|
||||
data = await response.json();
|
||||
} else {
|
||||
throw new Error("Server error: API endpoint not available");
|
||||
}
|
||||
|
||||
// Handle non-successful status codes
|
||||
if (!response.ok) {
|
||||
throw new Error(data.message || "Something went wrong.");
|
||||
}
|
||||
|
||||
// Redirect user
|
||||
} else {
|
||||
router.push("/summary");
|
||||
return;
|
||||
} catch (err: any) {
|
||||
setError(err.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-dvh w-full items-center justify-center">
|
||||
{/* Login Card Container */}
|
||||
<div className="w-full max-w-sm p-6 space-y-6 border border-border rounded-xl bg-card backdrop-blur-md">
|
||||
{/* Header Section */}
|
||||
<div className="space-y-2 text-center">
|
||||
<h1 className="text-2xl font-bold tracking-tight">
|
||||
Create an Account
|
||||
|
|
@ -98,7 +58,6 @@ export default function RegisterPage() {
|
|||
</p>
|
||||
</div>
|
||||
|
||||
{/* Error Notification Banner */}
|
||||
{error && (
|
||||
<div
|
||||
className="p-3 text-xs font-medium text-destructive bg-destructive-bg border border-destructive-border rounded-lg text-center"
|
||||
|
|
@ -108,7 +67,6 @@ export default function RegisterPage() {
|
|||
</div>
|
||||
)}
|
||||
|
||||
{/* Credentials Form */}
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<input
|
||||
|
|
@ -156,7 +114,6 @@ export default function RegisterPage() {
|
|||
</button>
|
||||
</form>
|
||||
|
||||
{/* Back Link Footer */}
|
||||
<div className="text-center text-xs text-foreground-muted">
|
||||
Already have an account?{" "}
|
||||
<Link href="/login" className="text-foreground hover:underline">
|
||||
|
|
|
|||
|
|
@ -1,18 +1,19 @@
|
|||
/**
|
||||
* @file services/auth.service.ts
|
||||
* @description Authentication service providing helper functions for retrieving authentication details such as guest credentials from environment variables.
|
||||
* @description Authentication service providing helper functions for guest credentials, sign-in, and registration.
|
||||
*/
|
||||
|
||||
import { signIn } from "next-auth/react";
|
||||
|
||||
/**
|
||||
* Retrieves configured guest account credentials from server or public environment variables.
|
||||
* Retrieves configured public guest credentials from environment variables.
|
||||
*
|
||||
* @async
|
||||
* @returns {Promise<{ email?: string; password?: string; error?: string }>} An object containing the guest email and password, or an error message if unconfigured.
|
||||
* @returns {Promise<{ email?: string; password?: string; error?: string }>} An object containing guest credentials or an error message if unconfigured.
|
||||
*/
|
||||
export async function getGuestCredentials() {
|
||||
const email = process.env.GUEST_EMAIL || process.env.NEXT_PUBLIC_GUEST_EMAIL;
|
||||
const password =
|
||||
process.env.GUEST_PASSWORD || process.env.NEXT_PUBLIC_GUEST_PASSWORD;
|
||||
const email = process.env.NEXT_PUBLIC_GUEST_EMAIL;
|
||||
const password = process.env.NEXT_PUBLIC_GUEST_PASSWORD;
|
||||
|
||||
if (!email || !password) {
|
||||
return { error: "Guest login is not configured on the server." };
|
||||
|
|
@ -20,3 +21,81 @@ export async function getGuestCredentials() {
|
|||
|
||||
return { email, password };
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticates a user using credentials via NextAuth.
|
||||
*
|
||||
* @async
|
||||
* @param {string} email - The user's email address.
|
||||
* @param {string} password - The user's account password.
|
||||
* @returns {Promise<{ success?: boolean; error?: string }>} An object indicating success or describing the authentication error.
|
||||
*/
|
||||
export async function loginUser(email: string, password: string) {
|
||||
try {
|
||||
const res = await signIn("credentials", {
|
||||
email,
|
||||
password,
|
||||
redirect: false,
|
||||
});
|
||||
|
||||
if (res?.error) {
|
||||
if (res.error === "CredentialsSignin") {
|
||||
return { error: "Incorrect email address or password." };
|
||||
}
|
||||
return { error: "An unexpected error has occurred." };
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
} catch (err: any) {
|
||||
if (
|
||||
err?.message?.includes("fetch") ||
|
||||
err?.name === "TypeError" ||
|
||||
err?.message?.includes("network")
|
||||
) {
|
||||
return { error: "Server error: API endpoint not available" };
|
||||
}
|
||||
return { error: err?.message || "An unexpected error has occurred." };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a new user account via the registration API endpoint.
|
||||
*
|
||||
* @async
|
||||
* @param {Record<string, any>} payload - The user registration form payload containing user details and passwords.
|
||||
* @returns {Promise<{ success?: boolean; error?: string }>} An object indicating success or describing the registration error.
|
||||
*/
|
||||
export async function registerUser(payload: Record<string, any>) {
|
||||
if (payload.password !== payload.confirmPassword) {
|
||||
return { error: "Passwords do not match" };
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch("/api/auth/register", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
|
||||
const contentType = response.headers.get("content-type");
|
||||
let data;
|
||||
|
||||
if (contentType && contentType.includes("application/json")) {
|
||||
try {
|
||||
data = await response.json();
|
||||
} catch (jsonErr) {
|
||||
return { error: "Server error: Invalid response format" };
|
||||
}
|
||||
} else {
|
||||
throw new Error("Server error: API endpoint not available");
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.message || "Something went wrong.");
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
} catch (err: any) {
|
||||
return { error: err.message || "Something went wrong." };
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue