basic next.js application using nextui
continuous-integration/drone/push Build is failing Details

pull/1/head
Guus van Meerveld 3 months ago
parent d61d3118a8
commit d966e9a909

@ -16,10 +16,12 @@
"full-test": "yarn test-build && yarn lint && yarn stylelint"
},
"dependencies": {
"@nextui-org/react": "^2.2.10",
"@prisma/client": "4.10.1",
"@tanstack/react-query": "^4.24.9",
"axios": "^1.1.2",
"bcrypt": "^5.1.0",
"framer-motion": "^11.0.8",
"fs-extra": "^11.2.0",
"iron-session": "^6.3.1",
"next": "^14.1.1",
@ -36,11 +38,14 @@
"@types/node": "^15.12.1",
"@types/react": "^17.0.9",
"@types/react-dom": "^17.0.6",
"autoprefixer": "^10.4.18",
"eslint": "^7.28.0",
"eslint-config-next": "13.1.6",
"postcss": "^8.4.35",
"prettier": "^2.3.1",
"prisma": "^5.8.1",
"sass": "^1.34.1",
"tailwindcss": "^3.4.1",
"typescript": "^4.3.2"
}
}

@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {}
}
};

@ -0,0 +1,29 @@
import { Metadata } from "next";
import { Providers } from "./providers";
import "@styles/global.scss";
export default function RootLayout({
children
}: {
children: React.ReactNode;
}) {
return (
<html lang="en" className="dark">
<body>
<Providers>{children}</Providers>
</body>
</html>
);
}
export const metadata: Metadata = {
title: {
default: "Portfolio",
template: "%s | Portfolio"
},
description: "Guus van Meerveld's portfolio",
applicationName: "Portfolio",
manifest: "/manifest.json"
};

@ -0,0 +1,11 @@
"use client";
import { Button } from "@nextui-org/react";
export default function Page() {
return (
<div>
<Button color="primary">Click me</Button>
</div>
);
}

@ -0,0 +1,7 @@
"use client";
import { NextUIProvider } from "@nextui-org/react";
export function Providers({ children }: { children: React.ReactNode }) {
return <NextUIProvider>{children}</NextUIProvider>;
}

@ -1,13 +0,0 @@
import styles from "./emptyPage.module.scss";
import { FC } from "react";
const EmptyPage: FC = ({ children }) => {
return (
<div className={styles.body}>
<div className="container">{children}</div>
</div>
);
};
export default EmptyPage;

@ -1,69 +0,0 @@
import Link from "next/link";
import styles from "./footer.module.scss";
import { FC } from "react";
import multipleClassNames from "@utils/multipleClassNames";
const Footer: FC = () => {
return (
<footer className={multipleClassNames("container", styles.main)}>
<div className="columns">
<div className="column col-8 col-md-12 col-mx-auto">
<div className="columns mb-2">
<div className="col col-12">
<h3>Guus van Meerveld</h3>
</div>
<div className="col col-12">
&middot;
<Link href="https://twitter.com/Guusvanmeerveld">
<a className="mx-2">Twitter</a>
</Link>
&middot;
<Link href="https://ko-fi.com/Guusvanmeerveld">
<a className="mx-2">Ko-fi</a>
</Link>
&middot;
<Link href="https://youtube.com/channel/UCYuqpoMay5SezCBrA_HKVWQ">
<a className="mx-2">Youtube</a>
</Link>
</div>
<div className="col col-12">
<span>Pages: </span>
<Link href={{ pathname: "/" }}>
<a className="mr-2">Home</a>
</Link>
&middot;
<Link href={{ pathname: "/blog" }}>
<a className="mx-2">Blog</a>
</Link>
&middot;
<Link href={{ pathname: "/link" }}>
<a className="mx-2">Links</a>
</Link>
&middot;
<Link href={{ pathname: "/login" }}>
<a className="mx-2">Login</a>
</Link>
</div>
<div className="col col-12">
<p>
Built with{" "}
<span role="img" aria-label="heart emoji">
</span>{" "}
by Guus van Meerveld, using{" "}
<Link href="https://picturepan2.github.io/spectre">
<a>Spectre.css</a>
</Link>
</p>
</div>
</div>
</div>
</div>
</footer>
);
};
export default Footer;

@ -1,16 +0,0 @@
import { FC } from "react";
import Footer from "@components/Footer";
import ThemeChanger from "@components/ThemeChanger";
const Layout: FC = ({ children }) => {
return (
<>
<ThemeChanger />
{children}
<Footer />
</>
);
};
export default Layout;

@ -1,71 +0,0 @@
import Link from "next/link";
import z from "zod";
import styles from "./linkComponent.module.scss";
import { FC, useCallback, useState } from "react";
import axios from "axios";
import { Link as LinkType } from "@prisma/client";
import { LinkId } from "@models/link";
import { parseAxiosError, parseAxiosResponse } from "@utils/fetch";
import useUser from "@utils/hooks/useUser";
import multipleClassNames from "@utils/multipleClassNames";
const LinkComponent: FC<{ link: LinkType }> = ({ link }) => {
const user = useUser();
const [error, setError] = useState<string | null>(null);
const [deleted, setDeleted] = useState(false);
const deleteLink = useCallback(async (id) => {
const parseUserInputResult = LinkId.safeParse(id);
if (!parseUserInputResult.success) {
setError(parseUserInputResult.error.message);
return;
}
const response = await axios
.delete("/api/link/delete", { params: { id } })
.then(parseAxiosResponse)
.catch(parseAxiosError);
if (!response.ok) {
setError(JSON.stringify(response.error));
return;
}
setDeleted(true);
}, []);
if (deleted) return <></>;
return (
<div className={multipleClassNames("bg-gray", "s-rounded", styles.body)}>
<div className="container">
<div className="columns">
<div className="col col-11">
<Link href={link.remoteAddress}>
<a>{link.location}</a>
</Link>
</div>
{user !== null && (
<div className="col col-1">
<Link href={`/link/edit/${link.id}`}>
<a className="mr-2">edit</a>
</Link>
<a className={styles.delete} onClick={() => deleteLink(link.id)}>
delete
</a>
</div>
)}
</div>
</div>
</div>
);
};
export default LinkComponent;

@ -1,104 +0,0 @@
import { useRouter } from "next/router";
import { FC, FormEvent, useCallback, useState } from "react";
import axios from "axios";
import { LoginCredentials } from "@models/login";
import { Response } from "@models/response";
import { parseUserInputError } from "@utils/errors";
import { parseAxiosError, parseAxiosResponse } from "@utils/fetch";
const LoginForm: FC = () => {
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const [rememberMe, setRememberMe] = useState(false);
const [error, setError] = useState<string | null>(null);
const router = useRouter();
const login = useCallback(
async (e: FormEvent) => {
e.preventDefault();
const parseUserInputResult = LoginCredentials.safeParse({
username,
password,
rememberMe
});
if (!parseUserInputResult.success) {
setError(parseUserInputError(parseUserInputResult.error.message));
return;
}
const response: Response = await axios
.post("/api/login", parseUserInputResult.data)
.then(parseAxiosResponse)
.catch(parseAxiosError);
if (response.ok) {
router.push("/blog");
} else {
setError(JSON.stringify(response.error));
}
},
[username, password, rememberMe, router]
);
return (
<form onSubmit={login}>
<div className="form-group">
<label className="form-label" htmlFor="email">
Email address
</label>
<input
required
className="form-input"
onChange={(e) => setUsername(e.target.value)}
name="username"
type="email"
id="email"
placeholder="mail@example.com"
/>
<label className="form-label" htmlFor="password">
Password
</label>
<input
required
className="form-input"
placeholder="Password"
onChange={(e) => setPassword(e.target.value)}
minLength={8}
maxLength={128}
name="password"
type="password"
id="password"
/>
<label className="form-checkbox">
<input
checked={rememberMe}
onChange={() => setRememberMe((state) => !state)}
name="rememberMe"
type="checkbox"
/>
<i className="form-icon" /> Remember me
</label>
{error !== null && (
<div className="toast toast-error mb-2">
<button
className="btn btn-clear float-right"
onClick={() => setError(null)}
/>
{error}
</div>
)}
<input className="btn btn-primary" type="submit" value="Login" />
</div>
</form>
);
};
export default LoginForm;

@ -1,54 +0,0 @@
import Link from "next/link";
import styles from "./post.module.scss";
import { FC } from "react";
import { Post, User } from "@prisma/client";
import multipleClassNames from "@utils/multipleClassNames";
const PostsPage: FC<{
post: Post & {
author: User;
};
}> = ({ post }) => {
return (
<div className="columns mt-2">
<div
className={multipleClassNames(
"column col-4 col-md-12 col-ml-auto bg-gray",
styles.body
)}
>
<h3>{post.title}</h3>
{post.tags.map((tag) => (
<span key={tag} className="chip">
{tag}
</span>
))}
{post.content && (
<p className="mt-2 mb-0">
{post.content?.length > 300
? post.content.slice(0, 300)
: post.content}{" "}
<Link href={`/blog/${post.id}`}>Continue reading</Link>
</p>
)}
</div>
<div
className={multipleClassNames(
"column col-4 col-md-12 col-mr-auto bg-gray",
styles.body,
styles.info
)}
>
<h5>Posted on {new Date(post.createdAt).toLocaleDateString()}</h5>
<h6>By {post.author.name}</h6>
</div>
</div>
);
};
export default PostsPage;

@ -1,111 +0,0 @@
import { useRouter } from "next/router";
import { FC, FormEvent, useCallback, useState } from "react";
import axios from "axios";
import { Response } from "@models/response";
import { SignupCredentials } from "@models/signup";
import { parseUserInputError } from "@utils/errors";
import { parseAxiosError, parseAxiosResponse } from "@utils/fetch";
const SignupForm: FC = () => {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [username, setUsername] = useState("");
const [error, setError] = useState<string | null>(null);
const router = useRouter();
const signup = useCallback(
async (e: FormEvent): Promise<void> => {
e.preventDefault();
const parseUserInputResult = SignupCredentials.safeParse({
email,
password,
name: username
});
if (!parseUserInputResult.success) {
setError(parseUserInputError(parseUserInputResult.error.message));
return;
}
const response: Response = await axios
.post("/api/signup", parseUserInputResult.data)
.then(parseAxiosResponse)
.catch(parseAxiosError);
if (response.ok) {
router.push("/blog");
} else {
setError(JSON.stringify(response.error));
}
},
[email, password, username, router]
);
return (
<form onSubmit={signup}>
<div className="form-group">
<label className="form-label" htmlFor="email">
Email address
</label>
<input
required
className="form-input"
onChange={(e) => setEmail(e.target.value)}
name="email"
type="email"
id="email"
placeholder="mail@example.com"
/>
<label className="form-label" htmlFor="password">
Password
</label>
<input
required
className="form-input"
placeholder="Password"
onChange={(e) => setPassword(e.target.value)}
minLength={8}
maxLength={128}
name="password"
type="password"
id="password"
/>
<label className="form-label" htmlFor="password">
Name
</label>
<input
required
className="form-input"
placeholder="Full name"
onChange={(e) => setUsername(e.target.value)}
maxLength={32}
name="name"
type="text"
id="name"
/>
{error !== null && (
<div className="toast toast-error mt-2">
<button
className="btn btn-clear float-right"
onClick={() => setError(null)}
/>
{error}
</div>
)}
<input className="btn btn-primary mt-2" type="submit" value="Signup" />
</div>
</form>
);
};
export default SignupForm;

@ -1,15 +0,0 @@
import { FC } from "react";
const Tags: FC<{ tags: string[] }> = ({ tags }) => {
return (
<>
{tags.map((tag) => (
<span key={tag} className="chip">
{tag}
</span>
))}
</>
);
};
export default Tags;

@ -1,30 +0,0 @@
import { useTheme } from "next-themes";
import styles from "./themeChanger.module.scss";
import { FC, useEffect, useState } from "react";
const ThemeChanger: FC = () => {
const [mounted, setMounted] = useState(false);
const { theme, setTheme } = useTheme();
useEffect(() => {
setMounted(true);
}, []);
if (!mounted) return <></>;
return (
// eslint-disable-next-line jsx-a11y/click-events-have-key-events
<a
role="button"
tabIndex={0}
className={styles.main}
onClick={() => setTheme(theme == "light" ? "dark" : "light")}
>
{theme}
</a>
);
};
export default ThemeChanger;

@ -1,73 +0,0 @@
import Image from "next/image";
import Link from "next/link";
import styles from "./user.module.scss";
import { FC } from "react";
import Owner from "@models/owner";
import multipleClassNames from "@utils/multipleClassNames";
const User: FC<{ owner: Owner }> = ({ owner }) => {
return (
<div className={styles.main}>
<div className="container">
<div className="columns">
<div
className={multipleClassNames(
"column",
"col-4",
"col-mx-auto",
styles.avatarCol
)}
>
<div className={styles.avatarContainer}>
<Image
src={owner.avatar ?? ""}
className={styles.avatar}
width={256}
height={256}
alt={`${owner.name}'s avatar`}
/>
</div>
</div>
<div className="column col-8 col-md-12 col-mx-auto">
<h1>{owner.fullName}</h1>
<h3>{owner.description}</h3>
<p>
<Link
target="_blank"
rel="noreferrer"
href={owner.contact.git}
className="btn btn-primary mr-2"
>
Git
</Link>
<Link
target="_blank"
rel="noreferrer"
href={owner.contact.linkedin}
className="btn btn-primary mr-2"
>
Git
</Link>
<Link
href={`mailto:${owner.contact.email}`}
className="btn btn-primary"
>
Contact
</Link>
</p>
</div>
</div>
</div>
</div>
);
};
export default User;

@ -1,3 +0,0 @@
.body {
padding: 10rem 0;
}

@ -1,6 +0,0 @@
$margin: 1rem;
.main {
margin-top: $margin;
margin-bottom: $margin;
}

@ -1,8 +0,0 @@
.body {
padding: 1rem;
margin-bottom: 1rem;
}
.delete {
cursor: pointer;
}

@ -1,7 +0,0 @@
.body {
padding: 1rem;
}
.info {
text-align: right;
}

@ -1,7 +0,0 @@
.card {
border: 0;
}
.main {
margin-bottom: 3rem;
}

@ -1,7 +0,0 @@
.main {
position: absolute;
top: 1rem;
right: 1rem;
cursor: pointer;
}

@ -1,17 +0,0 @@
.main {
padding-top: 10rem;
margin-bottom: 2rem;
}
.avatar {
border-radius: 5px;
}
.avatarContainer {
margin-right: 2rem;
}
.avatarCol {
display: flex;
justify-content: right;
}

@ -1,11 +0,0 @@
import z from "zod";
import { OwnerModel } from "./owner";
export const LandingModel = z.object({
owner: OwnerModel
});
export type Landing = z.infer<typeof LandingModel>;
export default Landing;

@ -1,15 +0,0 @@
import z from "zod";
export const LinkId = z.number().int().positive();
export const LinkIdFromString = z.preprocess(
(a) => parseInt(z.string().parse(a)),
LinkId
);
const Link = z.object({
remoteAddress: z.string().url(),
location: z.string()
});
export default Link;

@ -1,7 +0,0 @@
import z from "zod";
export const LoginCredentials = z.object({
username: z.string().email(),
password: z.string().min(8).max(128),
rememberMe: z.boolean()
});

@ -1,17 +0,0 @@
import z from "zod";
export const OwnerModel = z.object({
fullName: z.string(),
name: z.string(),
description: z.string(),
avatar: z.string().optional(),
contact: z.object({
email: z.string().email(),
linkedin: z.string().url(),
git: z.string().url()
})
});
export type Owner = z.infer<typeof OwnerModel>;
export default Owner;

@ -1,8 +0,0 @@
import z from "zod";
export const Post = z.object({
title: z.string().trim(),
content: z.string().min(100).trim(),
tags: z.string().trim(),
publish: z.boolean()
});

@ -1,27 +0,0 @@
import z from "zod";
interface Error {
ok: false;
error?: unknown;
}
export const ErrorModel: z.ZodType<Error> = z.object({
ok: z.literal(false),
error: z.unknown()
});
interface Ok {
ok: true;
data?: unknown;
}
export const OkModel: z.ZodType<Ok> = z.object({
ok: z.literal(true),
data: z.unknown()
});
export type Response = Ok | Error;
const ResponseModel: z.ZodType<Response> = ErrorModel.or(OkModel);
export default ResponseModel;

@ -1,7 +0,0 @@
import z from "zod";
export const SignupCredentials = z.object({
email: z.string().email(),
password: z.string().min(8).max(128),
name: z.string().max(32)
});

@ -1,8 +0,0 @@
import { z } from "zod";
import { SignupCredentials } from "./signup";
export const User = SignupCredentials.extend({
id: z.number(),
admin: z.boolean()
});

@ -1,9 +0,0 @@
import type { DefaultSeoProps } from "next-seo";
const SEO: DefaultSeoProps = {
titleTemplate: "%s | Guus van Meerveld",
defaultTitle: "Guus van Meerveld",
description: "Guus van Meerveld's portfolio"
};
export default SEO;

@ -1,8 +0,0 @@
.main {
height: 100vh;
margin-bottom: 1rem;
display: flex;
justify-content: center;
align-items: center;
}

@ -1,35 +0,0 @@
import { NextPage } from "next";
import { useRouter } from "next/router";
import styles from "./404.module.scss";
import multipleClassNames from "@utils/multipleClassNames";
import Layout from "@components/Layout";
const NotFound: NextPage = () => {
const router = useRouter();
return (
<Layout>
<div className={multipleClassNames("empty", styles.main)}>
<div>
<div className="empty-icon">
<i className="icon icon-stop"></i>
</div>
<p className="empty-title h5">Page not found</p>
<p className="empty-subtitle">
The page has either been deleted or moved
</p>
<div className="empty-action">
<button onClick={() => router.back()} className="btn btn-primary">
Go back
</button>
</div>
</div>
</div>
</Layout>
);
};
export default NotFound;

@ -1,25 +0,0 @@
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { DefaultSeo } from "next-seo";
import { ThemeProvider } from "next-themes";
import type { AppProps } from "next/app";
import SEO from "../next-seo.config";
import "@styles/globals.scss";
const queryClient = new QueryClient({
defaultOptions: { queries: { refetchOnWindowFocus: false } }
});
const App = ({ Component, pageProps }: AppProps): JSX.Element => (
<>
<DefaultSeo {...SEO} />
<ThemeProvider>
<QueryClientProvider client={queryClient}>
<Component {...pageProps} />
</QueryClientProvider>
</ThemeProvider>
</>
);
export default App;

@ -1,3 +0,0 @@
.body {
padding: 10rem 0;
}

@ -1,118 +0,0 @@
import { NextPage } from "next";
import { NextSeo } from "next-seo";
import styles from "./admin.module.scss";
import { Post, User } from "@prisma/client";
import prisma from "@utils/prisma";
import { withSessionSsr } from "@utils/session";
import Layout from "@components/Layout";
const AdminPage: NextPage<{
user: User;
users: User[];
posts: (Post & { author: User })[];
}> = ({ user, users, posts }) => {
return (
<Layout>
<NextSeo title="Admin" />
<div className={styles.body}>
<div className="container">
<div className="columns">
<div className="col col-8 col-mx-auto">
<h3>Welcome {user.name}</h3>
</div>
<div className="col col-8 col-md-12 col-mx-auto py-2">
<h4>Users</h4>
<table className="table table-striped table-hover mb-2">
<thead>
<tr>
<th>id</th>
<th>name</th>
<th>email</th>
<th>admin</th>
<th>postCount</th>
</tr>
</thead>
<tbody>
{users.map((user, i) => (
<tr key={user.id} className={i % 2 === 0 ? "active" : ""}>
<td>{user.id}</td>
<td>{user.name}</td>
<td>{user.email}</td>
<td>{user.admin ? "true" : "false"}</td>
<td>0</td>
</tr>
))}
</tbody>
</table>
</div>
<div className="col col-8 col-md-12 col-mx-auto">
<h4>Posts</h4>
<table className="table table-striped table-hover">
<thead>
<tr>
<th>id</th>
<th>title</th>
<th>content</th>
<th>published</th>
<th>createdAt</th>
<th>tags</th>
<th>author</th>
</tr>
</thead>
<tbody>
{posts.map((post, i) => (
<tr key={post.id} className={i % 2 === 0 ? "active" : ""}>
<td>{post.id}</td>
<td>{post.title}</td>
<td>{post.content}</td>
<td>{post.published ? "true" : "false"}</td>
<td>{new Date(post.createdAt).toLocaleString()}</td>
<td>{post.tags}</td>
<td>{post.author.name}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
</div>
</Layout>
);
};
export const getServerSideProps = withSessionSsr(async ({ req }) => {
const user = req.session.user;
if (user === undefined || !user.admin) return { notFound: true };
const posts = await prisma.post.findMany({
orderBy: { createdAt: "desc" },
take: 5,
include: { author: true }
});
const users = await prisma.user.findMany({
orderBy: { id: "desc" },
take: 5
});
return {
props: {
user,
users,
posts: posts.map((post) => ({
...post,
createdAt: post.createdAt.getTime(),
tags: post.tags.join(", "),
content: post.content?.slice(0, 100).concat("...")
}))
}
};
});
export default AdminPage;

@ -1,49 +0,0 @@
import { NextApiHandler } from "next";
import { Post } from "@models/post";
import { Response } from "@models/response";
import { methodNotAllowed, unauthorized } from "@utils/errors";
import prisma from "@utils/prisma";
import { withIronSession } from "@utils/session";
const handle: NextApiHandler<Response> = async (req, res) => {
if (req.method?.toUpperCase() != "POST") {
res.status(405).json(methodNotAllowed);
return;
}
const postData = Post.safeParse(req.body);
if (!postData.success) {
res.status(403).json({ ok: false, error: postData.error });
return;
}
const user = req.session.user;
if (user === undefined) {
res.status(401).json(unauthorized);
return;
}
const { publish, ...data } = postData.data;
await prisma.user
.update({
where: { id: user.id },
data: {
posts: {
create: {
...data,
published: publish,
tags: data.tags.split(" ")
}
}
}
})
.then(() => res.json({ ok: true, data: "Successfully created new post" }))
.catch((error) => res.status(500).json({ ok: false, error }));
};
export default withIronSession(handle);

@ -1,58 +0,0 @@
import { NextApiHandler } from "next";
import z from "zod";
import { LinkIdFromString } from "@models/link";
import { Response } from "@models/response";
import { methodNotAllowed, unauthorized } from "@utils/errors";
import prisma from "@utils/prisma";
import { withIronSession } from "@utils/session";
const handler: NextApiHandler<Response> = async (req, res) => {
if (req.method?.toUpperCase() !== "DELETE") {
res.status(405).json(methodNotAllowed);
return;
}
const user = req.session.user;
if (user === undefined) {
res.status(401).json(unauthorized);
return;
}
const parsePostDataResult = z
.object({
id: LinkIdFromString
})
.safeParse(req.query);
if (!parsePostDataResult.success) {
res
.status(400)
.json({ ok: false, error: parsePostDataResult.error.message });
return;
}
const link = await prisma.link.findUnique({
where: { id: parsePostDataResult.data.id },
include: { author: true }
});
if (link === null) {
res.status(400).json({ ok: false, error: "Link does not exist" });
return;
}
if (link.authorId !== user.id) {
res.status(401).json(unauthorized);
return;
}
await prisma.link
.delete({ where: { id: link.id } })
.then(() => res.json({ ok: true, data: "Successfully deleted link" }))
.catch((error) => res.status(500).json({ ok: false, error }));
};
export default withIronSession(handler);

@ -1,43 +0,0 @@
import { NextApiHandler } from "next";
import Link from "@models/link";
import { Response } from "@models/response";
import { methodNotAllowed, unauthorized } from "@utils/errors";
import prisma from "@utils/prisma";
import { withIronSession } from "@utils/session";
const handler: NextApiHandler<Response> = async (req, res) => {
if (req.method?.toUpperCase() !== "POST") {
res.status(405).json(methodNotAllowed);
return;
}
const user = req.session.user;
if (user === undefined) {
res.status(401).json(unauthorized);
return;
}
const parsedInputResult = Link.safeParse(req.body);
if (!parsedInputResult.success) {
res.status(400).json({ ok: false, error: parsedInputResult.error.message });
return;
}
await prisma.user
.update({
where: { id: user.id },
data: { links: { create: parsedInputResult.data } }
})
.then(() => res.json({ ok: true, data: "Successfully created link" }))
.catch((error) => {
console.log(error);
return res.status(500).json({ ok: false, error });
});
};
export default withIronSession(handler);

@ -1,54 +0,0 @@
import bcrypt from "bcrypt";
import { NextApiHandler } from "next";
import { LoginCredentials } from "@models/login";
import { Response } from "@models/response";
import { methodNotAllowed, unauthorized } from "@utils/errors";
import prisma from "@utils/prisma";
import { withIronSession } from "@utils/session";
const handle: NextApiHandler<Response> = async (req, res) => {
if (req.method?.toUpperCase() !== "POST") {
res.status(405).json(methodNotAllowed);
return;
}
const loginCredentials = LoginCredentials.safeParse(req.body);
if (!loginCredentials.success) {
res.status(403).json({ ok: false, error: loginCredentials.error });
return;
}
const email = loginCredentials.data.username;
const user = await prisma.user.findUnique({ where: { email } });
if (user === null) {
res.status(401).json(unauthorized);
return;
}
const password = loginCredentials.data.password;
const isCorrect = await new Promise((resolve, reject) =>
bcrypt.compare(password, user.password, (err, result) => {
if (err) reject(err);
else if (result !== undefined) resolve(result);
})
);
if (!isCorrect) {
res.status(401).json(unauthorized);
return;
}
req.session.user = user;
await req.session.save();
res.json({ ok: true, data: "Login successfull" });
};
export default withIronSession(handle);

@ -1,28 +0,0 @@
import { NextApiHandler } from "next";
import { Response } from "@models/response";
import { methodNotAllowed, unauthorized } from "@utils/errors";
import { withIronSession } from "@utils/session";
const handle: NextApiHandler<Response> = (req, res) => {
if (req.method?.toUpperCase() != "GET") {
res.status(405).json(methodNotAllowed);
return;
}
const user = req.session.user;
if (user === undefined) {
res.status(401).json(unauthorized);
return;
}
req.session.destroy();
req.session.user = undefined;
res.json({ ok: true, data: "Logout successfull" });
};
export default withIronSession(handle);

@ -1,17 +0,0 @@
import { NextApiHandler } from "next";
import { Response } from "@models/response";
import { registrationIsEnabled } from "@utils/config";
import { methodNotAllowed } from "@utils/errors";
const handler: NextApiHandler<Response> = (req, res) => {
if (req.method?.toUpperCase() !== "GET") {
res.status(405).json(methodNotAllowed);
return;
}
res.json({ ok: true, data: { registrationIsEnabled } });
};
export default handler;

@ -1,62 +0,0 @@
import bcrypt from "bcrypt";
import { NextApiHandler } from "next";
import { Response } from "@models/response";
import { SignupCredentials } from "@models/signup";
import { registrationIsEnabled, saltRoundsForPassword } from "@utils/config";
import { methodNotAllowed } from "@utils/errors";
import prisma from "@utils/prisma";
import { withIronSession } from "@utils/session";
const handle: NextApiHandler<Response> = async (req, res) => {
if (!registrationIsEnabled) {
res
.status(403)
.json({ ok: false, error: "Registration is not enabled on this server" });
return;
}
if (req.method?.toUpperCase() != "POST") {
res.status(405).json(methodNotAllowed);
return;
}
const signupCredentials = SignupCredentials.safeParse(req.body);
if (!signupCredentials.success) {
res.status(403).json({ ok: false, error: signupCredentials.error });
return;
}
const password: string = await new Promise((resolve, reject) =>
bcrypt.hash(
signupCredentials.data.password,
saltRoundsForPassword,
(err, hash) => {
if (err) return reject(err);
else if (hash) return resolve(hash);
}
)
);
await prisma.user
.create({
data: {
email: signupCredentials.data.email,
name: signupCredentials.data.name,
password,
admin: process.env.ADMIN_EMAIL === signupCredentials.data.email
}
})
.then(async (user) => {
req.session.user = user;
await req.session.save();
res.json({ ok: true, data: "Signup successfull" });
})
.catch((error) => res.status(500).json({ ok: false, error }));
};
export default withIronSession(handle);

@ -1,24 +0,0 @@
import { NextApiHandler } from "next";
import { Response } from "@models/response";
import { methodNotAllowed, unauthorized } from "@utils/errors";
import { withIronSession } from "@utils/session";
const handler: NextApiHandler<Response> = (req, res) => {
if (req.method?.toUpperCase() !== "GET") {
res.status(405).json(methodNotAllowed);
return;
}
const user = req.session.user;
if (user === undefined) {
res.status(401).json(unauthorized);
return;
}
res.json({ ok: true, data: user });
};
export default withIronSession(handler);

@ -1,3 +0,0 @@
.body {
padding: 10rem 0;
}

@ -1,84 +0,0 @@
import { GetStaticPaths, GetStaticProps, NextPage } from "next";