Skip to content

Commit

Permalink
add new routes to user
Browse files Browse the repository at this point in the history
  • Loading branch information
mariovyord committed Dec 15, 2023
1 parent b876fb9 commit ca4ca6f
Show file tree
Hide file tree
Showing 11 changed files with 218 additions and 119 deletions.
5 changes: 5 additions & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"singleQuote": false,
"bracketSpacing": true,
"printWidth": 120
}
33 changes: 9 additions & 24 deletions src/features/article/article-repository.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,8 @@
import Article from "./article-schema";
import {
ArticleEntity,
ICreateArticleData,
IPatchArticleData,
} from "./article-types";
import { ArticleEntity, ICreateArticleData, IPatchArticleData } from "./article-types";
import { IParsedQuery } from "./article-utils";

export async function findArticlesByQuery(
parsedQuery: IParsedQuery
): Promise<ArticleEntity[]> {
export async function findArticlesByQuery(parsedQuery: IParsedQuery): Promise<ArticleEntity[]> {
const articles = await Article.find(parsedQuery.find)
.sort(parsedQuery.sort)
.limit(parsedQuery.pagination.limit)
Expand All @@ -23,25 +17,17 @@ export async function findArticlesByQuery(
return articles.map((x) => new ArticleEntity(x));
}

export async function countDocumentsByQuery(
parsedQuery: IParsedQuery
): Promise<number> {
export async function countDocumentsByQuery(parsedQuery: IParsedQuery): Promise<number> {
if (parsedQuery.find) {
return Article.find(parsedQuery.find).countDocuments();
}

return Article.find().estimatedDocumentCount();
}

export async function findArticleById(
id: string,
populateField?: any
): Promise<ArticleEntity | null> {
export async function findArticleById(id: string, populateField?: any): Promise<ArticleEntity | null> {
// TODO: Make it more sensible
const article = await Article.findById(id).populate(
populateField.populate,
populateField.limitPopulate
);
const article = await Article.findById(id).populate(populateField.populate, populateField.limitPopulate);

if (!article) {
return null;
Expand All @@ -50,9 +36,7 @@ export async function findArticleById(
return new ArticleEntity(article);
}

export async function createArticle(
data: ICreateArticleData
): Promise<ArticleEntity> {
export async function createArticle(data: ICreateArticleData): Promise<ArticleEntity> {
const result = new Article(data);
await result.save();

Expand All @@ -67,14 +51,15 @@ export async function findAndUpdateArticleById(
const article = await Article.findById(id);

if (article === null) throw new Error("Article not found");
if (article.owner.toString() !== userId)
throw new Error("Only owners can update article");
if (article.owner.toString() !== userId) throw new Error("Only owners can update article");

for (const key of Object.keys(data)) {
//@ts-ignore
article[key] = data[key];
}

article.updatedAt = new Date();

await article.save();

return new ArticleEntity(article);
Expand Down
104 changes: 79 additions & 25 deletions src/features/user/user-handlers.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { IJsonResponse } from "../../utils/json-response";
import * as authService from "./user-service";
import * as userService from "./user-service";
import { NextFunction, Request, Response } from "express";
import { AppError } from "../../utils/app-error";
import { HttpStatusCode } from "../../utils/http-status-code";
import { IUserLocal, UserEntity } from "./user-types";

const authCookieName = "jwt";
const authCookieOptions = {
Expand All @@ -13,41 +14,27 @@ const authCookieOptions = {
};

export function signUp() {
return async (
req: Request,
res: Response<IJsonResponse>,
next: NextFunction
) => {
return async (req: Request, res: Response<IJsonResponse>, next: NextFunction) => {
try {
const userData = req.body;
const [token, user] = await authService.signUp(userData);

return res
.cookie(authCookieName, token, authCookieOptions)
.status(HttpStatusCode.CREATED)
.json({
code: HttpStatusCode.CREATED,
message: "Sign up successful",
data: user,
});
const [token, user] = await userService.signUp(userData);

return res.cookie(authCookieName, token, authCookieOptions).status(HttpStatusCode.CREATED).json({
code: HttpStatusCode.CREATED,
message: "Sign up successful",
data: user,
});
} catch (err) {
next(new AppError(HttpStatusCode.BAD_REQUEST, "Sign up failed"));
}
};
}

export function signIn() {
return async (
req: Request,
res: Response<IJsonResponse>,
next: NextFunction
) => {
return async (req: Request, res: Response<IJsonResponse>, next: NextFunction) => {
try {
const userData = req.body;
const [token, user] = await authService.signIn(
userData.username,
userData.password
);
const [token, user] = await userService.signIn(userData.username, userData.password);

return res.cookie(authCookieName, token, authCookieOptions).json({
code: HttpStatusCode.OK,
Expand All @@ -69,3 +56,70 @@ export function signOut() {
});
};
}

export function getUserData() {
return async (req: Request, res: Response<IJsonResponse>, next: NextFunction) => {
try {
const user = res.locals.user as IUserLocal;

const userData = await userService.getUser(user);

return res.json({
code: HttpStatusCode.OK,
message: "User fetched successfully",
data: userData,
});
} catch (err) {
next(new AppError(HttpStatusCode.BAD_REQUEST, "Failed to fetch user data"));
}
};
}

export function updateUser() {
return async (req: Request, res: Response<IJsonResponse>, next: NextFunction) => {
try {
const requestUserId = req.params?.id;
const user = res.locals.user as IUserLocal;

if (user.id !== requestUserId) {
throw new Error("Only owners can update user data");
}

const userData = req.body;
const updatedUser: UserEntity = await userService.updateUser(user.id, userData);

return res.json({
code: HttpStatusCode.OK,
message: "User updated successfully",
data: updatedUser,
});
} catch (err) {
next(new AppError(HttpStatusCode.BAD_REQUEST, "Failed to patch user data"));
}
};
}

export function updatePassword() {
return async (req: Request, res: Response<IJsonResponse>, next: NextFunction) => {
try {
const newPassword = req.body.newPassword;
const oldPassword = req.body.oldPassword;
const user = res.locals.user;
const requestedUserId = req.params.id;

if (user.id !== requestedUserId) {
throw new Error("Invalid user");
}

const updatedUser = await userService.updatePassword(user.id, oldPassword, newPassword);

return res.json({
code: HttpStatusCode.OK,
message: "Password updated successfully",
data: updatedUser,
});
} catch (err) {
next(new AppError(HttpStatusCode.BAD_REQUEST, "Failed to update password"));
}
};
}
28 changes: 13 additions & 15 deletions src/features/user/user-repository.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import User from "./user-schema";
import { SignUpUserData, UserEntity } from "./user-types";
import { ISignUpUserData, UserEntity } from "./user-types";

export async function findOneByUsername(
username: string | undefined
): Promise<UserEntity | null> {
export async function findOneByUsername(username: string | undefined) {
if (!username) return null;

const existing = await User.findOne({
Expand All @@ -12,13 +10,16 @@ export async function findOneByUsername(

if (!existing) return null;

return new UserEntity(existing);
return existing;
}

export async function findOneByPassword(
username: string | undefined,
password: string | undefined
): Promise<UserEntity | null> {
export async function findOneById(id: string) {
const existing = await User.findById(id);
if (!existing) return null;
return existing;
}

export async function findOneByPassword(username: string | undefined, password: string | undefined) {
if (!username || !password) return null;

const existing = await User.findOne({
Expand All @@ -31,14 +32,11 @@ export async function findOneByPassword(

if (!matching) return null;

return new UserEntity(existing);
return existing;
}

export async function createUser(
userData: SignUpUserData
): Promise<UserEntity> {
export async function createUser(userData: ISignUpUserData) {
const user = new User(userData);
await user.save();

return new UserEntity(user);
return user;
}
5 changes: 4 additions & 1 deletion src/features/user/user-router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import express, { Request, Response } from "express";
import * as userHandlers from "./user-handlers";
import authenticateToken from "../../middleware/auth-middleware";
import { validateBody } from "../../middleware/validator-middleware";
import { userSignInSchema, userSignUpSchema } from "./user-validators";
import { userPasswordSchema, userSignInSchema, userSignUpSchema, userUpdateSchema } from "./user-validators";
import { IJsonResponse } from "../../utils/json-response";
import { HttpStatusCode } from "../../utils/http-status-code";

Expand All @@ -19,5 +19,8 @@ router.all("/", (req: Request, res: Response<IJsonResponse>) => {
router.post("/signup", validateBody(userSignUpSchema), userHandlers.signUp());
router.post("/signin", validateBody(userSignInSchema), userHandlers.signIn());
router.delete("/signout", authenticateToken(), userHandlers.signOut());
router.get("/:id", authenticateToken(), userHandlers.getUserData());
router.patch("/:id", authenticateToken(), validateBody(userUpdateSchema), userHandlers.updateUser());
router.patch("/:id/password", authenticateToken(), validateBody(userPasswordSchema), userHandlers.updatePassword());

export default router;
4 changes: 1 addition & 3 deletions src/features/user/user-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,7 @@ userSchema.pre("save", function (next) {
userSchema.pre("save", async function (next) {
if (this.isModified("password")) {
if (!noBlacklistedChars(this.password))
throw new Error(
"Password should not contain whitespace or special symbols"
);
throw new Error("Password should not contain whitespace or special symbols");

this.password = await bcrypt.hash(this.password, 10);
}
Expand Down
66 changes: 56 additions & 10 deletions src/features/user/user-service.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
import jwt from "jsonwebtoken";
import getConfig from "../../config/get-config";
import { SignUpUserData, UserEntity } from "./user-types";
import { IUserLocal, ISignUpUserData, UserEntity, IUpdateUserData } from "./user-types";
import * as userRepository from "./user-repository";

export async function signUp(
userData: SignUpUserData
): Promise<[string, UserEntity]> {
export async function signUp(userData: ISignUpUserData): Promise<[string, UserEntity]> {
const existing = await userRepository.findOneByUsername(userData.username);

if (existing) {
Expand All @@ -15,13 +13,10 @@ export async function signUp(
const user = await userRepository.createUser(userData);
const token = await createToken(user.id);

return [token, user];
return [token, new UserEntity(user)];
}

export async function signIn(
username: string,
password: string
): Promise<[string, UserEntity]> {
export async function signIn(username: string, password: string): Promise<[string, UserEntity]> {
const user = await userRepository.findOneByPassword(username, password);

if (!user) {
Expand All @@ -30,7 +25,7 @@ export async function signIn(

const token = await createToken(user.id);

return [token, user];
return [token, new UserEntity(user)];
}

async function createToken(id: string) {
Expand All @@ -44,3 +39,54 @@ async function createToken(id: string) {
expiresIn: "7d",
});
}

export async function getUser(userData: IUserLocal): Promise<UserEntity> {
const user = await userRepository.findOneById(userData.id);

if (!user) {
throw new Error("Not found");
}

return new UserEntity(user);
}

const ALLOWED_UPDATE_FIELDS = ["firstName", "lastName"];

export async function updateUser(userId: string, userData: Partial<IUpdateUserData>): Promise<UserEntity> {
const user = await userRepository.findOneById(userId);

if (user === null || user._id.toString() !== userId) {
throw new Error("Failed to update user");
}

for (const key of ALLOWED_UPDATE_FIELDS) {
if (userData[key]) {
user[key] = userData[key];
}
}

// mongoose will auto-update updatedAt field
await user.save();

return new UserEntity(user);
}

export async function updatePassword(userId: string, oldPassword: string, newPassword: string) {
const user = await userRepository.findOneById(userId);

if (!user) {
throw new Error("User does not exist");
}

const match = await user.comparePassword(oldPassword);

if (!match) {
throw new Error("Incorrect password");
}

user.password = newPassword;

await user.save();

return user;
}
Loading

0 comments on commit ca4ca6f

Please sign in to comment.