Skip to content
Open
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
19 changes: 19 additions & 0 deletions .eslintrc.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
module.exports = {
extends: [
'@mate-academy/eslint-config',
'plugin:@typescript-eslint/recommended',
],
parser: '@typescript-eslint/parser',
plugins: ['jest', '@typescript-eslint'],
env: {
jest: true,
node: true,
},
ignorePatterns: ['dist/', 'node_modules/', 'src/generated/'],
rules: {
'no-console': 'off',
'no-shadow': 'off',
indent: 'off',
'no-proto': 0,
},
};
10 changes: 0 additions & 10 deletions .eslintrc.js

This file was deleted.

5 changes: 2 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,5 @@ node_modules
# MacOS
.DS_Store

# env files
*.env
.env*
.env
src/generated
32 changes: 29 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@
"version": "1.0.0",
"description": "Auth app",
"main": "src/index.js",
"type": "module",
"scripts": {
"dev": "tsx watch src/index.ts",
"build": "tsc",
"start": "node dist/index.js",
"init": "mate-scripts init",
"start": "node src/index.js",
"lint": "npm run format && mate-scripts lint",
"format": "prettier --ignore-path .prettierignore --write './src/**/*.{js,ts}'",
"test:only": "mate-scripts test",
Expand All @@ -17,14 +20,37 @@
"license": "GPL-3.0",
"devDependencies": {
"@mate-academy/eslint-config": "latest",
"@mate-academy/scripts": "^1.8.6",
"@mate-academy/scripts": "^2.1.3",
"@types/bcrypt": "^6.0.0",
"@types/cors": "^2.8.19",
"@types/express": "^5.0.6",
"@types/jsonwebtoken": "^9.0.10",
"@types/node": "^25.6.2",
"@types/nodemailer": "^8.0.0",
"@types/pg": "^8.20.0",
"@typescript-eslint/eslint-plugin": "^7.18.0",
"@typescript-eslint/parser": "^7.18.0",
"eslint": "^8.57.0",
"eslint-plugin-jest": "^28.6.0",
"eslint-plugin-node": "^11.1.0",
"jest": "^29.7.0",
"prettier": "^3.3.2"
"prettier": "3.5.3",
"prisma": "^7.8.0",
"tsx": "^4.21.0",
"typescript": "^6.0.3"
},
"mateAcademy": {
"projectType": "javascript"
},
"dependencies": {
"@prisma/adapter-pg": "^7.8.0",
"@prisma/client": "^7.8.0",
"bcrypt": "^6.0.0",
"cors": "^2.8.6",
"dotenv": "^17.4.2",
"express": "^5.2.1",
"jsonwebtoken": "^9.0.3",
"nodemailer": "^8.0.7",
"pg": "^8.20.0"
}
}
12 changes: 12 additions & 0 deletions src/errors/AppError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export class AppError extends Error {
statusCode: number;
details?: unknown;

constructor(statusCode: number, message: string, details?: unknown) {
super(message);

this.name = 'AppError';
this.statusCode = statusCode;
this.details = details;
}
}
1 change: 0 additions & 1 deletion src/index.js

This file was deleted.

38 changes: 38 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import express from 'express';
import cors from 'cors';
import dotenv from 'dotenv';
import { errorMiddleware } from './middlewares/error.middleware.js';
import { AppError } from './errors/AppError.js';
import { authRoutes } from './modules/auth/auth.routes.js';
import { profileRoutes } from './modules/profile/profile.routes.js';
import cookieParser from 'cookie-parser';
import { pagesRoutes } from './modules/pages/pages.routes.js';

dotenv.config();

const app = express();

const PORT = process.env.PORT ?? 3000;

app.use(cors());
Comment on lines +15 to +17

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The task requires 'redirect to Profile after activation', but this returns JSON. Should use res.redirect('/profile') or serve an HTML activation page.

app.use(express.json());

app.use(express.urlencoded({ extended: true }));
app.use(cookieParser());

app.use(pagesRoutes);

//
app.use('/api/auth', authRoutes);
app.use('/api/profile', profileRoutes);
//

app.use((_req, _res, next) => {
next(new AppError(404, 'Route not found'));
});

app.use(errorMiddleware);
Comment on lines +32 to +34

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The task requires 'redirect to Login after logout', but this returns 204 No Content. Should use res.redirect('/auth/login').


app.listen(PORT, () => {
console.log(`Server is running on http://localhost:${PORT}`);
});
12 changes: 12 additions & 0 deletions src/lib/prisma.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import 'dotenv/config';

import { PrismaClient } from '../generated/prisma/client.js';
import { PrismaPg } from '@prisma/adapter-pg';

const adapter = new PrismaPg({
connectionString: process.env.DATABASE_URL!,
});

export const prisma = new PrismaClient({
adapter,
});
15 changes: 15 additions & 0 deletions src/middlewares/asyncHandler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import type { RequestHandler } from 'express';
import type { ParamsDictionary, Query } from 'express-serve-static-core';

export const asyncHandler = <
Params extends ParamsDictionary = ParamsDictionary,
ResBody = unknown,
ReqBody = unknown,
ReqQuery extends Query = Query,
>(
handler: RequestHandler<Params, ResBody, ReqBody, ReqQuery>,
): RequestHandler<Params, ResBody, ReqBody, ReqQuery> => {
return (req, res, next) => {
Promise.resolve(handler(req, res, next)).catch(next);
};
};
25 changes: 25 additions & 0 deletions src/middlewares/auth.middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import type { RequestHandler } from 'express';
import { AppError } from '../errors/AppError.js';
import { validateAccessToken } from '../utils/jwt.js';

export const authMiddleware: RequestHandler = (req, _res, next) => {
const authHeader = req.headers.authorization;
const [type, headerToken] = authHeader?.split(' ') || [];

const token =
type === 'Bearer' && headerToken ? headerToken : req.cookies?.accessToken;

if (!token) {
throw new AppError(401, 'Unauthorized');
}

const userData = validateAccessToken(token);

if (!userData) {
throw new AppError(401, 'Invalid or expired token');
}

req.user = userData;

next();
};
21 changes: 21 additions & 0 deletions src/middlewares/error.middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import type { ErrorRequestHandler } from 'express';
import { AppError } from '../errors/AppError.js';

export const errorMiddleware: ErrorRequestHandler = (error, _req, res) => {
if (error instanceof AppError) {
return res.status(error.statusCode).json({
error: {
message: error.message,
details: error.details,
},
});
}

console.error(error);

return res.status(500).json({
error: {
message: 'Internal Server Error',
},
});
};
64 changes: 64 additions & 0 deletions src/modules/auth/auth.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import type { RequestHandler } from 'express';
import { authService } from './auth.service.js';

type ActivateParams = {
token: string;
};

type ResetPasswordParams = {
token: string;
};

const register: RequestHandler = async (req, res) => {
const user = await authService.register(req.body);

res.status(201).json({ user });
};

const activate: RequestHandler<ActivateParams> = async (req, res) => {
const { token } = req.params;

const user = await authService.activate(token);

res.json({
user,
message: 'Account activated successfully',
});
Comment on lines +17 to +26

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The task requires 'redirect to Profile after activation'. This returns JSON instead of redirecting. Should use res.redirect('/profile') or serve an HTML activation success page.

};

const login: RequestHandler = async (req, res) => {
const authData = await authService.login(req.body);

res.json(authData);
Comment on lines +28 to +32

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The task requires 'Redirect to profile after login'. This returns JSON instead of redirecting. Should use res.redirect('/profile').

};

const logout: RequestHandler = async (req, res) => {
res.status(204).send();
Comment on lines +34 to +36

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The task requires 'Redirect to login after logging out'. Returning 204 No Content doesn't satisfy this. Should use res.redirect('/auth/login') or res.redirect('/login').

};

const forgotPassword: RequestHandler = async (req, res) => {
await authService.forgotPassword(req.body);

res.json({
message: 'If an account exists, a password reset link has been sent',
});
Comment on lines +38 to +44

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The task requires 'Show email sent page' after forgot password. Returning JSON doesn't satisfy this. Should serve an HTML page or redirect to a confirmation page.

};

const resetPassword: RequestHandler<ResetPasswordParams> = async (req, res) => {
const { token } = req.params;

await authService.resetPassword(token, req.body);

res.json({
message: 'Password has been reset successfully',
});
Comment on lines +46 to +54

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The task requires 'Show Success page with a link to login' after password reset. Returning JSON doesn't satisfy this. Should serve an HTML page with a link to the login page.

};

export const authController = {
register,
activate,
login,
logout,
forgotPassword,
resetPassword,
};
21 changes: 21 additions & 0 deletions src/modules/auth/auth.routes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { Router } from 'express';
import { authController } from './auth.controller.js';
import { asyncHandler } from '../../middlewares/asyncHandler.js';
import { authMiddleware } from '../../middlewares/auth.middleware.js';

export const authRoutes = Router();

authRoutes.post('/register', asyncHandler(authController.register));
authRoutes.get('/activate/:token', asyncHandler(authController.activate));
authRoutes.post('/login', asyncHandler(authController.login));
authRoutes.post('/logout', authMiddleware, authController.logout);

authRoutes.post(
'/forgot-password',
asyncHandler(authController.forgotPassword),
);

authRoutes.post(
'/reset-password/:token',
asyncHandler(authController.resetPassword),
);
Loading
Loading