Skip to content

Commit

Permalink
feat: add full auth system
Browse files Browse the repository at this point in the history
  • Loading branch information
rtpa25 committed May 12, 2024
1 parent 46c3e40 commit eaf8b37
Show file tree
Hide file tree
Showing 17 changed files with 340 additions and 61 deletions.
30 changes: 19 additions & 11 deletions apps/api/src/controllers/user.controller.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,32 @@
import { type NextFunction, type Request, type RequestHandler, type Response } from "express";

import {
type UserCreateInputType,
type UserCreateOutputType,
type UserGetOutputType,
} from "@repo/models";

import { generateEncryptedToken, logger } from "@repo/lib";

import { type UserCreateType } from "../schemas/user.schema";
import { cookieService } from "../services/cookie.service";
import { userService } from "../services/user.service";

class UserController {
createUser: RequestHandler = async (
req: Request<object, object, UserCreateType["body"]>,
res: Response,
upsertUser: RequestHandler = async (
req: Request<object, object, UserCreateInputType["body"]>,
res: Response<UserCreateOutputType>,
next: NextFunction,
) => {
try {
const user = await userService.createUser(req.body.email);
logger.info(`User created: ${user.id}`);
const user = await userService.upsertUser(req.body.email);

const { token } = await generateEncryptedToken({
uid: user.id,
});

cookieService.setTokenCookie({ res, token });

res.status(201).send(user);
res.status(201).json({ user });
} catch (error) {
logger.error(error);
next(error);
Expand All @@ -34,18 +38,22 @@ class UserController {
const user = req.user;
await userService.deleteUser(user.id);
cookieService.clearTokenCookie({ res });
res.status(204).send();
res.status(204);
} catch (error) {
logger.error(error);
next(error);
}
};

getCurrentUser: RequestHandler = async (req: Request, res: Response, next: NextFunction) => {
getCurrentUser: RequestHandler = async (
req: Request,
res: Response<UserGetOutputType>,
next: NextFunction,
) => {
try {
const user = req.user;
res.status(200).send({
userId: user.id,
res.status(200).json({
user,
});
} catch (error) {
logger.error(error);
Expand Down
2 changes: 2 additions & 0 deletions apps/api/src/middlewares/auth.middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ export const authenticate: RequestHandler = async (
req.user = {
id: res.uid,
};

next();
} catch (error) {
logger.error(error);
next(error);
Expand Down
11 changes: 8 additions & 3 deletions apps/api/src/middlewares/validate.middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ import { type AnyZodObject } from "zod";

import { AppError } from "@repo/models";

const validate = (schema: AnyZodObject) => (req: Request, res: Response, next: NextFunction) => {
import { logger } from "@repo/lib";

const validate = (schema: AnyZodObject) => (req: Request, _res: Response, next: NextFunction) => {
try {
const safeParse = schema.safeParse({
query: req.query,
Expand All @@ -13,7 +15,10 @@ const validate = (schema: AnyZodObject) => (req: Request, res: Response, next: N
});

if (!safeParse.success) {
const errorMessages = safeParse.error.issues.map((issue) => issue.message);
const errorMessages = safeParse.error.issues.map(
(issue) => `${issue.path.join(".")} ${issue.message}`,
);
logger.error(errorMessages.join(", "));
// not really a meaningful error message for client, but these errors should ideally be caught on the client side, for forcefull server calls this is fine
throw new AppError({
code: "BAD_REQUEST",
Expand All @@ -23,7 +28,7 @@ const validate = (schema: AnyZodObject) => (req: Request, res: Response, next: N

next();
} catch (e: unknown) {
return res.status(400).send(e);
next(e);
}
};

Expand Down
5 changes: 3 additions & 2 deletions apps/api/src/routers/user.router.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
import { Router } from "express";

import { userCreateInputSchema } from "@repo/models";

import { userController } from "../controllers/user.controller";
import { authenticate } from "../middlewares/auth.middleware";
import validate from "../middlewares/validate.middleware";
import { userCreateSchema } from "../schemas/user.schema";

export const userRouter = Router({ mergeParams: true });

/**
* @method POST @url /users @desc create a new user and authenticate
*/
userRouter.post("/", validate(userCreateSchema), userController.createUser);
userRouter.post("/", validate(userCreateInputSchema), userController.upsertUser);

/**
* @method GET @url /users @desc get's the current authenticated user
Expand Down
8 changes: 0 additions & 8 deletions apps/api/src/schemas/user.schema.ts

This file was deleted.

10 changes: 7 additions & 3 deletions apps/api/src/services/user.service.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import { prismaClient } from "@repo/models";

class UserService {
async createUser(email: string) {
return prismaClient.user.create({
data: {
async upsertUser(email: string) {
return prismaClient.user.upsert({
where: {
email,
},
create: {
email,
},
update: {},
select: {
id: true,
},
Expand Down
4 changes: 4 additions & 0 deletions apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,12 @@
},
"license": "MIT",
"dependencies": {
"@repo/models": "workspace:*",
"@nextui-org/react": "^2.3.6",
"@t3-oss/env-nextjs": "^0.9.2",
"@tanstack/react-query": "^5.35.5",
"autoprefixer": "^10.4.19",
"axios": "^1.6.8",
"framer-motion": "^11.1.9",
"next": "^14.1.1",
"postcss": "^8.4.38",
Expand Down
10 changes: 10 additions & 0 deletions apps/web/src/app/(auth)/login/loading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { Spinner } from "@nextui-org/react";
import * as React from "react";

export default function Loading() {
return (
<div>
<Spinner color="primary" labelColor="primary" />
</div>
);
}
62 changes: 49 additions & 13 deletions apps/web/src/app/(auth)/login/page.tsx
Original file line number Diff line number Diff line change
@@ -1,29 +1,65 @@
"use client";

import { Button, Input } from "@nextui-org/react";
import { useMutation } from "@tanstack/react-query";
import { useRouter } from "next/navigation";
import React from "react";
import * as React from "react";

import { api } from "~/lib/api";

import Loading from "~/app/(auth)/login/loading";
import { useUser } from "~/hook/user-user";

const Login = () => {
const router = useRouter();
const [email, setEmail] = React.useState("");

const { data, isLoading } = useUser();

const submitHandler = (e: React.FormEvent<HTMLFormElement>) => {
const loginMutation = useMutation({
mutationFn: api.createUser,
onError(error) {
console.error(error);
},
onSuccess() {
router.push("/");
},
});

const submitHandler = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
router.push("/");
await loginMutation.mutateAsync({ email });
};

if (data) {
router.push("/");
return null;
}

return (
<main className="flex justify-center items-center h-screen">
<div className="bg-slate-900 md:w-1/2 w-full lg:w-1/3 p-4 m-4 rounded-md shadow-2xl shadow-slate-900 flex flex-col gap-y-8">
<div className="flex flex-col gap-y-2">
<h1 className="font-bold text-4xl">Login</h1>
<p>Please login to continue.</p>
</div>
<form onSubmit={submitHandler} className="flex flex-col items-end gap-y-4">
<Input type="email" variant={"bordered"} label="Email" />
<Button color="primary" type="submit">
Login
</Button>
</form>
{isLoading && <Loading />}
{!isLoading && !data && (
<>
<div className="flex flex-col gap-y-2">
<h1 className="font-bold text-4xl">Login</h1>
<p>Please login to continue.</p>
</div>
<form onSubmit={submitHandler} className="flex flex-col items-end gap-y-4">
<Input
type="email"
variant={"bordered"}
label="Email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<Button color="primary" type="submit" isLoading={loginMutation.isPending}>
Login
</Button>
</form>
</>
)}
</div>
</main>
);
Expand Down
54 changes: 34 additions & 20 deletions apps/web/src/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,38 +1,52 @@
"use client";

import { Button, useDisclosure } from "@nextui-org/react";
import { useRouter } from "next/navigation";

import Todo from "~/app/components/todo";
import TodoModal from "~/app/components/todoModal";
import { useUser } from "~/hook/user-user";

export default function Page(): JSX.Element {
export default function Page() {
const { isOpen, onOpen, onOpenChange } = useDisclosure();
const router = useRouter();

const { data, isLoading } = useUser();

const addTodo = () => {
onOpen();
};

if (!data && !isLoading) {
router.push("/login");
return null;
}

return (
<main className="max-w-screen-lg mx-auto p-6 flex flex-col gap-y-12">
<div className="flex justify-between items-center">
<h1 className="font-bold text-4xl">Todos</h1>
<Button color="primary" variant="shadow" onClick={addTodo}>
Add Todo
</Button>
</div>

<ul className="flex flex-col gap-y-4">
<Todo />
</ul>

<TodoModal
isOpen={isOpen}
onOpenChange={onOpenChange}
heading="Add Todo"
onPrimaryAction={() => {
console.info("Add Todo");
}}
/>
{data && (
<>
<div className="flex justify-between items-center">
<h1 className="font-bold text-4xl">Todos</h1>
<Button color="primary" variant="shadow" onClick={addTodo}>
Add Todo
</Button>
</div>

<ul className="flex flex-col gap-y-4">
<Todo />
</ul>

<TodoModal
isOpen={isOpen}
onOpenChange={onOpenChange}
heading="Add Todo"
onPrimaryAction={() => {
console.info("Add Todo");
}}
/>
</>
)}
</main>
);
}
10 changes: 9 additions & 1 deletion apps/web/src/app/providers.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
"use client";

import { NextUIProvider } from "@nextui-org/react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import * as React from "react";

export function Providers({ children }: { children: React.ReactNode }) {
return <NextUIProvider>{children}</NextUIProvider>;
const [queryClient] = React.useState(() => new QueryClient());

return (
<NextUIProvider>
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
</NextUIProvider>
);
}
11 changes: 11 additions & 0 deletions apps/web/src/hook/user-user.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { useQuery } from "@tanstack/react-query";

import { api } from "~/lib/api";

export function useUser() {
return useQuery({
queryFn: api.getUser,
queryKey: ["user"],
retry: 0,
});
}
Loading

0 comments on commit eaf8b37

Please sign in to comment.