Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f13c32671e | ||
|
|
9c568f40cd | ||
|
|
c1d8ff9049 | ||
|
|
b9d8dfe7b7 | ||
| 73c49252f6 | |||
| 4ad42312b1 | |||
|
|
789c5ab26a | ||
|
|
1293d40894 | ||
|
|
ca6fd65f96 | ||
|
|
6b5deae129 | ||
|
|
9293c92e58 | ||
|
|
e40ed22746 | ||
|
|
efa42a53b2 | ||
|
|
f055e77eea | ||
|
|
662b26ad47 | ||
|
|
fa206fe658 | ||
|
|
d3064fd764 | ||
|
|
f5466faa70 | ||
|
|
3be05a9752 |
36 changed files with 2093 additions and 903 deletions
|
|
@ -1,2 +1,2 @@
|
|||
FRONTEND_URL=
|
||||
NEXT_PUBLIC_FRONTEND_URL=
|
||||
BACKEND_URL=
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
import UnderConstruction from "@/components/static_pages/UnderConstruction";
|
||||
|
||||
export default function Login() {
|
||||
return (
|
||||
<UnderConstruction />
|
||||
)
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 />;
|
||||
}
|
||||
|
|
|
|||
30
app/Authentication/Login/page.tsx
Normal file
30
app/Authentication/Login/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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) => (
|
||||
|
|
@ -92,10 +119,10 @@ export default function Projects() {
|
|||
className={`${project.isChildProject ? "ml-2 mr-2 pl-3 border-l-4 border-t-2 border-slate-500 text-sm" : "mt-2 rounded-r-lg"}
|
||||
px-4 py-2 text-left transition
|
||||
${
|
||||
activeProject.id === project.id
|
||||
? "bg-slate-300 shadow-md"
|
||||
: "bg-slate-400 hover:bg-slate-500"
|
||||
}`}
|
||||
activeProject.id === project.id
|
||||
? "bg-slate-300 shadow-md"
|
||||
: "bg-slate-400 hover:bg-slate-500"
|
||||
}`}
|
||||
>
|
||||
{project.title}
|
||||
</button>
|
||||
|
|
@ -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>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
)
|
||||
);
|
||||
}
|
||||
61
app/Playground/Components/page.tsx
Normal file
61
app/Playground/Components/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
25
app/api/users/login/route.ts
Normal file
25
app/api/users/login/route.ts
Normal 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,
|
||||
});
|
||||
}
|
||||
|
|
@ -1,32 +1,30 @@
|
|||
const BACKEND_URL = process.env.BACKEND_URL;
|
||||
|
||||
export async function GET() {
|
||||
const response = await fetch(`${BACKEND_URL}/users`);
|
||||
const response = await fetch(`${BACKEND_URL}/users`);
|
||||
|
||||
if (!response.ok) {
|
||||
return new Response("Request failed in backend", { status: response.status, });
|
||||
}
|
||||
if (!response.ok) {
|
||||
return new Response("Request failed in backend", {
|
||||
status: response.status,
|
||||
});
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const data = await response.json();
|
||||
|
||||
return Response.json(data);
|
||||
return Response.json(data);
|
||||
}
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const response = await fetch(`${BACKEND_URL}/users`, {
|
||||
method: "POST",
|
||||
headers: request.headers,
|
||||
body: request.body,
|
||||
duplex: "half",
|
||||
});
|
||||
|
||||
const body = await request.json();
|
||||
const data = await response.json();
|
||||
|
||||
const response = await fetch(`${BACKEND_URL}/users`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
return Response.json(data, {
|
||||
status: response.status,
|
||||
});
|
||||
return Response.json(data, {
|
||||
status: response.status,
|
||||
});
|
||||
}
|
||||
19
app/api/users/user/id/[id]/route.ts
Normal file
19
app/api/users/user/id/[id]/route.ts
Normal 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);
|
||||
}
|
||||
21
app/api/users/user/route.ts
Normal file
21
app/api/users/user/route.ts
Normal 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);
|
||||
}
|
||||
19
app/api/users/user/username/[username]/route.ts
Normal file
19
app/api/users/user/username/[username]/route.ts
Normal 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);
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
25
components/ui/Button.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
47
components/ui/Dropdown.tsx
Normal file
47
components/ui/Dropdown.tsx
Normal 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
26
components/ui/Form.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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
21
components/ui/NavLink.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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>
|
||||
)
|
||||
);
|
||||
}
|
||||
17
components/wrappers/ProtectedRoute.tsx
Normal file
17
components/wrappers/ProtectedRoute.tsx
Normal 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>;
|
||||
}
|
||||
|
|
@ -13,19 +13,20 @@ const eslintConfig = defineConfig([
|
|||
plugins: {
|
||||
"unused-imports": unusedImports,
|
||||
},
|
||||
rules: {
|
||||
"no-unused-vars": "warn",
|
||||
"no-empty-function": "warn",
|
||||
"no-undef": "error",
|
||||
"react/self-closing-comp": "warn",
|
||||
"import/order": [
|
||||
"warn",
|
||||
rules: {
|
||||
"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([
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
}
|
||||
|
|
@ -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),
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
39
lib/logging/logger.ts
Normal 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;
|
||||
23
lib/server_functions/dataAccessLayer.ts
Normal file
23
lib/server_functions/dataAccessLayer.ts
Normal 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,
|
||||
};
|
||||
});
|
||||
44
lib/server_functions/userAuth.ts
Normal file
44
lib/server_functions/userAuth.ts
Normal 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
2136
package-lock.json
generated
File diff suppressed because it is too large
Load diff
10
package.json
10
package.json
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue