WIP: Major Update 0.2 #3

Draft
Hannah wants to merge 19 commits from Devbranch into main
36 changed files with 2093 additions and 903 deletions

View file

@ -1,2 +1,2 @@
FRONTEND_URL=
NEXT_PUBLIC_FRONTEND_URL=
BACKEND_URL=

View file

@ -1,7 +0,0 @@
import UnderConstruction from "@/components/static_pages/UnderConstruction";
export default function Login() {
return (
<UnderConstruction />
)
}

View file

@ -1,7 +1,84 @@
import UnderConstruction from "@/components/static_pages/UnderConstruction";
"use client";
import { useEffect, useState } from "react";
import RoundedContainer from "@/components/ui/RoundedContainer";
import { getServices } from "@/lib/api/helpers/services";
import { Project, Service, User } from "@/lib/api/types";
import { getUsers } from "@/lib/api/helpers/users";
export default function Overview() {
return(
<UnderConstruction />
)
const [projectStatus, setProjectStatus] = useState<boolean>(true);
const [projects, setProjects] = useState<Project[]>([]);
/*useEffect(() => {
getProjects()
.then((projects) => {
if (projects.length > 0) {
setProjects(projects);
}
})
.catch(() => {
setProjectStatus(false);
});
}, []); IMPLEMENT GET PROJECTS */
const [serviceStatus, setServiceStatus] = useState<boolean>(true);
const [services, setServices] = useState<Service[]>([]);
useEffect(() => {
getServices()
.then((services) => {
if (services.length > 0) {
setServices(services);
}
})
.catch(() => {
setServiceStatus(false);
});
}, []);
const [userStatus, setUserStatus] = useState<boolean>(true);
const [users, setUsers] = useState<User[]>([]);
useEffect(() => {
getUsers()
.then((users) => {
if (users.length > 0) {
setUsers(users);
}
})
.catch(() => {
setUserStatus(false);
});
}, []);
return (
<div>
<RoundedContainer>
<p>Users</p>
{users.length < 1 ? (
users.map((user) => <p key={user.id}>{user.username}</p>)
) : userStatus.valueOf() ? (
<p>Loading users...</p>
) : (
<p>Failed to load users :C</p>
)}
</RoundedContainer>
<RoundedContainer>
<p>Services</p>
{services.length < 1 ? (
services.map((service) => <p key={service.id}>{service.title}</p>)
) : serviceStatus ? (
<p>Loading services...</p>
) : (
<p>Failed to load services :C</p>
)}
</RoundedContainer>
<RoundedContainer>
<p>Projects</p>
{projects.length < 1 ? (
projects.map((project) => <p key={project.id}>{project.title}</p>)
) : projectStatus ? (
<p>Loading projects...</p>
) : (
<p>Failed to load projects :C</p>
)}
</RoundedContainer>
</div>
);
}

View file

@ -1,11 +1,13 @@
import React from "react";
export default function AdminLayout({children}: {children: React.ReactNode}) {
import ProtectedRoute from "@/components/wrappers/ProtectedRoute";
export default async function AdminLayout({
children,
}: {
children: React.ReactNode;
}) {
// TODO: Put user state management here
//redirect("wherever state says") <= stateRouteManager()
return (
<div>
{children}
</div>
)
}
return <ProtectedRoute>{children}</ProtectedRoute>;
}

View file

@ -1,6 +1,5 @@
import { redirect } from "next/navigation";
import UnderConstruction from "@/components/static_pages/UnderConstruction";
export default function Admin() {
// TODO: Replace with Layout State Manager
redirect("/Admin/Login");
return <UnderConstruction />;
}

View file

@ -0,0 +1,30 @@
"use client";
import { useState } from "react";
import { login, signup } from "@/lib/server_functions/userAuth";
import Form from "@/components/ui/Form";
export default function Login() {
const [createAccount, setCreateAccount] = useState(false);
return (
<div className="flex h-full w-full justify-center py-10">
<Form
action={createAccount ? login : signup}
submitText={createAccount ? "Login" : "Signup"}
>
<input type="text" name="username" placeholder="Username" required />
<input
type="password"
name="password"
placeholder="Password"
required
/>
<button onClick={() => setCreateAccount(!createAccount)}>
{createAccount
? "Don't have an account?"
: "Already have an account?"}
</button>
</Form>
</div>
);
}

View file

@ -1,10 +1,10 @@
"use client";
import React, {useState} from "react";
import React, { useState } from "react";
import Image from "next/image";
import RoundedContainer from "@/components/ui/RoundedContainer";
import Hyperlink from "@/components/ui/Hyperlink";
import Hyperlink from "@/components/ui/NavLink";
type Project = {
id: number;
@ -12,7 +12,7 @@ type Project = {
parentProject?: number;
title: string;
content: React.ReactNode;
}
};
const projects: Project[] = [
{
@ -22,13 +22,20 @@ const projects: Project[] = [
content: (
<div className="prose space-y-2">
<p className="text-xl">Hello!</p>
<p>{"This is the project view page. Here you can get a quick idea of what sort of things I've worked on. Unfortunately it relies on me making these project sides manually, and thus only notable projects will end up on here. If you're looking for a brief overview of, for example what languages I've worked in, I suggest you take a peek at my git page."}</p>
<p>
{
"This is the project view page. Here you can get a quick idea of what sort of things I've worked on. Unfortunately it relies on me making these project sides manually, and thus only notable projects will end up on here. If you're looking for a brief overview of, for example what languages I've worked in, I suggest you take a peek at my git page."
}
</p>
<div className="flex flex-row justify-around">
<Hyperlink name={"My self-hosted Git"} link={"dagemark.se/git/Hannah"} />
<Hyperlink
name={"My self-hosted Git"}
link={"dagemark.se/git/Hannah"}
/>
<Hyperlink name={"My Github"} link={"github.com/Hannah-Dagemark"} />
</div>
</div>
)
),
},
{
id: 2,
@ -36,7 +43,11 @@ const projects: Project[] = [
title: "Dagemark Homelab",
content: (
<div className="prose space-y-2">
<p>{"The main project which I've been working on since December. For those unfamiliar with the concept, a HomeLab is a custom server one builds and runs in their own home to give them more autonomy and control over their digital services. In my case, it's also to let me get some very useful experience in networking, docker management, service handling, server interfacing, and more. Due to this project's size, it's been split up into sub-projects."}</p>
<p>
{
"The main project which I've been working on since December. For those unfamiliar with the concept, a HomeLab is a custom server one builds and runs in their own home to give them more autonomy and control over their digital services. In my case, it's also to let me get some very useful experience in networking, docker management, service handling, server interfacing, and more. Due to this project's size, it's been split up into sub-projects."
}
</p>
<Image
src="/ServerUML.png"
alt="(Server UML Image Here)"
@ -44,9 +55,13 @@ const projects: Project[] = [
height={500}
priority
/>
<p>{"Here's an example of how the networking looks like inside the server right now, with most services (including this website!) being ran with docker using a docker compose setup. One of those services is Nginx Proxy Manager which does reverse proxying and routing for the server."}</p>
<p>
{
"Here's an example of how the networking looks like inside the server right now, with most services (including this website!) being ran with docker using a docker compose setup. One of those services is Nginx Proxy Manager which does reverse proxying and routing for the server."
}
</p>
</div>
)
),
},
{
id: 3,
@ -55,23 +70,36 @@ const projects: Project[] = [
title: "Main Website",
content: (
<div className="prose space-y-3">
<a href="https://dagemark.se/git/Hannah/Dagemark-Webapp" >Click here to go to Git Page</a>
<p>{"The main website that you're currently on. The website is written in Typescript, using the NextJS React toolkit. It also uses TailwindCSS for CSS styling, as well as eslint to keep code clean and uniform."}</p>
<p>{"Currently the website is only a frontend page, although a backend is in the works. The idea is to have the website be very editable online, including these project pages as an example. For that a robust user handling system needs to be created however, and a good backend api, which is the planned next step."}</p>
<p>{"This project has been a very nice way for me to jog my memory from my technical fourth year, where I originally learned this development stack. I also find web development to be a good way of training the other aspects of development, as planning routes, page layouts, apis, reusable components and thematics is very essential."}</p>
<a href="https://dagemark.se/git/Hannah/Dagemark-Webapp">
Click here to go to Git Page
</a>
<p>
{
"The main website that you're currently on. The website is written in Typescript, using the NextJS React toolkit. It also uses TailwindCSS for CSS styling, as well as eslint to keep code clean and uniform."
}
</p>
<p>
{
"Currently the website is only a frontend page, although a backend is in the works. The idea is to have the website be very editable online, including these project pages as an example. For that a robust user handling system needs to be created however, and a good backend api, which is the planned next step."
}
</p>
<p>
{
"This project has been a very nice way for me to jog my memory from my technical fourth year, where I originally learned this development stack. I also find web development to be a good way of training the other aspects of development, as planning routes, page layouts, apis, reusable components and thematics is very essential."
}
</p>
</div>
)
),
},
{
id: 4,
isChildProject: false,
title: "Count Duch King",
content: "[To be implemented]"
content: "[To be implemented]",
},
]
];
export default function Projects() {
// TODO: make the content server stored, of course!
const [activeProject, setActiveProject] = useState(projects[0]);
@ -82,7 +110,6 @@ export default function Projects() {
<div className="flex w-230 h-250 justify-center min-h-screen bg-gray-600 rounded-2xl py-10">
{/* Notebook Wrapper */}
<div className="relative flex w-full h-full pr-5">
{/* Chapter Tabs */}
<div className="flex flex-col justify-start pt-10 pr-5">
{projects.map((project) => (
@ -107,14 +134,11 @@ export default function Projects() {
<h2 className="text-slate-800 text-2xl font-bold mb-4">
{activeProject.title}
</h2>
<p className="text-slate-700">
{activeProject.content}
</p>
<p className="text-slate-700">{activeProject.content}</p>
</div>
</div>
</div>
</RoundedContainer>
</div>
)
);
}

View file

@ -1,20 +1,28 @@
import Image from "next/image";
import Referencelink from "@/components/ui/Referencelink";
import RoundedContainer from "@/components/ui/RoundedContainer";
import NavLink from "@/components/ui/NavLink";
export default function Hannah() {
return (
<div className="flex h-full w-full justify-center py-10">
<RoundedContainer slim={false}>
<div className="flex w-full flex-row justify-between">
<Referencelink name={"Projects"} link={"/Hannah/Projects"} />
<Referencelink name={"CV"} link={"/Hannah/CV"} />
<NavLink name={"Projects"} link={"/Hannah/Projects"} />
<NavLink name={"CV"} link={"/Hannah/CV"} />
</div>
<div className="flex w-full flex-col space-y-2 py-10">
<h1 className="text-2xl font-bold">Hi!</h1>
<p>{"I'm Hannah, and I made this website! It's currently very much in it's early stages, but I am actively developing it and soon it'll hopefully contain some good examples of what I do. Besides being a place to display my projects, it also acts as a hub for the different services I've incorporated into our home web ecosystem, or HomeLab."}</p>
<p>{"If you came here from a job application, chances are you'll want to take a look at the Projects tab. As said It's still fairly new, but it'll give you a good example of what I put my time into! Just press the \"Projects\" button if you're interested"}</p>
<p>
{
"I'm Hannah, and I made this website! It's currently very much in it's early stages, but I am actively developing it and soon it'll hopefully contain some good examples of what I do. Besides being a place to display my projects, it also acts as a hub for the different services I've incorporated into our home web ecosystem, or HomeLab."
}
</p>
<p>
{
"If you came here from a job application, chances are you'll want to take a look at the Projects tab. As said It's still fairly new, but it'll give you a good example of what I put my time into! Just press the \"Projects\" button if you're interested"
}
</p>
<p>{"In the meantime, here's a picture of my cat :D"}</p>
<Image
src="/Missy.jpg"
@ -26,5 +34,5 @@ export default function Hannah() {
</div>
</RoundedContainer>
</div>
)
);
}

View file

@ -0,0 +1,61 @@
"use client";
import Button from "@/components/ui/Button";
import Dropdown from "@/components/ui/Dropdown";
import Form from "@/components/ui/Form";
import NavLink from "@/components/ui/NavLink";
import RoundedContainer from "@/components/ui/RoundedContainer";
export default function Components() {
return (
<div className="flex flex-col items-center gap-4 w-full h-full px-10">
<h1>Dropdown</h1>
<Dropdown title="Dropdown" mode="DARK">
<NavLink name="Home" link="/" />
<NavLink name="Admin" link="/Admin" />
<NavLink name="Hannah" link="/Hannah" />
</Dropdown>
<h1>Button</h1>
<Button
title={"Button"}
mode="DARK"
onClick={() => {
console.log("Button!");
}}
/>
<h1>Form</h1>
<Form action={"Action here"} submitText="Example submit button ">
<input type="text" name="text" placeholder="example placeholder text" />
{/* Add custom input! */}
</Form>
<h1>NavLink</h1>
<NavLink name="Example Navlink" link="/Playground/Components/" />
<h2>External</h2>
<NavLink
name="Example External Navlink"
external={true}
link={`${process.env.NEXT_PUBLIC_FRONTEND_URL}/Playground/Components/`}
/>
<h1>Rounded Container</h1>
<RoundedContainer>
<div className=" min-h-5 min-w-5">
<p>Contents</p>
<p>More Contents</p>
</div>
</RoundedContainer>
<h2>Non-slim</h2>
<RoundedContainer slim={false}>
<div className=" min-h-5 min-w-5">
<p>Contents</p>
<p>More Contents</p>
</div>
</RoundedContainer>
</div>
);
}

View file

@ -0,0 +1,25 @@
import logger from "@/lib/logging/logger";
const BACKEND_URL = process.env.BACKEND_URL;
export async function POST(request: Request) {
const indata = await request.json();
const body = new URLSearchParams({
username: indata.username,
password: indata.password,
});
logger.log("debug", `recieved users/login call with data: ${body}`); // for debugging login form calls
const response = await fetch(`${BACKEND_URL}/users/auth`, {
method: "POST",
body: body,
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
});
const data = await response.json();
return Response.json(data, {
status: response.status,
});
}

View file

@ -4,7 +4,9 @@ export async function GET() {
const response = await fetch(`${BACKEND_URL}/users`);
if (!response.ok) {
return new Response("Request failed in backend", { status: response.status, });
return new Response("Request failed in backend", {
status: response.status,
});
}
const data = await response.json();
@ -13,15 +15,11 @@ export async function GET() {
}
export async function POST(request: Request) {
const body = await request.json();
const response = await fetch(`${BACKEND_URL}/users`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(body),
headers: request.headers,
body: request.body,
duplex: "half",
});
const data = await response.json();

View file

@ -0,0 +1,19 @@
const BACKEND_URL = process.env.BACKEND_URL;
export async function GET(
_request: Request,
{ params }: { params: Promise<{ id: string }> },
) {
const { id } = await params;
const response = await fetch(`${BACKEND_URL}/users/user/id/${id}`);
if (!response.ok) {
return new Response("Request failed in backend", {
status: response.status,
});
}
const data = await response.json();
return Response.json(data);
}

View file

@ -0,0 +1,21 @@
const BACKEND_URL = process.env.BACKEND_URL;
export async function GET(request: Request) {
const authorization = request.headers.get("authorization");
const response = await fetch(`${BACKEND_URL}/users/user`, {
headers: {
Authorization: authorization ?? "",
},
});
if (!response.ok) {
return new Response("Request failed in backend", {
status: response.status,
});
}
const data = await response.json();
return Response.json(data);
}

View file

@ -0,0 +1,19 @@
const BACKEND_URL = process.env.BACKEND_URL;
export async function GET(
_request: Request,
{ params }: { params: Promise<{ username: string }> },
) {
const { username } = await params;
const response = await fetch(`${BACKEND_URL}/users/user/name/${username}`);
if (!response.ok) {
return new Response("Request failed in backend", {
status: response.status,
});
}
const data = await response.json();
return Response.json(data);
}

View file

@ -16,9 +16,11 @@ export default function RootLayout({
}>) {
return (
<html lang="en">
<body className={`antialiased`}> {/* className=${geistSans.variable} ${geistMono.variable} */}
<Header/>
<div className="flex min-h-screen bg-zinc-200 font-sans dark:bg-slate-800">
<body className={`antialiased min-h-screen flex flex-col`}>
{" "}
{/* className=${geistSans.variable} ${geistMono.variable} */}
<Header />
<div className="flex grow bg-zinc-200 font-sans dark:bg-slate-800">
{children}
</div>
</body>

View file

@ -1,11 +1,11 @@
import Image from "next/image";
import Image from "next/image";
import RoundedContainer from "@/components/ui/RoundedContainer";
import RoundedContainer from "@/components/ui/RoundedContainer";
export default function Home() {
return (
<main className="flex items-center justify-center w-full bg-zinc-200 font-sans dark:bg-slate-800">
<RoundedContainer>
<RoundedContainer slim={false}>
<Image
className="dark:invert"
src="/next.svg"
@ -19,7 +19,8 @@ export default function Home() {
Welcome to the Dagemark Server
</h1>
<p className="max-w-md text-lg leading-8 text-zinc-600 dark:text-zinc-400 italic">
Design In Progress, current layout and theme may change, have a good day :D
Design In Progress, current layout and theme may change, have a good
day :D
</p>
</div>
</RoundedContainer>

View file

@ -2,7 +2,7 @@
import { useEffect, useState } from "react";
import Hyperlink from "@/components/ui/Hyperlink";
import Hyperlink from "@/components/ui/NavLink";
import { getServices } from "@/lib/api/helpers/services";
import { Service } from "@/lib/api/types";

25
components/ui/Button.tsx Normal file
View file

@ -0,0 +1,25 @@
import { ComponentProps } from "react";
import React from "react";
type ButtonProps = ComponentProps<"button"> & {
title: string;
mode?: "DARK" | "LIGHT";
icon?: React.ReactElement;
};
export default function Button({
title,
mode = "LIGHT",
icon,
...props
}: ButtonProps) {
return (
<button
{...props}
className={`${mode == "LIGHT" ? "bg-slate-400 dark:bg-slate-800 border-slate-800 hover:bg-slate-500" : "bg-gray-400 dark:bg-slate-900 dark:border-slate-900 border-slate-400 hover:bg-slate-600"} flex flex-row items-center border-4 border-double cursor-pointer font-medium py-2 px-4 rounded-lg shadow-md transition duration-200 ease-in-out`}
>
{title}
{icon}
</button>
);
}

View file

@ -0,0 +1,47 @@
import { useState } from "react";
import React from "react";
import { ArrowRightIcon } from "@heroicons/react/24/outline";
import Button from "./Button";
import RoundedContainer from "./RoundedContainer";
export default function Dropdown({
title,
mode,
children,
}: {
title?: string;
mode?: "DARK" | "LIGHT";
children: React.ReactNode;
}) {
const [dropped, setDropped] = useState<boolean>(false);
const [degrees, setDegrees] = useState<number>(0);
return (
<div>
<Button
title={title ? title : ""}
mode={mode}
onClick={() => {
setDropped(!dropped);
if (degrees < 90) {
setDegrees(90);
} else {
setDegrees(0);
}
}}
icon={
<ArrowRightIcon
className={`size-4 transition-transform rotate-${degrees}`}
/>
}
/>
<RoundedContainer
slim={true}
className={`${!dropped && "invisible"} absolute z-10 flex flex-col gap-2 transition-discrete duration-150 ease-out`}
>
{children}
</RoundedContainer>
</div>
);
}

26
components/ui/Form.tsx Normal file
View file

@ -0,0 +1,26 @@
import React from "react";
import RoundedContainer from "./RoundedContainer";
import Button from "./Button";
export default function Form({
action,
children,
submitText,
}: {
// eslint-disable-next-line no-unused-vars
action?: string | ((formData: FormData) => void | Promise<void>);
children: React.ReactNode;
submitText: string;
}) {
return (
<div>
<form action={action}>
<RoundedContainer slim={false}>
{children}
<Button title={submitText} type="submit" />
</RoundedContainer>
</form>
</div>
);
}

View file

@ -1,9 +0,0 @@
export default function Hyperlink({name, link}: {name: string; link: string}) {
return (
<div className="bg-slate-400 dark:bg-slate-800 hover:bg-slate-600 font-medium py-2 px-4 rounded-lg shadow-md transition duration-200 ease-in-out">
<a href={`https://${link}`} className="block">
<h1 className="font-semibold text-slate-800dark:text-slate-400 text-lg">{name}</h1>
</a>
</div>
)
}

21
components/ui/NavLink.tsx Normal file
View file

@ -0,0 +1,21 @@
import Link from "next/link";
export default function NavLink({
name,
link,
external = false,
}: {
name: string;
link: string;
external?: boolean;
}) {
return (
<div className="bg-slate-400 dark:bg-slate-800 hover:bg-slate-600 font-medium py-2 px-4 rounded-lg shadow-md transition duration-200 ease-in-out">
<Link href={`${external ? "https:" : ""}${link}`} className="block">
<h1 className="font-semibold text-slate-800dark:text-slate-400 text-lg">
{name}
</h1>
</Link>
</div>
);
}

View file

@ -1,9 +0,0 @@
export default function Referencelink({name, link}: {name: string; link: string}) {
return (
<div className="bg-slate-400 dark:bg-slate-800 hover:bg-slate-600 font-medium py-2 px-4 rounded-lg shadow-md transition duration-200 ease-in-out">
<a href={`${link}`} className="block">
<h1 className="font-semibold text-slate-800dark:text-slate-400 text-lg">{name}</h1>
</a>
</div>
)
}

View file

@ -1,9 +1,21 @@
import React from "react";
export default function RoundedContainer({children, slim = true}: {children: React.ReactNode, slim?: boolean}) {
export default function RoundedContainer({
children,
slim = true,
direction = "column",
className,
}: {
children: React.ReactNode;
slim?: boolean;
direction?: "column" | "row";
className?: string;
}) {
return (
<div className={`flex ${slim ? "max-w-6xl" : "max-w-3xl"} rounded-4xl flex-col items-center py-8 px-8 bg-gray-400 dark:bg-slate-900 sm:items-start`}>
<div
className={`flex ${slim ? "max-w-6xl py-3 px-3" : "max-w-3xl py-6 px-6"} rounded-2xl ${direction == "column" ? "flex-col" : "flex-row"} items-center bg-gray-400 dark:bg-slate-900 sm:items-start ${className}`}
>
{children}
</div>
)
);
}

View file

@ -0,0 +1,17 @@
import React from "react";
import { redirect } from "next/navigation";
import { verifySession } from "@/lib/server_functions/dataAccessLayer";
export default async function ProtectedRoute({
children,
}: {
children: React.ReactNode;
}) {
const session = await verifySession();
if (!session.isAuthed) {
redirect("/Authentication/Login");
}
return <div>{children}</div>;
}

View file

@ -17,15 +17,16 @@ const eslintConfig = defineConfig([
"no-unused-vars": "warn",
"no-empty-function": "warn",
"no-undef": "error",
"no-console": "warn",
"react/self-closing-comp": "warn",
"import/order": [
"warn",
{
"groups": ["builtin", "external", "internal"],
"newlines-between": "always"
}
]
}
groups: ["builtin", "external", "internal"],
"newlines-between": "always",
},
],
},
},
// Override default ignores of eslint-config-next.
globalIgnores([

View file

@ -1,6 +1,6 @@
import { RequestInit } from "next/dist/server/web/spec-extension/request";
const FRONTEND_URL = process.env.FRONTEND_URL;
const FRONTEND_URL = process.env.NEXT_PUBLIC_FRONTEND_URL;
export async function cmFetch<T>(
bffUrl: string | URL,
@ -14,3 +14,17 @@ export async function cmFetch<T>(
return response.json();
}
export async function cmFetchRaw(
bffUrl: string | URL,
options?: RequestInit,
ignoreStatuses?: number[],
): Promise<Response> {
const response = await fetch(`${FRONTEND_URL}/${bffUrl}`, options);
if (!response.ok && !ignoreStatuses?.includes(response.status)) {
throw new Error("Request failed");
}
return response;
}

View file

@ -0,0 +1,18 @@
import { cmFetch } from "../cmFetch";
import { Project } from "../types";
export async function getProjects() {
return Promise<Project[]>; // TODO: IMPLEMENT
}
export async function getProject(/*id: number */) {
return Promise<Project>; // TODO: IMPLEMENT
}
export async function createProject(/*project: Project*/) {
return Promise<Project>; // TODO: IMPLEMENT
}
export async function deleteService(/*id: number*/) {
return Promise<Project>;
}

View file

@ -1,18 +1,50 @@
export async function getUsers() {
const response = await fetch("/api/users");
if (!response.ok) {
throw new Error(`Response status: ${response.status}`);
}
import { getSessionToken } from "@/lib/server_functions/userAuth";
return response.json();
import { cmFetch, cmFetchRaw } from "../cmFetch";
import { Token, User } from "../types";
export async function getUsers() {
return cmFetch<User[]>(`/api/users`);
}
export async function getCurrentUser() {
return cmFetchRaw(
`/api/users/user`,
{ headers: { Authorization: `Bearer ${await getSessionToken()}` } },
[401],
);
}
export async function getUser(id: number) {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) {
throw new Error(`Response status: ${response.status}`);
}
return response.json();
return cmFetch<User>(`/api/users/${id}`);
}
export async function loginUser(user: FormData) {
const body = {
username: user.get("username"),
password: user.get("password"),
};
// logger.log("debug", `Recieved login user request for form: ${body}`); // debug login form submission, ISSUE, THIS IS A CLIENT HELPER!
return cmFetch<Token>(`/api/users/login`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(body),
});
}
export async function createUser(user: FormData) {
const body = {
username: user.get("username"),
password: user.get("password"),
authority: 0,
};
return cmFetch<Token>(`/api/users`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(body),
});
}

View file

@ -6,6 +6,12 @@ export interface User {
authority: number;
}
export interface Token {
access_token: string;
token_type: string;
}
export interface Service {
id: number | null;

39
lib/logging/logger.ts Normal file
View file

@ -0,0 +1,39 @@
import winston from "winston";
const logger = winston.createLogger({
level: "info",
format: winston.format.json(),
defaultMeta: { service: "user-service" },
transports: [
//
// - Write all logs with importance level of `error` or higher to `error.log`
// (i.e., error, fatal, but not other levels)
//
new winston.transports.File({
filename: "/lib/logging/error.log",
level: "error",
}),
//
// - Write all logs with importance level of `info` or higher to `combined.log`
// (i.e., fatal, error, warn, and info, but not trace)
//
new winston.transports.File({ filename: "/lib/logging/combined.log" }),
],
});
//
// If we're not in production then log to the `console` with the format:
// `${info.level}: ${info.message} JSON.stringify({ ...rest }) `
//
if (process.env.NODE_ENV !== "production") {
logger.add(
new winston.transports.Console({
format: winston.format.simple(),
}),
);
logger.configure({
level: "debug",
});
}
export default logger;

View file

@ -0,0 +1,23 @@
import { cookies } from "next/headers";
import { cache } from "react";
import "server-only";
import { getCurrentUser } from "../api/helpers/users";
import { User } from "../api/types";
export const verifySession = cache(async () => {
const cookie = (await cookies()).get("access_token")?.value;
if (!cookie) {
return { isAuthed: false };
}
const response = await getCurrentUser();
if (response.status == 401) {
return { isAuthed: false };
}
const user = (await response.json()) as User;
return {
isAuthed: true,
surface_auth_level: user.authority,
username: user.username,
};
});

View file

@ -0,0 +1,44 @@
"use server";
import { cookies } from "next/headers";
import { redirect } from "next/navigation";
import { createUser, loginUser } from "../api/helpers/users";
import { Token } from "../api/types";
import logger from "../logging/logger";
export async function login(formData: FormData) {
"use server";
const token = await loginUser(formData);
// ERROR: No transport? logger.log("debug", `Fetched following token for login: ${token}`); // token check for backend debugging
await createSession(token);
redirect("/Admin/Overview");
}
export async function signup(formData: FormData) {
// Technically same code as login for now but will do different stuff with it later so separating it early on
"use server";
const token = await createUser(formData);
// ERROR: No transport? logger.log("debug", `Fetched following token for login: ${token}`); // token check for backend debugging
await createSession(token);
redirect("/Admin/Overview");
}
export async function createSession(token: Token) {
const access_token = token.access_token;
const cookieStore = await cookies();
cookieStore.set("access_token", access_token, {
httpOnly: true,
secure: process.env.NODE_ENV === "production",
sameSite: "lax",
path: "/",
});
}
export async function getSessionToken() {
const cookieStore = await cookies();
// ERROR: No transport? logger.log("debug", cookieStore.getAll()); // debug: log cookies when checking for session token, ideally logs session token too
const token = cookieStore.get("access_token")?.value;
return token ? token : null;
}

2136
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -10,9 +10,11 @@
"lint:fix": "eslint --fix"
},
"dependencies": {
"next": "16.1.1",
"@heroicons/react": "^2.2.0",
"next": "^16.3.4",
"react": "19.2.3",
"react-dom": "19.2.3"
"react-dom": "19.2.3",
"winston": "^3.19.0"
},
"devDependencies": {
"@tailwindcss/postcss": "^4",
@ -27,5 +29,9 @@
"tailwindcss": "^4",
"typescript": "^5",
"typescript-eslint": "^8.56.0"
},
"allowScripts": {
"unrs-resolver@1.12.2": true,
"sharp@0.34.5": true
}
}