Skip to content

Commit 5bfcfd6

Browse files
committed
Add basic auth pages with minimal handling
1 parent 05f5eae commit 5bfcfd6

File tree

10 files changed

+390
-6
lines changed

10 files changed

+390
-6
lines changed

peerprep/api/gateway.ts

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
1-
import { Question, StatusBody, QuestionFullBody } from "./structs";
1+
import { SafeParseReturnType, SafeParseSuccess } from "zod";
2+
import {
3+
Question,
4+
StatusBody,
5+
QuestionFullBody,
6+
LoginResponse,
7+
SigninResponse,
8+
} from "./structs";
29

310
const questions: { [key: string]: Question } = {
411
"0": {
@@ -123,3 +130,59 @@ export async function getAllQuestions(): Promise<Question[] | StatusBody> {
123130
return { error: err.message, status: 400 };
124131
}
125132
}
133+
134+
export async function getSessionLogin(
135+
validatedFields: {
136+
email: string;
137+
password: string;
138+
}
139+
): Promise<LoginResponse | StatusBody> {
140+
try {
141+
const res = await fetch(`${process.env.NEXT_PUBLIC_USER_SERVICE}/auth/login`, {
142+
method: "POST",
143+
body: JSON.stringify(validatedFields),
144+
headers: {
145+
"Content-type": "application/json; charset=UTF-8",
146+
}
147+
});
148+
const json = await res.json();
149+
150+
if (!res.ok) {
151+
// TODO: handle not OK
152+
return { error: json.message, status: res.status };
153+
}
154+
// TODO: handle OK
155+
return json;
156+
} catch (err: any) {
157+
return { error: err.message, status: 400 };
158+
}
159+
}
160+
161+
export async function postSignupUser(
162+
validatedFields: {
163+
username: string;
164+
email: string;
165+
password: string;
166+
}
167+
): Promise<SigninResponse | StatusBody> {
168+
try {
169+
console.log(JSON.stringify(validatedFields));
170+
const res = await fetch(`${process.env.NEXT_PUBLIC_USER_SERVICE}/users`, {
171+
method: "POST",
172+
body: JSON.stringify(validatedFields),
173+
headers: {
174+
"Content-type": "application/json; charset=UTF-8",
175+
}
176+
});
177+
const json = await res.json();
178+
179+
if (!res.ok) {
180+
// TODO: handle not OK
181+
return { error: json.message, status: res.status };
182+
}
183+
// TODO: handle OK
184+
return json;
185+
} catch (err: any) {
186+
return { error: err.message, status: 400 };
187+
}
188+
}

peerprep/api/structs.ts

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { z } from "zod";
2+
13
export enum Difficulty {
24
All = 0,
35
Easy = 1,
@@ -29,8 +31,72 @@ export interface StatusBody {
2931
error?: string;
3032
}
3133

34+
export interface UserData {
35+
id: string;
36+
username: string;
37+
email: string;
38+
isAdmin: boolean;
39+
createdAt: number;
40+
}
41+
42+
export interface UserDataAccessToken extends UserData {
43+
accessToken: string;
44+
}
45+
46+
export interface LoginResponse {
47+
message: string;
48+
data: UserDataAccessToken;
49+
}
50+
51+
export interface SigninResponse {
52+
message: string;
53+
data: UserData;
54+
}
55+
56+
// credit - taken from Next.JS Auth tutorial
57+
export type FormState =
58+
| {
59+
errors?: {
60+
name?: string[];
61+
email?: string[];
62+
password?: string[];
63+
};
64+
message?: string;
65+
}
66+
| undefined;
67+
68+
export const SignupFormSchema = z.object({
69+
username: z
70+
.string()
71+
.min(2, { message: "Name must be at least 2 characters long." })
72+
.trim(),
73+
email: z.string().email({ message: "Please enter a valid email." }).trim(),
74+
password: z
75+
.string()
76+
.min(8, { message: "Be at least 8 characters long" })
77+
.regex(/[a-zA-Z]/, { message: "Contain at least one letter." })
78+
.regex(/[0-9]/, { message: "Contain at least one number." })
79+
.regex(/[^a-zA-Z0-9]/, {
80+
message: "Contain at least one special character.",
81+
})
82+
.trim(),
83+
});
84+
85+
export const LoginFormSchema = z.object({
86+
email: z.string().email({ message: "Please enter a valid email." }).trim(),
87+
password: z
88+
.string()
89+
.min(8, { message: "Be at least 8 characters long" })
90+
.regex(/[a-zA-Z]/, { message: "Contain at least one letter." })
91+
.regex(/[0-9]/, { message: "Contain at least one number." })
92+
.regex(/[^a-zA-Z0-9]/, {
93+
message: "Contain at least one special character.",
94+
})
95+
.trim(),
96+
});
97+
3298
export function isError(
33-
obj: Question[] | Question | StatusBody
99+
obj: any | StatusBody
34100
): obj is StatusBody {
35101
return (obj as StatusBody).status !== undefined;
36102
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
"use server"
2+
import { getSessionLogin, postSignupUser } from '@/api/gateway';
3+
// defines the server-sided login action.
4+
import { SignupFormSchema, LoginFormSchema, FormState, isError } from '@/api/structs';
5+
import { createSession } from '@/app/actions/session';
6+
import { cookies } from 'next/headers';
7+
import { redirect } from 'next/navigation';
8+
9+
// credit - taken from Next.JS Auth tutorial
10+
export async function signup(state: FormState, formData: FormData) {
11+
// Validate form fields
12+
const validatedFields = SignupFormSchema.safeParse({
13+
username: formData.get('username'),
14+
email: formData.get('email'),
15+
password: formData.get('password'),
16+
});
17+
18+
// If any form fields are invalid, return early
19+
if (!validatedFields.success) {
20+
return {
21+
errors: validatedFields.error.flatten().fieldErrors,
22+
}
23+
}
24+
25+
const json = await postSignupUser(validatedFields.data);
26+
27+
if (!isError(json)) {
28+
// TODO: handle OK
29+
redirect("/auth/login");
30+
} else {
31+
// TODO: handle failure codes: 400, 409, 500.
32+
console.log(`${json.status}: ${json.error}`);
33+
}
34+
}
35+
36+
export async function login(state: FormState, formData: FormData) {
37+
// Validate form fields
38+
const validatedFields = LoginFormSchema.safeParse({
39+
email: formData.get('email'),
40+
password: formData.get('password'),
41+
});
42+
43+
// If any form fields are invalid, return early
44+
if (!validatedFields.success) {
45+
return {
46+
errors: validatedFields.error.flatten().fieldErrors,
47+
}
48+
}
49+
50+
const json = await getSessionLogin(validatedFields.data);
51+
if (!isError(json)) {
52+
if (cookies().has('session')) {
53+
console.log(cookies().get('session'));
54+
console.log("Note a cookie already exists, overriding!");
55+
}
56+
await createSession(json.data.accessToken);
57+
58+
if (cookies().has('session')) {
59+
console.log(`New cookie: ${cookies().get('session')?.value}`);
60+
}
61+
} else {
62+
console.log(json.error);
63+
}
64+
}

peerprep/app/actions/session.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import 'server-only';
2+
import { cookies } from 'next/headers';
3+
4+
export async function createSession(accessToken: string) {
5+
const expiresAt = new Date(Date.now() + 24 * 60 * 60 * 1000);
6+
cookies().set('session', accessToken, {
7+
httpOnly: true,
8+
secure: true,
9+
expires: expiresAt,
10+
sameSite: 'lax',
11+
path: '/',
12+
})
13+
}

peerprep/app/auth/login/page.tsx

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
"use client";
2+
import React from "react";
3+
import style from "@/style/form.module.css";
4+
import { useFormState, useFormStatus } from "react-dom";
5+
import FormTextInput from "@/components/shared/form/FormTextInput";
6+
import FormPasswordInput from "@/components/shared/form/FormPasswordInput";
7+
import { login } from "@/app/actions/server_actions";
8+
import Link from "next/link";
9+
10+
type Props = {}
11+
12+
function LoginPage({}: Props) {
13+
const [state, action] = useFormState(login, undefined);
14+
return (
15+
// we can actually use server actions to auth the user... maybe we can
16+
// change our AddQn action too.
17+
<div className={style.wrapper}>
18+
<form className={style.form_container} action={action}>
19+
<h1 className={style.title}>Login to PeerPrep</h1>
20+
<FormTextInput
21+
required
22+
label="Email:"
23+
name="email"
24+
/>
25+
{state?.errors?.email && <p>{state.errors.email}</p>}
26+
<FormPasswordInput
27+
required
28+
label="Password:"
29+
name="password"
30+
/>
31+
{state?.errors?.password && (
32+
<div>
33+
<p>Password must:</p>
34+
<ul>
35+
{state.errors.password.map((error) => (
36+
<li key={error}>- {error}</li>
37+
))}
38+
</ul>
39+
</div>
40+
)}
41+
<SubmitButton />
42+
<p>No account? <Link href="/auth/register">Register here.</Link></p>
43+
</form>
44+
</div>
45+
)
46+
}
47+
48+
function SubmitButton() {
49+
const { pending } = useFormStatus()
50+
51+
return (
52+
<button disabled={pending} type="submit">
53+
Login
54+
</button>
55+
)
56+
}
57+
58+
export default LoginPage;
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
"use client";
2+
import React from "react";
3+
import style from "@/style/form.module.css";
4+
import { useFormState, useFormStatus } from "react-dom";
5+
import FormTextInput from "@/components/shared/form/FormTextInput";
6+
import FormPasswordInput from "@/components/shared/form/FormPasswordInput";
7+
import { signup } from "@/app/actions/server_actions";
8+
import Link from "next/link";
9+
10+
type Props = {}
11+
12+
function RegisterPage({}: Props) {
13+
const [state, action] = useFormState(signup, undefined);
14+
return (
15+
// we can actually use server actions to auth the user... maybe we can
16+
// change our AddQn action too.
17+
<div className={style.wrapper}>
18+
<form className={style.form_container} action={action}>
19+
<h1 className={style.title}>Sign up for an account</h1>
20+
<FormTextInput
21+
required
22+
label="Username:"
23+
name="username"
24+
/>
25+
{state?.errors?.username && <p>{state.errors.username}</p>}
26+
<FormTextInput
27+
required
28+
label="Email:"
29+
name="email"
30+
/>
31+
{state?.errors?.email && <p>{state.errors.email}</p>}
32+
<FormPasswordInput
33+
required
34+
label="Password:"
35+
name="password"
36+
/>
37+
{state?.errors?.password && (
38+
<div>
39+
<p>Password must:</p>
40+
<ul>
41+
{state.errors.password.map((error) => (
42+
<li key={error}>- {error}</li>
43+
))}
44+
</ul>
45+
</div>
46+
)}
47+
<SubmitButton />
48+
<p>Have an account? <Link href="/auth/login">Login.</Link></p>
49+
</form>
50+
</div>
51+
)
52+
}
53+
54+
function SubmitButton() {
55+
const { pending } = useFormStatus()
56+
57+
return (
58+
<button disabled={pending} type="submit">
59+
Sign up
60+
</button>
61+
)
62+
}
63+
64+
export default RegisterPage;

0 commit comments

Comments
 (0)