improve feedback feature, remove add-admin feature, fix types of list in gap exercise

This commit is contained in:
Erza Bytyci
2026-05-20 11:48:18 +00:00
parent 158dd62716
commit 3e022c2567
8 changed files with 37 additions and 194 deletions
-38
View File
@@ -1,38 +0,0 @@
"use client";
import { Button } from "@/components/ui/button";
import { useRequest } from "@/shared/hooks/useRequest";
import { useState } from "react";
export default function AddAdmin() {
const [email, setEmail] = useState("");
const [error, setError] = useState(null);
const [success, setSuccess] = useState(null);
const request = useRequest();
const handleSubmit = async function (e) {
e.preventDefault();
try {
const res = await request("admin/add-admin", { email });
setSuccess(res.data.message || "user added as admin successfully");
setError(null); // nollaa virhe jos onnistuu
} catch (err) {
setError(err.message || "Tuntematon virhe");
setSuccess(null);
}
};
return (
<form onSubmit={handleSubmit}>
<input
type="text"
placeholder="email@example.com"
onChange={(e) => setEmail(e.target.value)}
/>
<br />
<Button type="submit">Submit</Button>
{error && <p style={{ color: "red", marginTop: "8px" }}>{error}</p>}
{success && <p style={{ color: "green", marginTop: "8px" }}>{success}</p>}
</form>
);
}
@@ -1,87 +0,0 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { POST } from "./route";
import { useTestRequest } from "@/backend/test/mock-request";
import { useTestDatabase } from "@/backend/test/testdb";
import { TestFactory } from "@/backend/test/testfactory";
import { checkSession } from "@/backend/auth/session";
import { DB } from "@/backend/db";
vi.mock("@/backend/auth/session", () => ({
checkSession: vi.fn(),
}));
describe("GET /api/add-admin", () => {
useTestDatabase();
let adminUser;
let normalUser;
beforeEach(async () => {
adminUser = await TestFactory.user({ is_admin: true });
normalUser = await TestFactory.user({ is_admin: false });
vi.clearAllMocks();
});
const postRequest = async (email, authUser) => {
const { mockPost } = useTestRequest();
const request = mockPost("/api/add-admin", { email }, authUser);
checkSession.mockImplementation(() =>
Promise.resolve(
authUser
? {
id: authUser.id,
email: authUser.email,
is_admin: authUser.is_admin,
}
: null
)
);
return POST(request);
};
it("should allow an admin to promote a user to admin", async () => {
const response = await postRequest(normalUser.email, adminUser);
const result = await response.json();
expect(response.status).toBe(200);
expect(result.success).toBe(true);
expect(result.message).toBe(
"Benutzer erfolgreich als administrator hinzugefügt"
);
const dbUser = await DB.pool(`SELECT is_admin FROM users WHERE id = $1`, [
normalUser.id,
]);
expect(dbUser.rows[0].is_admin).toBe(true);
});
it("should return 404 if user with email does not exist", async () => {
const response = await postRequest("notfound@example.com", adminUser);
const result = await response.json();
expect(response.status).toBe(404);
expect(result.error).toBe(
"Ein Benutzer mit dieser E-Mail-Adresse existiert nicht."
);
});
it("should require authentication", async () => {
const response = await postRequest(normalUser.email, null);
expect(response.status).toBe(401);
expect(checkSession).toHaveBeenCalled();
});
it("should throw on database errors", async () => {
const originalPool = DB.pool;
DB.pool = vi.fn().mockRejectedValueOnce(new Error("DB failure"));
await expect(postRequest(normalUser.email, adminUser)).rejects.toThrow(
"DB failure"
);
DB.pool = originalPool;
});
});
@@ -1,55 +0,0 @@
import { withAuth } from "@/backend/middleware/withAuth";
import { DB } from "@/backend/db";
import { NextResponse } from "next/server";
export const POST = withAuth(
async (req) => {
const body = await req.json();
const { email } = body;
// Check first if a user can be found with the email adress given:
const userResult = await DB.pool(
`
SELECT id FROM users
WHERE email = $1
`,
[email]
);
// If not, return an error message
if (userResult.rows.length === 0) {
return NextResponse.json(
{ error: "Ein Benutzer mit dieser E-Mail-Adresse existiert nicht." },
{ status: 404 }
);
}
const userId = userResult.rows[0].id;
// Below the user is updated to an admin:
await DB.transaction(async (tx) => {
await tx.query(
`
UPDATE users SET is_admin = true
WHERE id = $1
`,
[userId]
);
});
return NextResponse.json(
{
success: true,
message: "Benutzer erfolgreich als administrator hinzugefügt",
},
{ status: 200 }
);
},
{
requireAdmin: true,
requireAuth: true,
}
);
+9 -2
View File
@@ -33,9 +33,10 @@
} }
.auth-title { .auth-title {
text-align: left; text-align: center;
margin-bottom: var(--u-lg); margin-bottom: var(--u-lg);
color: var(--fg1); color: var(--fg1);
font-size: 1.9em;
} }
.talkback-message-input { .talkback-message-input {
@@ -44,6 +45,13 @@
max-width: 100%; max-width: 100%;
} }
.talkback-description {
color: var(--fg1);
margin-top: 0;
margin-bottom: var(--u-lg);
font-style: italic;
}
.form-group { .form-group {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -56,7 +64,6 @@
.form-label { .form-label {
display: block; display: block;
/* margin-bottom: var(--u-md); */
} }
.form-input { .form-input {
+11 -4
View File
@@ -15,7 +15,7 @@ export default function ContactPage() {
const [isSending, setIsSending] = useState(false); const [isSending, setIsSending] = useState(false);
// Show nothing while session is being resolved, or if not logged in. // Show nothing while session is being resolved, or if not logged in.
// Middleware already redirects unauthenticated requests to /auth/login. // Middleware already redirects unauthenticated requests to /auth/register.
if (!isAuthResolved || !isLoggedIn) return null; if (!isAuthResolved || !isLoggedIn) return null;
const handleSubmit = async (event) => { const handleSubmit = async (event) => {
@@ -61,18 +61,25 @@ export default function ContactPage() {
return ( return (
<section className="talkback-container"> <section className="talkback-container">
<div className="talkback-box"> <div className="talkback-box">
<h1 className="auth-title">Kontaktieren Sie unser Team</h1> <h2 className="auth-title talkback-title">
Kontaktieren Sie unser Team
</h2>
<p className="talkback-description">
Bitte vergessen Sie Ihre <strong>eigene E-Mail-Adresse</strong> nicht,
damit wir Ihnen antworten können.
</p>
{error && <p className="error">{error}</p>} {error && <p className="error">{error}</p>}
<form className="talkback-form" onSubmit={handleSubmit}> <form className="talkback-form" onSubmit={handleSubmit}>
<div className="form-group"> <div className="form-group">
<label className="form-label" htmlFor="contact-message"> {/* <label className="form-label" htmlFor="contact-message">
Nachricht Nachricht
</label> </label> */}
<textarea <textarea
className="form-input talkback-message-input" className="form-input talkback-message-input"
id="contact-message" id="contact-message"
placeholder="Nachricht eingeben..."
value={message} value={message}
onChange={(e) => setMessage(e.target.value)} onChange={(e) => setMessage(e.target.value)}
required required
@@ -110,6 +110,10 @@
list-style-type: decimal; list-style-type: decimal;
} }
.rendered-html li>.ql-ui {
display: none;
}
/* Quill editor in dark/light theme */ /* Quill editor in dark/light theme */
.ql-snow .ql-editor { .ql-snow .ql-editor {
color: var(--fg1); color: var(--fg1);
@@ -201,11 +201,6 @@ const adminSidebarLinks = [
link: "/admin/users", link: "/admin/users",
id: "users", id: "users",
}, },
{
title: "Admin hinzufügen",
link: "/admin/add-admin",
id: "add-admin",
},
{ {
title: "Neue Seite", title: "Neue Seite",
link: "/admin/new-page", link: "/admin/new-page",
+13 -3
View File
@@ -10,6 +10,9 @@ import { AUTH_COOKIE_NAME } from "@/shared/const";
*/ */
const authRequired = ["/grammar/exercises", "/edit_info", "/contact"]; const authRequired = ["/grammar/exercises", "/edit_info", "/contact"];
// Protected paths that should send logged-out users to registration first.
const registerFirstPaths = ["/contact"];
// Restricted paths logged-in users cannot access // Restricted paths logged-in users cannot access
/** /**
* unauthRequired - array of path prefixes that should not be accessible * unauthRequired - array of path prefixes that should not be accessible
@@ -45,9 +48,16 @@ export async function middleware(request) {
!hasCookie && !hasCookie &&
authRequired.some((path) => pathnameWithoutBasePath.startsWith(path)) authRequired.some((path) => pathnameWithoutBasePath.startsWith(path))
) { ) {
const loginUrl = new URL(`${basePath}/auth/login`, request.url); const shouldRedirectToRegister = registerFirstPaths.some((path) =>
loginUrl.searchParams.set("redirect", pathnameWithoutBasePath); // Add the original path as a query parameter pathnameWithoutBasePath.startsWith(path)
return NextResponse.redirect(loginUrl); );
const targetPath = shouldRedirectToRegister
? "/auth/register"
: "/auth/login";
const authUrl = new URL(`${basePath}${targetPath}`, request.url);
authUrl.searchParams.set("redirect", pathnameWithoutBasePath); // Add the original path as a query parameter
return NextResponse.redirect(authUrl);
} }
// Otherwise, allow the request to proceed and prevent caching of the response // Otherwise, allow the request to proceed and prevent caching of the response
const response = NextResponse.next(); const response = NextResponse.next();