Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Feat/login #23

Merged
merged 9 commits into from
Nov 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ prod:

.PHONY: fmt
fmt:
npx prettier --write frontend/src backend/src
npx prettier --write frontend backend

.PHONY: update
update:
Expand Down
2 changes: 1 addition & 1 deletion backend/.prettierrc
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
"singleQuote": true,
"trailingComma": "all"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/*
Warnings:

- Added the required column `password` to the `User` table without a default value. This is not possible if the table is not empty.

*/
-- AlterTable
ALTER TABLE "User" ADD COLUMN "password" TEXT NOT NULL;
1 change: 1 addition & 0 deletions backend/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,5 @@ model User {
id Int @id @default(autoincrement())
email String @unique
name String?
password String
}
Comment on lines 14 to 18
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The password field is being stored as a plain String. This is a serious security issue as it exposes sensitive user data. Passwords should be hashed before being stored in the database. Consider using a library like bcrypt to hash passwords.

- password String
+ password String @db.VarChar(60)

The @db.VarChar(60) directive is used to ensure the hashed password fits into the database column. Bcrypt, for example, generates a string of length 60.


Commitable suggestion (Beta)
Suggested change
id Int @id @default(autoincrement())
email String @unique
name String?
password String
}
id Int @id @default(autoincrement())
email String @unique
name String?
- password String
+ password String @db.VarChar(60)
}

4 changes: 2 additions & 2 deletions backend/src/user/user.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export class UserController {

@Post()
create(
@Body() userData: { name?: string; email: string },
@Body() userData: { name?: string; email: string; password: string },
): Promise<UserModel> {
return this.userService.create(userData);
}
Expand All @@ -35,7 +35,7 @@ export class UserController {
@Patch(':id')
update(
@Param('id') id: string,
@Body() userData: { name?: string; email?: string },
@Body() userData: { name?: string; email?: string; password?: string },
) {
return this.userService.update(+id, userData);
}
Expand Down
2 changes: 1 addition & 1 deletion backend/test/jest-e2e.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"moduleNameMapper": {
"^src/(.*)$": "<rootDir>/../src/$1"
"^src/(.*)$": "<rootDir>/../src/$1"
},
"moduleFileExtensions": ["js", "json", "ts"],
"rootDir": ".",
Expand Down
4 changes: 3 additions & 1 deletion frontend/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ import "./globals.css";

// components
import { ThemeProvider } from "@/components/theme-provider";
import Nav from "@/components/Nav";
import { Toaster } from "@/components/ui/toaster";

// ui
import Nav from "@/app/ui/nav";

const inter = Inter({ subsets: ["latin"] });

export const metadata: Metadata = {
Expand Down
31 changes: 31 additions & 0 deletions frontend/app/lib/client-actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
"use client";

import { RedirectType, redirect } from "next/navigation";
import { toast } from "@/components/ui/use-toast";

export async function createUser(formData: FormData) {
const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/user`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
name: formData.get("name"),
email: formData.get("email"),
password: formData.get("password"),
}),
});
const data = await res.json();
if (!res.ok) {
toast({
title: res.status + " " + res.statusText,
description: data.message,
});
} else {
toast({
title: "Success",
description: "User created successfully.",
});
redirect("/user", RedirectType.push);
}
}
6 changes: 2 additions & 4 deletions frontend/components/Nav.tsx → frontend/app/ui/nav.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import Image from "next/image";
import { ModeToggle } from "./toggle-mode";
import { ModeToggle } from "@/components/toggle-mode";
import Link from "next/link";

export default function Nav() {
Expand All @@ -18,9 +18,7 @@ export default function Nav() {
/>
</li>
<li className="flex gap-8 items-center">
<Link href="/" className="">
Home
</Link>
<Link href="/">Home</Link>
<Link href="/user">User List</Link>
<Link href="/user/signup">Sign Up</Link>
<Link href="/playground/pong.html" target="_blank">
Expand Down
116 changes: 116 additions & 0 deletions frontend/app/ui/user/card.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
"use client";

import { redirect, RedirectType } from "next/navigation";

// components
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Button } from "@/components/ui/button";
import { toast } from "@/components/ui/use-toast";

export type User = { id: number; name?: string; email?: string };

export default function UserCard({ user }: { user: User }) {
async function updateUser(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault();
const { id, ...updateData } = Object.fromEntries(
new FormData(event.currentTarget),
);
const res = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/user/${user.id}`,
{
method: "PATCH",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(updateData),
},
);
const data = await res.json();
if (!res.ok) {
toast({
title: res.status + " " + res.statusText,
description: data.message,
});
} else {
toast({
title: "Success",
description: "User updated successfully.",
});
redirect("/user", RedirectType.push);
}
}
async function deleteUser(event: React.SyntheticEvent) {
event.preventDefault();
const res = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/user/${user.id}`,
{
method: "DELETE",
},
);
const data = await res.json();
if (!res.ok) {
toast({
title: res.status + " " + res.statusText,
description: data.message,
});
} else {
toast({
title: "Success",
description: "User deleted successfully.",
});
redirect("/user", RedirectType.push);
}
}
return (
<>
<Card className="w-[300px]">
<CardHeader>ID: {user.id}</CardHeader>
<CardContent>
<form onSubmit={updateUser} id={"UpdateUserForm." + user.id}>
<div className="grid w-full items-center gap-4">
<div className="flex flex-col space-y-1.5">
<Label htmlFor="name">Name</Label>
<Input
id="name"
type="text"
name="name"
defaultValue={user.name}
/>
</div>
<div className="flex flex-col space-y-1.5">
<Label htmlFor="email">Email</Label>
<Input
id="email"
type="email"
name="email"
defaultValue={user.email}
/>
</div>
</div>
</form>
</CardContent>
<CardFooter className="flex justify-between">
<Button type="button" onClick={deleteUser}>
Delete
</Button>
<Button
variant="outline"
type="submit"
form={"UpdateUserForm." + user.id}
>
Update
</Button>
</CardFooter>
</Card>
</>
);
}
40 changes: 40 additions & 0 deletions frontend/app/ui/user/create-form.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
"use client";

import { createUser } from "@/app/lib/client-actions";

// components
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Button } from "@/components/ui/button";

export default function UserCreateForm() {
return (
<form action={createUser}>
Copy link
Contributor

@coderabbitai coderabbitai bot Nov 3, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The action attribute in a form is typically used to specify the URL where the form data is sent on submission. Here, it seems you're trying to assign a JavaScript function to it, which won't work as expected. Instead, you should handle form submission using the onSubmit event. Remember to prevent the default form submission behavior using event.preventDefault() to avoid a page refresh.

- <form action={createUser}>
+ <form onSubmit={(event) => {event.preventDefault(); createUser();}}>

Commitable suggestion (Beta)
Suggested change
<form action={createUser}>
<form onSubmit={(event) => {event.preventDefault(); createUser();}}>

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is React's new feature. You should look into it.
https://react.dev/reference/react-dom/components/form

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@usatie Thank you for pointing out the new feature in React. I appreciate your input and the link you provided. I've reviewed the documentation and it seems you're correct. The action attribute can now accept a function in React, which is a departure from traditional HTML form behavior. This is a great example of how frameworks like React can enhance and simplify web development. I apologize for the oversight and appreciate your correction.

<div className="grid w-full items-center gap-8">
<div className="flex flex-col space-y-1.5">
<Label htmlFor="name">User Name</Label>
<Input id="name" type="text" name="name" placeholder="e.g. nop" />
</div>
<div className="flex flex-col space-y-1.5">
<Label htmlFor="email">Email</Label>
<Input
id="email"
type="email"
name="email"
placeholder="e.g. [email protected]"
/>
</div>
<div className="flex flex-col space-y-1.5">
<Label htmlFor="password">Password</Label>
<Input
id="password"
type="password"
name="password"
placeholder="Must have at least 6 characters"
/>
</div>
<Button type="submit">Sign Up</Button>
</div>
</form>
);
}
Comment on lines +10 to +40
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The form lacks state management for the input fields. You should use React's useState hook to manage the state of each input field. When the form is submitted, these state variables should be passed to the createUser function.

export default function UserCreateForm() {
  const [name, setName] = React.useState("");
  const [email, setEmail] = React.useState("");
  const [password, setPassword] = React.useState("");

  const handleSubmit = (event) => {
    event.preventDefault();
    createUser({ name, email, password });
  };

  return (
    <form onSubmit={handleSubmit}>
      <div className="grid w-full items-center gap-8">
        <div className="flex flex-col space-y-1.5">
          <Label htmlFor="name">User Name</Label>
          <Input id="name" type="text" name="name" placeholder="e.g. nop" value={name} onChange={(e) => setName(e.target.value)} />
        </div>
        <div className="flex flex-col space-y-1.5">
          <Label htmlFor="email">Email</Label>
          <Input id="email" type="email" name="email" placeholder="e.g. [email protected]" value={email} onChange={(e) => setEmail(e.target.value)} />
        </div>
        <div className="flex flex-col space-y-1.5">
          <Label htmlFor="password">Password</Label>
          <Input id="password" type="password" name="password" placeholder="Must have at least 6 characters" value={password} onChange={(e) => setPassword(e.target.value)} />
        </div>
        <Button type="submit">Sign Up</Button>
      </div>
    </form>
  );
}

2 changes: 1 addition & 1 deletion frontend/app/user/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import UserCard from "@/components/UserCard";
import UserCard from "@/app/ui/user/card";

async function getUser(id: number) {
const res = await fetch(`${process.env.API_URL}/user/${id}`, {
Expand Down
61 changes: 5 additions & 56 deletions frontend/app/user/signup/page.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,3 @@
"use client";

import { useRouter } from "next/navigation";

// components
import {
Card,
Expand All @@ -11,63 +7,16 @@ import {
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Button } from "@/components/ui/button";
import { useToast } from "@/components/ui/use-toast";
import Form from "@/app/ui/user/create-form";

export default function SignUp() {
const router = useRouter();
const { toast } = useToast();
async function createUser(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault();
const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/user`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(
Object.fromEntries(new FormData(event.currentTarget)),
),
});
const data = await res.json();
if (!res.ok) {
toast({
title: res.status + " " + res.statusText,
description: data.message,
});
} else {
toast({
title: "Success",
description: "User created successfully.",
});
router.push("/user");
router.refresh();
}
}

return (
<Card className="w-[300px]">
<CardHeader>Create Account</CardHeader>
<CardHeader>
<CardTitle>Create Account</CardTitle>
</CardHeader>
<CardContent>
<form onSubmit={createUser}>
<div className="grid w-full items-center gap-8">
<div className="flex flex-col space-y-1.5">
<Label htmlFor="name">Name</Label>
<Input id="name" type="text" name="name" placeholder="nop" />
</div>
<div className="flex flex-col space-y-1.5">
<Label htmlFor="email">Email</Label>
<Input
id="email"
type="email"
name="email"
placeholder="[email protected]"
/>
</div>
<Button type="submit">Sign Up</Button>
</div>
</form>
<Form />
</CardContent>
</Card>
);
Expand Down
2 changes: 1 addition & 1 deletion frontend/components.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,4 @@
"components": "@/components",
"utils": "@/lib/utils"
}
}
}
4 changes: 2 additions & 2 deletions frontend/next.config.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/** @type {import('next').NextConfig} */
const nextConfig = {}
const nextConfig = {};

module.exports = nextConfig
module.exports = nextConfig;
2 changes: 1 addition & 1 deletion frontend/postcss.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@ module.exports = {
tailwindcss: {},
autoprefixer: {},
},
}
};
Loading