Skip to content

Commit 723b321

Browse files
authored
Merge pull request #22 from CS3219-AY2425S1/login-page
Add Register, Login Pages and Refactor out routes used by client components
2 parents 5c476f5 + 3681298 commit 723b321

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+3302
-101
lines changed

peerprep/api/gateway.ts

Lines changed: 53 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,29 @@
1-
import { Question, StatusBody, QuestionFullBody } from "./structs";
1+
import { cookies } from "next/headers";
2+
import { Question, StatusBody, LoginResponse, SigninResponse } from "./structs";
23

3-
const questions: { [key: string]: Question } = {
4-
"0": {
5-
id: 0,
6-
difficulty: 2,
7-
title: "Two Sum",
8-
description:
9-
"Given an array of integers, return indices of the two numbers such that they add up to a specific target.",
10-
categories: ["Hash Table", "Array"],
11-
test_cases: {
12-
"[2, 7, 11, 15], 9": "[0, 1]",
13-
"[3, 2, 4], 6": "[1, 2]",
14-
"[3, 3], 6": "[0, 1]",
15-
},
16-
},
17-
"1": {
18-
id: 1,
19-
difficulty: 1,
20-
title: "Reverse Integer",
21-
description: "Given a 32-bit signed integer, reverse digits of an integer.",
22-
categories: ["Math"],
23-
test_cases: {
24-
"123": "321",
25-
"1": "1",
26-
"22": "22",
27-
},
28-
},
29-
};
4+
export function generateAuthHeaders() {
5+
return {
6+
Authorization: `Bearer ${cookies().get("session")}`,
7+
};
8+
}
9+
10+
export function generateJSONHeaders() {
11+
return {
12+
...generateAuthHeaders(),
13+
"Content-type": "application/json; charset=UTF-8",
14+
};
15+
}
3016

3117
export async function fetchQuestion(
3218
questionId: string
3319
): Promise<Question | StatusBody> {
34-
// remove this when services are up
35-
if (process.env.DEV_ENV === "dev") {
36-
return questions[questionId] === undefined
37-
? { error: "Question not found", status: 404 }
38-
: questions[questionId];
39-
}
4020
try {
4121
const response = await fetch(
42-
`${process.env.NEXT_PUBLIC_QUESTION_SERVICE}/questions/solve/${questionId}`
22+
`${process.env.NEXT_PUBLIC_QUESTION_SERVICE}/questions/solve/${questionId}`,
23+
{
24+
method: "GET",
25+
headers: generateAuthHeaders(),
26+
}
4327
);
4428
if (!response.ok) {
4529
return {
@@ -53,72 +37,56 @@ export async function fetchQuestion(
5337
}
5438
}
5539

56-
export async function addQuestion(body: QuestionFullBody): Promise<StatusBody> {
40+
export async function getSessionLogin(validatedFields: {
41+
email: string;
42+
password: string;
43+
}): Promise<LoginResponse | StatusBody> {
5744
try {
58-
const response = await fetch(
59-
`${process.env.NEXT_PUBLIC_QUESTION_SERVICE}/questions`,
45+
const res = await fetch(
46+
`${process.env.NEXT_PUBLIC_USER_SERVICE}/auth/login`,
6047
{
6148
method: "POST",
62-
body: JSON.stringify(body).replace(
63-
/(\"difficulty\":)\"([1-3])\"/,
64-
`$1$2`
65-
),
49+
body: JSON.stringify(validatedFields),
6650
headers: {
6751
"Content-type": "application/json; charset=UTF-8",
6852
},
6953
}
7054
);
71-
if (response.ok) {
72-
return {
73-
status: response.status,
74-
};
75-
}
76-
return {
77-
error: (await response.json())["Error adding question: "],
78-
status: response.status,
79-
};
80-
} catch (err: any) {
81-
return { error: err.message, status: 0 };
82-
}
83-
}
55+
const json = await res.json();
8456

85-
export async function deleteQuestion(question: Question): Promise<StatusBody> {
86-
try {
87-
const response = await fetch(
88-
`${process.env.NEXT_PUBLIC_QUESTION_SERVICE}/questions/delete/${question.id}`,
89-
{
90-
method: "DELETE",
91-
headers: {
92-
"Content-type": "application/json; charset=UTF-8",
93-
},
94-
}
95-
);
96-
if (response.ok) {
97-
return {
98-
status: response.status,
99-
};
57+
if (!res.ok) {
58+
// TODO: handle not OK
59+
return { error: json.message, status: res.status };
10060
}
101-
return {
102-
error: (await response.json())["Error deleting question: "],
103-
status: response.status,
104-
};
61+
// TODO: handle OK
62+
return json;
10563
} catch (err: any) {
106-
return { error: err.message, status: 0 };
64+
return { error: err.message, status: 400 };
10765
}
10866
}
10967

110-
export async function getAllQuestions(): Promise<Question[] | StatusBody> {
68+
export async function postSignupUser(validatedFields: {
69+
username: string;
70+
email: string;
71+
password: string;
72+
}): Promise<SigninResponse | StatusBody> {
11173
try {
112-
const response = await fetch(
113-
`${process.env.NEXT_PUBLIC_QUESTION_SERVICE}/questions`
114-
);
115-
if (!response.ok) {
116-
return {
117-
error: await response.text(),
118-
status: response.status,
119-
};
74+
console.log(JSON.stringify(validatedFields));
75+
const res = await fetch(`${process.env.NEXT_PUBLIC_USER_SERVICE}/users`, {
76+
method: "POST",
77+
body: JSON.stringify(validatedFields),
78+
headers: {
79+
"Content-type": "application/json; charset=UTF-8",
80+
},
81+
});
82+
const json = await res.json();
83+
84+
if (!res.ok) {
85+
// TODO: handle not OK
86+
return { error: json.message, status: res.status };
12087
}
121-
return (await response.json()) as Question[];
88+
// TODO: handle OK
89+
return json;
12290
} catch (err: any) {
12391
return { error: err.message, status: 400 };
12492
}

peerprep/api/structs.ts

Lines changed: 67 additions & 3 deletions
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,70 @@ export interface StatusBody {
2931
error?: string;
3032
}
3133

32-
export function isError(
33-
obj: Question[] | Question | StatusBody
34-
): obj is StatusBody {
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+
98+
export function isError(obj: any | StatusBody): obj is StatusBody {
3599
return (obj as StatusBody).status !== undefined;
36100
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
"use server";
2+
import { getSessionLogin, postSignupUser } from "@/api/gateway";
3+
// defines the server-sided login action.
4+
import {
5+
SignupFormSchema,
6+
LoginFormSchema,
7+
FormState,
8+
isError,
9+
} from "@/api/structs";
10+
import { createSession } from "@/app/actions/session";
11+
import { redirect } from "next/navigation";
12+
13+
// credit - taken from Next.JS Auth tutorial
14+
export async function signup(state: FormState, formData: FormData) {
15+
// Validate form fields
16+
const validatedFields = SignupFormSchema.safeParse({
17+
username: formData.get("username"),
18+
email: formData.get("email"),
19+
password: formData.get("password"),
20+
});
21+
22+
// If any form fields are invalid, return early
23+
if (!validatedFields.success) {
24+
return {
25+
errors: validatedFields.error.flatten().fieldErrors,
26+
};
27+
}
28+
29+
const json = await postSignupUser(validatedFields.data);
30+
31+
if (!isError(json)) {
32+
// TODO: handle OK
33+
redirect("/auth/login");
34+
} else {
35+
// TODO: handle failure codes: 400, 409, 500.
36+
console.log(`${json.status}: ${json.error}`);
37+
}
38+
}
39+
40+
export async function login(state: FormState, formData: FormData) {
41+
// Validate form fields
42+
const validatedFields = LoginFormSchema.safeParse({
43+
email: formData.get("email"),
44+
password: formData.get("password"),
45+
});
46+
47+
// If any form fields are invalid, return early
48+
if (!validatedFields.success) {
49+
return {
50+
errors: validatedFields.error.flatten().fieldErrors,
51+
};
52+
}
53+
54+
const json = await getSessionLogin(validatedFields.data);
55+
if (!isError(json)) {
56+
await createSession(json.data.accessToken);
57+
redirect("/questions");
58+
} else {
59+
console.log(json.error);
60+
}
61+
}

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+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { QuestionFullBody, StatusBody } from "@/api/structs";
2+
3+
export async function deleteQuestion(id: number): Promise<StatusBody> {
4+
const res = await fetch(
5+
`${process.env.NEXT_PUBLIC_NGINX}/api/internal/questions`,
6+
{
7+
method: "DELETE",
8+
body: JSON.stringify({ qid: id }),
9+
}
10+
);
11+
if (res.ok) {
12+
return { status: res.status };
13+
}
14+
const json = await res.json();
15+
return json as StatusBody;
16+
}
17+
18+
export async function addQuestion(
19+
question: QuestionFullBody
20+
): Promise<StatusBody> {
21+
const res = await fetch(
22+
`${process.env.NEXT_PUBLIC_NGINX}/api/internal/questions`,
23+
{
24+
method: "POST",
25+
body: JSON.stringify(question).replace(
26+
/(\"difficulty\":)\"([1-3])\"/,
27+
`$1$2`
28+
),
29+
}
30+
);
31+
if (res.ok) {
32+
return { status: res.status };
33+
}
34+
const json = await res.json();
35+
return json as StatusBody;
36+
}

0 commit comments

Comments
 (0)