mirror of
https://github.com/OHTU-German-learning-website/OHTU-German-learning-website.git
synced 2026-10-12 19:13:51 +03:00
Vili/routing register (#66)
* route to backend (hello world) * add route for register * fix lint * fix routing * connect to db * add hashing with salt * add error handling * add unit tests for register * fix errors * axel/fix-import * fix register * update response to use error instead of message * fix tests * fix register * testfactory user toLowerCase() * fix migrate * fix register style * fix layout --------- Co-authored-by: DoebelnAtz <axel.ej.adlercreutz@gmail.com>
This commit is contained in:
co-authored by
DoebelnAtz
parent
4d34590cd6
commit
e8639db6c0
@@ -0,0 +1 @@
|
||||
ALTER TABLE users ADD COLUMN IF NOT EXISTS salt text NOT NULL;
|
||||
+2
-1
@@ -83,7 +83,8 @@ CREATE TABLE public.users (
|
||||
email text UNIQUE NOT NULL,
|
||||
password_hash text NOT NULL,
|
||||
created_at timestamp with time zone NOT NULL DEFAULT now(),
|
||||
updated_at timestamp with time zone NOT NULL DEFAULT now()
|
||||
updated_at timestamp with time zone NOT NULL DEFAULT now(),
|
||||
salt text NOT NULL
|
||||
);
|
||||
|
||||
CREATE TRIGGER updated_at BEFORE UPDATE ON public.users FOR EACH ROW EXECUTE FUNCTION updated_at();
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import { DB } from "../../../../backend/db";
|
||||
import { hashPassword } from "@/backend/auth/hash";
|
||||
import { emailRegex } from "@/shared/const";
|
||||
|
||||
export async function POST(request) {
|
||||
const json = await request.json();
|
||||
const email = json["email"].toLowerCase().trim();
|
||||
const password = json["password"];
|
||||
const { salt, hashedPassword } = await hashPassword(password);
|
||||
const existingUser = await DB.pool("SELECT * FROM users WHERE email = $1", [
|
||||
email,
|
||||
]);
|
||||
if (existingUser.rows.length > 0) {
|
||||
return Response.json(
|
||||
{ error: "Konto ist bereits vorhanden." },
|
||||
{ status: 409 }
|
||||
);
|
||||
}
|
||||
if (email === "" || password === "") {
|
||||
return Response.json(
|
||||
{ error: "E-Mail und Passwort sind erforderlich." },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
if (emailRegex.test(email) === false) {
|
||||
return Response.json(
|
||||
{ error: "Ungültige E-Mail-Adresse." },
|
||||
{ status: 422 }
|
||||
);
|
||||
}
|
||||
if (password.length < 8 || password.length > 64) {
|
||||
return Response.json(
|
||||
{ error: "Das Passwort muss mindestens 8 Zeichen lang sein." },
|
||||
{ status: 422 }
|
||||
);
|
||||
}
|
||||
|
||||
await DB.pool(
|
||||
"INSERT INTO users (email, password_hash, salt) VALUES ($1, $2, $3)",
|
||||
[email, hashedPassword, salt]
|
||||
);
|
||||
return Response.json({
|
||||
status: 200,
|
||||
message: "Account created.",
|
||||
});
|
||||
}
|
||||
@@ -1,48 +1,55 @@
|
||||
"use client";
|
||||
import { useState } from "react";
|
||||
import { useRequest } from "@/shared/hooks/useRequest";
|
||||
import { useRouter } from "next/navigation";
|
||||
import Link from "next/link";
|
||||
|
||||
export default function Register() {
|
||||
const [email, setEmail] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [submitted, setSubmitted] = useState(false);
|
||||
const [error, setError] = useState(null);
|
||||
const [error, setError] = useState("");
|
||||
const router = useRouter();
|
||||
|
||||
const makeRequest = useRequest();
|
||||
|
||||
const handleSubmit = async (event) => {
|
||||
event.preventDefault();
|
||||
if (email === "" || password === "") {
|
||||
setError(true);
|
||||
setSubmitted(false);
|
||||
} else {
|
||||
try {
|
||||
const data = await makeRequest("/auth/register", { email, password });
|
||||
setSubmitted(true);
|
||||
setError(false);
|
||||
|
||||
setTimeout(() => {
|
||||
window.location.href = "/auth/login";
|
||||
}, 1500);
|
||||
router.push("/auth/login");
|
||||
}, 2000);
|
||||
setError("");
|
||||
} catch (error) {
|
||||
setError(error.message);
|
||||
setSubmitted(false);
|
||||
}
|
||||
};
|
||||
|
||||
const successMessage = () => {
|
||||
return submitted ? (
|
||||
<div className="success-message">
|
||||
Benutzer {email} erfolgreich registriert
|
||||
return (
|
||||
<div className="success">
|
||||
<h1>Benutzer erfolgreich registriert</h1>
|
||||
</div>
|
||||
) : null;
|
||||
);
|
||||
};
|
||||
|
||||
const errorMessage = () => {
|
||||
return error ? (
|
||||
<div className="error-message">Bitte alle Felder ausfüllen</div>
|
||||
) : null;
|
||||
return (
|
||||
<div className="error">
|
||||
<h1>{error}</h1>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<h1 className="auth-title">Registrieren</h1>
|
||||
<div className="messages">
|
||||
{errorMessage()}
|
||||
{successMessage()}
|
||||
{!!error && errorMessage()}
|
||||
{submitted && successMessage()}
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit}>
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
const crypto = require("crypto");
|
||||
|
||||
export async function hashPassword(password) {
|
||||
const salt = crypto.randomBytes(16).toString("hex");
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
crypto.scrypt(password, salt, 64, (err, derivedKey) => {
|
||||
if (err) reject(err);
|
||||
const hashedPassword = derivedKey.toString("hex");
|
||||
resolve({ salt, hashedPassword });
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import { useTestDatabase } from "./testdb";
|
||||
import { POST } from "../../app/api/auth/register/route";
|
||||
import { DB } from "../db";
|
||||
import { TestFactory } from "./testfactory";
|
||||
import { useTestRequest } from "@/backend/test/mock-request";
|
||||
|
||||
describe("POST /register", () => {
|
||||
useTestDatabase();
|
||||
it("should return 409 if account already exists", async () => {
|
||||
const user = await TestFactory.user();
|
||||
const { mockPost } = useTestRequest();
|
||||
const request = mockPost("@/api/auth/register", {
|
||||
email: user.email,
|
||||
password: user.password_hash,
|
||||
});
|
||||
|
||||
const response = await POST(request);
|
||||
const result = await response.json();
|
||||
expect(response.status).toBe(409);
|
||||
expect(result.error).toBe("Konto ist bereits vorhanden.");
|
||||
});
|
||||
|
||||
it("should return 400 if email and password are empty", async () => {
|
||||
const email = "";
|
||||
const password = "";
|
||||
const { mockPost } = useTestRequest();
|
||||
|
||||
const request = mockPost("@/api/auth/register", {
|
||||
email,
|
||||
password,
|
||||
});
|
||||
|
||||
const response = await POST(request);
|
||||
const result = await response.json();
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(result.error).toBe("E-Mail und Passwort sind erforderlich.");
|
||||
});
|
||||
|
||||
it("should return 422 if email is invalid", async () => {
|
||||
const email = "invalidemail";
|
||||
const password = "goodpassword123";
|
||||
const { mockPost } = useTestRequest();
|
||||
|
||||
const request = mockPost("@/api/auth/register", {
|
||||
email,
|
||||
password,
|
||||
});
|
||||
|
||||
const response = await POST(request);
|
||||
const result = await response.json();
|
||||
|
||||
expect(response.status).toBe(422);
|
||||
expect(result.error).toBe("Ungültige E-Mail-Adresse.");
|
||||
});
|
||||
|
||||
it("should return 422 if password is too short", async () => {
|
||||
const email = "validemail@email.com";
|
||||
const password = "short";
|
||||
const { mockPost } = useTestRequest();
|
||||
|
||||
const request = mockPost("@/api/auth/register", {
|
||||
email,
|
||||
password,
|
||||
});
|
||||
|
||||
const response = await POST(request);
|
||||
const result = await response.json();
|
||||
|
||||
expect(response.status).toBe(422);
|
||||
expect(result.error).toBe(
|
||||
"Das Passwort muss mindestens 8 Zeichen lang sein."
|
||||
);
|
||||
});
|
||||
|
||||
it("should return 422 if password is too long", async () => {
|
||||
const email = "anothervalidemail@email.com";
|
||||
const password =
|
||||
"thispasswordiswaytoolongandshouldnotbeacceptedbecauseitexceedsthemaximumlengthallowed";
|
||||
const { mockPost } = useTestRequest();
|
||||
|
||||
const request = mockPost("@/api/auth/register", {
|
||||
email,
|
||||
password,
|
||||
});
|
||||
|
||||
const response = await POST(request);
|
||||
const result = await response.json();
|
||||
|
||||
expect(response.status).toBe(422);
|
||||
expect(result.error).toBe(
|
||||
"Das Passwort muss mindestens 8 Zeichen lang sein."
|
||||
);
|
||||
});
|
||||
|
||||
it("should return 200 if account is created", async () => {
|
||||
const email = "validanduniqueemail@email.com";
|
||||
const password = "goodpassword123";
|
||||
const { mockPost } = useTestRequest();
|
||||
|
||||
const request = mockPost("@/api/auth/register", {
|
||||
email,
|
||||
password,
|
||||
});
|
||||
|
||||
const response = await POST(request);
|
||||
const result = await response.json();
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(result.message).toBe("Account created.");
|
||||
});
|
||||
});
|
||||
@@ -34,10 +34,11 @@ function modelFactory(tableName, base) {
|
||||
faker.seed(123);
|
||||
|
||||
const user = modelFactory("users", () => ({
|
||||
email: faker.internet.email(),
|
||||
email: faker.internet.email().toLowerCase().trim(),
|
||||
password_hash: createHash("sha256")
|
||||
.update(faker.internet.password())
|
||||
.digest("hex"),
|
||||
salt: faker.string.alphanumeric(16),
|
||||
}));
|
||||
|
||||
export const TestFactory = {
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
export const emailRegex = new RegExp(
|
||||
`^([-!#-'*+/-9=?A-Z^-~]+(\.[-!#-'*+/-9=?A-Z^-~]+)*|"([]!#-[^-~ \t]|(\\[\t -~]))+")@([-!#-'*+/-9=?A-Z^-~]+(\.[-!#-'*+/-9=?A-Z^-~]+)*|\[[\t -Z^-~]*])$`,
|
||||
"i"
|
||||
);
|
||||
@@ -27,7 +27,7 @@ export const useRequest = () => {
|
||||
|
||||
return response;
|
||||
} catch (error) {
|
||||
if (error.response && error.response.data && error.response.data.error) {
|
||||
if (error.response && error.response.data?.error) {
|
||||
throw new Error(error.response.data.error);
|
||||
} else {
|
||||
throw new Error("An unexpected error occurred.");
|
||||
|
||||
Reference in New Issue
Block a user