Skip to content

Commit

Permalink
Merge pull request #13 from chuang8511/feat-login
Browse files Browse the repository at this point in the history
Feat login
  • Loading branch information
chuang8511 authored Apr 8, 2024
2 parents 1c124f8 + 27a301b commit 3521784
Show file tree
Hide file tree
Showing 17 changed files with 357 additions and 12 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"test-all": "jest",
"test-b-silent": "jest --silent src/tests",
"test-b": "jest src/tests",
"test-f": "jest src/client/tests",
"test-f": "jest --silent src/client/tests",
"test:watch": "jest --watchAll",
"presf": "npx webpack --config webpack.config.js --mode development ",
"sf": "npx webpack serve --mode development",
Expand Down
4 changes: 2 additions & 2 deletions src/client/Layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ const Layout = () => {
<Link to="/sign_up">Sign up</Link>
</li>

{/* <li>
<li>
<Link to="/login">Log in</Link>
</li> */}
</li>
{/* <li>
<Link to="/friends">Friends</Link>
</li>
Expand Down
24 changes: 24 additions & 0 deletions src/client/LoginPage/component/input.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import React from "react";

interface InputProps {
value: string;
onChange: (value: string) => void;
label: string;
type: "email" | "password";
placeholder?: string;
}

const LoginInput: React.FC<InputProps> = ({ value, onChange, label, type, placeholder }) => {
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
onChange(event.target.value);
};

return (
<div>
<label htmlFor={type}>{label}:</label>
<input id={type} type={type} placeholder={placeholder} value={value} onChange={handleChange} />
</div>
);
};

export default LoginInput;
80 changes: 80 additions & 0 deletions src/client/LoginPage/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import React, { useState } from "react";
import LoginInput from "./component/input";
import fetch from "node-fetch"

const LoginPage: React.FC = () => {
const [email, setLoginEmail] = useState<string>("");
const [password, setLoginPassword] = useState<string>("");

const [message, setLoginResultMessage] = useState<string>("");

const handleLogin = async (email: string, password: string, event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault()
console.log("Login data:", { email })
setLoginResultMessage("");

if (!(email && password)) {
setLoginResultMessage("Please fill in all fields")
return
}

try {
const response = await fetch("http://localhost:3001/v1/api/account/login" , {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({ email, password })
})

const status = response.status
const apiData = await response.json();
const failureReason = apiData.data?.failureReason

if (status == 201) {
setLoginResultMessage("Successfully Login")
// to display the friends page and set the session to keep login status

} else if (status == 403 && failureReason == "no user") {
setLoginResultMessage("No email is found")

} else if (status == 403 && failureReason == "wrong password") {
const remainingTimes = apiData.data.times
setLoginResultMessage("Password is wrong, you still have " + remainingTimes + " times to try today")

} else {
throw new Error("Failed to connect server")
}

} catch (error: any) {
console.error("Unexpected error", error.message);
setLoginResultMessage("Something weng wrong, please do it later.")
}

}
return (
<div>
<form onSubmit={(event) => handleLogin(email, password, event)}>
<LoginInput
value={email}
onChange={setLoginEmail}
label="Email"
type="email"
placeholder="xxx@gmail.com"
/>
<LoginInput
value={password}
onChange={setLoginPassword}
label="Password"
type="password"
placeholder="xxx"
/>
<button type="submit" data-testid="login-form">Login</button>
</form>
{message && <p data-testid="submit-message">{message}</p>}
</div>
)

}

export default LoginPage
Empty file added src/client/LoginPage/style.css
Empty file.
6 changes: 4 additions & 2 deletions src/client/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,17 @@ import FriendPageIndex from './friend_page';
import SignUpIndex from './SignUpPage';
import Layout from './Layout';
import NoPage from './NoPage';
import LoginPage from './LoginPage';

const App: React.FC = () => {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<Layout/>}>
{/* <Route path="/" element={<SignUpIndex/>}> */}
{/* <Route path="/" element={<LoginPage/>}> */}
<Route path="sign_up" element={<SignUpIndex/>}/>
<Route path="friends" element={<FriendPageIndex/>}/>
<Route path="login" element={<LoginPage/>} />
{/* <Route path="friends" element={<FriendPageIndex/>}/> */}
<Route/>
<Route/>
<Route path="*" element={<NoPage/>}/>
Expand Down
31 changes: 31 additions & 0 deletions src/client/tests/LoginPage/components/input.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import React from "react";
import { render, fireEvent } from "@testing-library/react"
import "@testing-library/jest-dom"
import LoginInput from "../../../LoginPage/component/input";

describe("LoginInput", () => {
it("renders email input", () => {
const fakeOnChange = jest.fn()
const { getByLabelText } = render(<LoginInput value="" onChange={fakeOnChange} label="Email" type="email" placeholder="xxx@gmail.com" />);
const emailInput = getByLabelText("Email:")
expect(emailInput).toHaveAttribute("type", "email")
})

it("renders password input", () =>{
const fakeOnChange = jest.fn()
const { getByLabelText } = render(<LoginInput value="" onChange={fakeOnChange} label="Password" type="password" placeholder="xxx" />);
const passwordInput = getByLabelText("Password:")
expect(passwordInput).toHaveAttribute("type", "password")
})

it("triggers the function when the input is updated", () => {
const fakeOnChange = jest.fn()
const { getByLabelText } = render(<LoginInput value="" onChange={fakeOnChange} label="Email" type="email" placeholder="xxx@gmail.com" />);
const emailInput = getByLabelText("Email:")
fireEvent.change(emailInput, { target: { value: "test@gmail.com" }})
expect(fakeOnChange).toHaveBeenCalledTimes(1)
expect(fakeOnChange).toHaveBeenCalledWith("test@gmail.com")

})

})
20 changes: 20 additions & 0 deletions src/client/tests/LoginPage/index.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import React from "react";
import { render, fireEvent, waitFor } from "@testing-library/react"
import LoginPage from "../../LoginPage";

describe("Login page", () => {
it("render without crashing", () => {
render(<LoginPage/ >)
})

it("display error message when submitting with empty fileds", async () => {
const { getByTestId } = render(<LoginPage />);
const loginBtn = getByTestId("login-form")
fireEvent.submit(loginBtn);

await waitFor(() => {
expect(getByTestId("submit-message").textContent).toBe("Please fill in all fields")
})
})
// todo: add the test case after knowing how to mock fetch
})
82 changes: 80 additions & 2 deletions src/controllers/AccountController.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import express from "express"
import { Account } from "../entity/Account.entity"
import { LoginRecord } from "../entity/LoginRecord.entity"
import { AccountPersistence } from "../persistences/AccountPersistence"

interface AccountInformation {
interface RegisterAccountInformation {
userName: string
email: string
password: string
Expand All @@ -12,10 +14,24 @@ interface RegisterAccountResponse {
data?: any
}

interface LoginInformation {
email: string
password: string
}

interface LoginResponse {
message: string
data?: {
failureReason?: "no user" | "wrong password"
times?: number
}
}

const DayLimit = 5

export class AccountController {

static register = async (req: express.Request<{}, {}, AccountInformation>, res: express.Response<RegisterAccountResponse>) => {
static register = async (req: express.Request<{}, {}, RegisterAccountInformation>, res: express.Response<RegisterAccountResponse>) => {
const { userName, email, password } = req.body;

const newAccount = new Account();
Expand All @@ -41,4 +57,66 @@ export class AccountController {

}

static login = async (req: express.Request<{}, {}, LoginInformation>, res: express.Response<LoginResponse>) => {
const { email, password } = req.body

try {
const account = await Account.findOne({
where: { email: email }
})

if (!account) {
res.status(403).json({
message: "cannot find account by " + email,
data: {
failureReason: "no user"
}
})
return
}

const loginRecord = new LoginRecord();
// todo: check why it is always null
const ip = req.headers["x-forwarded-for"] as string
const userIp = ip
loginRecord.email = email
loginRecord.loginIp = userIp

const isLogin = await account.comparePassword(password)

if (isLogin) {
loginRecord.result = "OK"

await loginRecord.save();
res.status(201).json({
message: "Successfully login",
data: {}
})
return
}

loginRecord.result = "NG"
await loginRecord.save();



const failureTimes = await AccountPersistence.getFailureTimes(email)

const remainingTime = DayLimit - failureTimes
res.status(403).json({
message: "wrong password for " + email,
data: {
failureReason: "wrong password",
times: remainingTime
}
})


} catch (error) {
res.status(500).json({ message: 'Failed to connect DB', data: {} });
}


}

}
5 changes: 3 additions & 2 deletions src/entity/Account.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import bcrypt from "bcrypt";

@Entity()
export class Account extends BaseEntity {

@PrimaryGeneratedColumn()
id: number;

Expand Down Expand Up @@ -31,8 +32,8 @@ export class Account extends BaseEntity {
}
}

async comparePassword(candidatePassword: string): Promise<boolean> {
return bcrypt.compare(candidatePassword, this.password);
async comparePassword(password: string): Promise<boolean> {
return bcrypt.compare(password, this.password);
}

@CreateDateColumn()
Expand Down
24 changes: 24 additions & 0 deletions src/entity/LoginRecord.entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { Column, Entity, Index, PrimaryGeneratedColumn, BeforeInsert, BeforeUpdate, CreateDateColumn, UpdateDateColumn, BaseEntity } from 'typeorm'

@Entity()
export class LoginRecord extends BaseEntity {
@PrimaryGeneratedColumn()
id: number;

@Column()
email: string;

@Column({
nullable: true
})
loginIp: string;

@Column()
result: string;

@CreateDateColumn()
created_at: Date;

@UpdateDateColumn()
updated_at: Date;
}
16 changes: 16 additions & 0 deletions src/persistences/AccountPersistence.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { LoginRecord } from "../entity/LoginRecord.entity"

export class AccountPersistence {
static getFailureTimes = async (email: string): Promise<number> => {
const today = new Date();
today.setHours(0, 0, 0, 0)
let failureTimes = await LoginRecord
.createQueryBuilder("record")
.where("record.email = :email", { email: email })
.where("record.result = :result", { result: "NG" })
.where("record.created_at >= :startDate", { startDate: today })
.getCount()

return failureTimes
}
}
1 change: 1 addition & 0 deletions src/routes/account.route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@ import { AccountController } from '../controllers/AccountController';
const accountRouter: Router = express.Router();

accountRouter.post("/", AccountController.register);
accountRouter.post("/login", AccountController.login);

export default accountRouter;
2 changes: 1 addition & 1 deletion src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import accountRouter from "./routes/account.route";

export const app = express();

const allowedOrigins = ['https://example.com', 'http://localhost:3000'];
const allowedOrigins = ['http://localhost:3000'];
const corsOptions: cors.CorsOptions = {
origin: function (origin, callback) {
if (!origin || allowedOrigins.includes(origin)) {
Expand Down
Loading

0 comments on commit 3521784

Please sign in to comment.