From 02d7c6a1d434874c14465b423b1f6844f023d58d Mon Sep 17 00:00:00 2001 From: Netcup Gituser Date: Tue, 5 Dec 2023 23:13:29 +0100 Subject: [PATCH] change username and password --- src/controllers/userController.ts | 110 ++++++++++++++++++++++++------ src/models/user.ts | 12 +++- src/routes/userRoutes.ts | 14 +++- src/utils/constants.ts | 13 ++++ src/utils/utils.ts | 13 ++++ src/validation/validation.ts | 56 +++++++++++++++ 6 files changed, 193 insertions(+), 25 deletions(-) create mode 100644 src/validation/validation.ts diff --git a/src/controllers/userController.ts b/src/controllers/userController.ts index a71b1d6..b21ff40 100644 --- a/src/controllers/userController.ts +++ b/src/controllers/userController.ts @@ -1,16 +1,22 @@ import { Request, Response } from "express"; -import bcrypt from "bcrypt"; import { User } from "../models/user"; -import { saveSession } from "../utils/utils"; +import { + saveSession, + hashPassword, + decodeBase64, + matchPassword, +} from "../utils/utils"; import { MONGODB_IGNORED_FIELDS } from "../utils/constants"; +import { isUsernameValid, isPasswordValid } from "../validation/validation"; +import { Session } from "../models/session"; export async function SignUp(req: Request, res: Response) { - if (!req.body.accountName || !req.body.username || !req.body.password) { + const { accountName, username, password } = req.body; + + if (!accountName || !username || !password) { return res.status(400).json({ status: "err" }); } - const { accountName, username, password } = req.body; - const existingUser = await User.findOne({ accountName }) .select("accountName -_id") .lean(); @@ -19,18 +25,12 @@ export async function SignUp(req: Request, res: Response) { return res.status(400).json({ status: 1 }); } - const isBase64Password = - /^([A-Za-z0-9+/]{4})*([A-Za-z0-9+/]{4}|[A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{2}==)$/.test( - password - ); - - if (!isBase64Password) { + if (!isPasswordValid(password)) { return res.status(400).json({ status: "err" }); } - const decodedPassword = Buffer.from(password, "base64").toString("utf-8"); - - const hashedPassword = await bcrypt.hash(decodedPassword, 10); + const decodedPassword = decodeBase64(password); + const hashedPassword = await hashPassword(decodedPassword); const user = new User({ accountName: accountName, @@ -70,12 +70,8 @@ export async function Login(req: Request, res: Response) { return res.status(401).json({ status: "err" }); } - const decodedPassword = Buffer.from(password, "base64").toString("utf-8"); - - const isPasswordValid = await bcrypt.compare( - decodedPassword, - user.password - ); + const decodedPassword = decodeBase64(password); + const isPasswordValid = await matchPassword(decodedPassword, user.password); if (!isPasswordValid) { return res.status(401).json({ status: "err" }); @@ -100,8 +96,6 @@ export async function GetUserProfile(req: Request, res: Response) { return res.status(404).json({ status: "err" }); } - console.log("user:", user); - res.json({ accountName: user.accountName, username: user.username, @@ -111,3 +105,75 @@ export async function GetUserProfile(req: Request, res: Response) { res.status(500).json({ status: "err" }); } } + +export async function ChangeUsername(req: Request, res: Response) { + const { accountName, newUsername } = req.body; + + if (!accountName || !newUsername) { + return res.status(400).json({ status: "err" }); + } + + if (!isUsernameValid(newUsername)) { + return res.status(400).json({ status: "err" }); + } + + try { + await User.updateOne( + { accountName: accountName }, + { $set: { username: newUsername } } + ); + + res.status(200).json({ status: "ok" }); + } catch (error) { + console.error(error); + res.status(500).json({ status: "err" }); + } +} + +export async function ChangePassword(req: Request, res: Response) { + const { accountName, currentPassword, newPassword } = req.body; + + if (!accountName || !currentPassword || !newPassword) { + return res.status(400).json({ status: "err" }); + } + + try { + const decodedCurrentPassword = decodeBase64(currentPassword); + + const existingUser = await User.findOne({ accountName }) + .select("password -_id") + .lean(); + + if (!existingUser) { + return res.status(400).json({ status: 1 }); + } + + const decodedNewPassword = decodeBase64(newPassword); + + if (!isPasswordValid(decodedCurrentPassword)) { + return res.status(400).json({ status: "err" }); + } + + const isPasswordMatching = await matchPassword( + decodedCurrentPassword, + existingUser.password + ); + + if (!isPasswordMatching || !isPasswordValid(decodedNewPassword)) { + return res.status(401).json({ status: "err" }); + } + + await User.updateOne( + { accountName: accountName }, + { $set: { password: await hashPassword(decodedNewPassword) } } + ); + + res.status(200).json({ status: "ok" }); + + // delete all sessions + await Session.deleteMany({ accountName: accountName }); + } catch (error) { + console.error(error); + res.status(500).json({ status: "err" }); + } +} diff --git a/src/models/user.ts b/src/models/user.ts index 0dd1d86..a0c0e61 100644 --- a/src/models/user.ts +++ b/src/models/user.ts @@ -1,5 +1,15 @@ import mongoose, { InferSchemaType, Schema } from "mongoose"; +export interface IUser extends Document { + accountName: string; + username: string; + password: string; + followers: number; + following: number; + visited: number; + isAdmin: boolean; +} + export const userSchema = new Schema({ accountName: String, username: String, @@ -24,4 +34,4 @@ export const userSchema = new Schema({ export type User = InferSchemaType; -export const User = mongoose.model("User", userSchema); +export const User = mongoose.model("User", userSchema); diff --git a/src/routes/userRoutes.ts b/src/routes/userRoutes.ts index 6db1231..aec0db8 100644 --- a/src/routes/userRoutes.ts +++ b/src/routes/userRoutes.ts @@ -1,6 +1,7 @@ -import express from "express"; +import express, { Router } from "express"; const router = express.Router(); import * as userController from "../controllers/userController"; +import { sessionProtection } from "../middleware/authMiddleware"; /** * @swagger @@ -126,6 +127,15 @@ router.post("/signup", userController.SignUp); */ router.post("/login", userController.Login); -router.get("/profile/:accountName", userController.GetUserProfile); +router.get( + "/profile/:accountName", + sessionProtection, + userController.GetUserProfile +); + +router.post("/username", sessionProtection, userController.ChangeUsername); +router.post("/password", sessionProtection, userController.ChangePassword); + +//router.post("/logout", sessionProtection, userController.Logout); export default router; diff --git a/src/utils/constants.ts b/src/utils/constants.ts index b7a096d..87f2f37 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -9,3 +9,16 @@ export const MONGODB_IGNORED_FIELDS: string = "-password -_id -__v"; // Header name for the session ID export const HEADER_X_AUTHORIZATION: string = "x-authorization"; + +export const USERNAME_MIN_LENGTH: number = 3; +export const USERNAME_MAX_LENGTH: number = 20; +export const USERNAME_REGEX: RegExp = /^[a-zA-Z0-9_]+$/; // Alphanumeric and underscore +export const PASSWORD_MIN_LENGTH: number = 8; +export const PASSWORD_MAX_LENGTH: number = 64; +export const PASSWORD_REGEX: RegExp = + /^([A-Za-z0-9+/]{4})*([A-Za-z0-9+/]{4}|[A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{2}==)$/; +export const ACCOUNT_NAME_MIN_LENGTH: number = 3; +export const ACCOUNT_NAME_MAX_LENGTH: number = 20; + +// alphanumerics, numbers, underscores, and dashes +export const ACCOUNT_NAME_REGEX: RegExp = /^[a-zA-Z0-9_-]+$/; diff --git a/src/utils/utils.ts b/src/utils/utils.ts index 09fff9c..08aea41 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -1,6 +1,7 @@ import crypto from "crypto"; import { Session } from "../models/session"; import { Response } from "express"; +import bcrypt from "bcrypt"; export async function saveSession(res: Response, accountName: string) { try { @@ -23,3 +24,15 @@ export async function saveSession(res: Response, accountName: string) { res.status(500).json({ status: "err" }); } } + +export async function matchPassword(decodedPassword: string, password: string) { + return await bcrypt.compare(decodedPassword, password); +} + +export async function hashPassword(password: string) { + return await bcrypt.hash(password, 10); +} + +export function decodeBase64(value: string) { + return Buffer.from(value, "base64").toString("utf-8"); +} diff --git a/src/validation/validation.ts b/src/validation/validation.ts new file mode 100644 index 0000000..f4268a7 --- /dev/null +++ b/src/validation/validation.ts @@ -0,0 +1,56 @@ +import { + USERNAME_MAX_LENGTH, + USERNAME_MIN_LENGTH, + USERNAME_REGEX, + PASSWORD_MAX_LENGTH, + PASSWORD_MIN_LENGTH, + PASSWORD_REGEX, +} from "../utils/constants"; + +export function isUsernameValid(username: string) { + if (!username) { + return false; + } + + if (!USERNAME_REGEX.test(username)) { + return false; + } + + if (username.length > USERNAME_MAX_LENGTH) { + return false; + } + + if (username.length < USERNAME_MIN_LENGTH) { + return false; + } + + return true; +} + +export function isPasswordValid(password: string) { + if (!password) { + return false; + } + + if (!PASSWORD_REGEX.test(password)) { + return false; + } + + if (password.length > PASSWORD_MAX_LENGTH) { + return false; + } + + if (password.length < PASSWORD_MIN_LENGTH) { + return false; + } + + return true; +} + +export function isAccountNameValid(accountName: string) { + if (!accountName) { + return false; + } + + return true; +}