mirror of
https://github.com/OHTU-German-learning-website/OHTU-German-learning-website.git
synced 2026-10-12 19:13:51 +03:00
improve feedback feature, remove add-admin feature, fix types of list in gap exercise
This commit is contained in:
@@ -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,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
Reference in New Issue
Block a user