Commit 4b9863f9 by Farhaan Khan

drive- backend - authentication adn oAuth completion

parent 35387e68
'use strict';
/** @type {import('sequelize-cli').Migration} */
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.createTable('users', {
id: {
type: Sequelize.INTEGER,
allowNull: false,
primaryKey: true,
autoIncrement: true,
},
email: {
type: Sequelize.STRING,
allowNull: false,
unique: true,
},
password: {
type: Sequelize.STRING,
allowNull: false,
},
role: {
type: Sequelize.ENUM('USER', 'ADMIN'),
defaultValue: 'USER',
},
createdAt: {
type: Sequelize.DATE,
defaultValue: Sequelize.NOW,
},
updatedAt: {
type: Sequelize.DATE,
defaultValue: Sequelize.NOW,
},
});
},
async down(queryInterface, Sequelize) {
await queryInterface.dropTable('users');
},
};
import { sequelize } from '../utils/database';
import { DataTypes, Model, Optional } from 'sequelize';
interface UserAttributes {
id: number;
email: string;
password: string;
role: string;
}
interface UserCreationAttributes extends Optional<UserAttributes, 'id'> {}
class User extends Model<UserAttributes, UserCreationAttributes> implements UserAttributes {
declare id: number;
declare email: string;
declare password: string;
declare role: string;
}
User.init(
{
id: {
type: DataTypes.INTEGER,
autoIncrement: true,
primaryKey: true,
},
email: {
type: DataTypes.STRING,
allowNull: false,
unique: true,
},
password: {
type: DataTypes.STRING,
allowNull: false,
},
role: {
type: DataTypes.STRING,
allowNull: false,
defaultValue: 'user',
},
},
{
sequelize,
modelName: 'User',
tableName: 'users',
timestamps: true,
}
);
export { User };
'use strict';
const fs = require('fs');
const path = require('path');
const Sequelize = require('sequelize');
const process = require('process');
const basename = path.basename(__filename);
const env = process.env.NODE_ENV || 'development';
const config = require(__dirname + '/../config/config.json')[env];
const db = {};
let sequelize;
if (config.use_env_variable) {
sequelize = new Sequelize(process.env[config.use_env_variable], config);
} else {
sequelize = new Sequelize(config.database, config.username, config.password, config);
}
fs
.readdirSync(__dirname)
.filter(file => {
return (
file.indexOf('.') !== 0 &&
file !== basename &&
file.slice(-3) === '.js' &&
file.indexOf('.test.js') === -1
);
})
.forEach(file => {
const model = require(path.join(__dirname, file))(sequelize, Sequelize.DataTypes);
db[model.name] = model;
});
Object.keys(db).forEach(modelName => {
if (db[modelName].associate) {
db[modelName].associate(db);
}
});
db.sequelize = sequelize;
db.Sequelize = Sequelize;
module.exports = db;
This source diff could not be displayed because it is too large. You can view the blob instead.
......@@ -11,17 +11,33 @@
"license": "ISC",
"description": "",
"devDependencies": {
"@types/bcrypt": "^5.0.2",
"@types/express-validator": "^2.20.33",
"@types/jsonwebtoken": "^9.0.9",
"@types/node": "^22.15.3",
"@types/sequelize": "^4.28.20",
"sequelize-cli": "^6.6.3",
"ts-node-dev": "^2.0.0",
"typescript": "^5.8.3"
},
"dependencies": {
"@fastify/auth": "^5.0.2",
"@fastify/cors": "^11.0.1",
"@types/bcryptjs": "^2.4.6",
"bcrypt": "^5.1.1",
"bcryptjs": "^3.0.2",
"dotenv": "^16.5.0",
"express-validator": "^7.2.1",
"fastify": "^5.3.2",
"fastify-cors": "^6.0.3",
"fastify-plugin": "^5.0.1",
"google-auth-library": "^9.15.1",
"googleapis": "^148.0.0",
"jsonwebtoken": "^9.0.2",
"mysql2": "^3.14.1",
"sequelize": "^6.37.7",
"sequelize-typescript": "^2.1.6",
"sqlite3": "^5.1.7",
"zod": "^3.24.4"
}
}
import { FastifyRequest, FastifyReply } from 'fastify';
import { User } from '../../models/User';
import bcrypt from 'bcryptjs';
import jwt from 'jsonwebtoken';
const JWT_SECRET = process.env.JWT_SECRET || 'yourSuperSecretKey';
const loginSchema = {
type: 'object',
properties: {
email: { type: 'string' },
password: { type: 'string' },
},
required: ['email', 'password'],
};
export const loginRouteOptions = {
schema: {
body: loginSchema,
},
};
// Login function
export const login = async (req: FastifyRequest, reply: FastifyReply) => {
const { email, password } = req.body as { email: string; password: string };
try {
console.log('Attempting login for:', email);
const user = await User.findOne({ where: { email } });
if (!user) {
console.log('User not found');
return reply.code(401).send({ message: 'Invalid credentials' });
}
console.log('User found:', user.email);
// Add this check to ensure user.password exists
if (!user.password) {
console.log('No password set for user');
return reply.code(401).send({ message: 'Invalid credentials' });
}
console.log('Comparing passwords...');
const isMatch = await bcrypt.compare(password, user.password);
console.log('Password match:', isMatch);
if (!isMatch) {
return reply.code(401).send({ message: 'Invalid credentials' });
}
const token = jwt.sign(
{ id: user.id, email: user.email, role: user.role },
JWT_SECRET,
{ expiresIn: '1h' }
);
return reply.send({
message: 'Login successful',
token,
user: {
id: user.id,
email: user.email,
role: user.role
}
});
} catch (error) {
console.error('Login error:', error);
return reply.code(500).send({
message: 'Internal server error',
error: error instanceof Error ? error.message : 'Unknown error'
});
}
};
// Register function
interface RegisterRequestBody {
email: string;
password: string;
role?: string;
}
export const register = async (
req: FastifyRequest<{ Body: RegisterRequestBody }>,
reply: FastifyReply
) => {
const { email, password, role } = req.body;
try {
const existingUser = await User.findOne({ where: { email } });
if (existingUser) {
return reply.status(400).send({ message: 'Email is already registered' });
}
const hashedPassword = await bcrypt.hash(password, 10);
const newUser = await User.create({
email,
password: hashedPassword,
role: role || 'user',
});
return reply.status(201).send({ message: 'User registered successfully', userId: newUser.id });
} catch (error) {
console.error('Register error:', error);
return reply.status(500).send({ message: 'Internal server error' });
}
};
\ No newline at end of file
import { FastifyRequest, FastifyReply } from 'fastify';
import { User } from '../../models/User';
interface AuthenticatedRequest extends FastifyRequest {
user: {
id: number;
email: string;
role: string;
};
}
export const getProfileHandler = async (
request: AuthenticatedRequest,
reply: FastifyReply
) => {
try {
const user = await User.findByPk((request as AuthenticatedRequest).user.id, {
attributes: ['id', 'name', 'email', 'role', 'createdAt', 'updatedAt'],
});
if (!user) {
return reply.status(404).send({ error: 'User not found' });
}
return reply.send({ user });
} catch (err) {
console.error(err);
return reply.status(500).send({ error: 'Failed to fetch profile' });
}
};
import Fastify from 'fastify';
import db from './plugins/db';
import userRoutes from './routes/user';
import cors from '@fastify/cors';
import { authRoutes } from './routes/auth.routes';
import { profileRoutes } from './routes/profile';
import { googleAuthRoutes } from './routes/auth/google';
const app = Fastify();
app.register(db);
app.register(userRoutes, { prefix: '/api/users' });
app.get('/', async (request, reply) => {
return { message: 'Fastify server is running!' };
app.register(cors, {
origin: 'http://localhost:3000',
credentials: true,
});
app.register(authRoutes);
app.register(profileRoutes);
app.register(googleAuthRoutes, { prefix: '/api' });
app.listen({ port: 3001 }, (err) => {
if (err) throw err;
console.log('Server running on http://localhost:3001');
});
app.listen({ port: 3001 }, (err, address) => {
if (err) {
console.error(err);
process.exit(1);
}
console.log(`Server ready at ${address}`);
});
import { FastifyRequest, FastifyReply } from 'fastify';
import jwt from 'jsonwebtoken';
export const verifyToken = async (request: FastifyRequest, reply: FastifyReply) => {
try {
const authHeader = request.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return reply.status(401).send({ error: 'Missing or invalid token' });
}
const token = authHeader.split(' ')[1];
const decoded = jwt.verify(token, process.env.JWT_SECRET || 'default-secret') as any;
(request as any).user = {
id: decoded.id,
email: decoded.email,
role: decoded.role,
};
} catch (err) {
return reply.status(401).send({ error: 'Unauthorized' });
}
};
\ No newline at end of file
import { Table, Column, Model, DataType } from 'sequelize-typescript';
@Table
export class User extends Model {
@Column({
type: DataType.STRING,
allowNull: false,
})
name!: string;
@Column({
type: DataType.STRING,
allowNull: false,
unique: true,
})
email!: string;
}
import { login, loginRouteOptions, register } from '../controllers/auth.controller';
import { FastifyInstance } from 'fastify';
import { verifyToken } from '../middlewares/authenticate';
export const authRoutes = async (fastify: FastifyInstance) => {
fastify.post('/login', { ...loginRouteOptions }, login);
fastify.post('/register', register);
};
// src/routes/auth/google.ts
import { FastifyInstance, FastifyRequest } from 'fastify';
import { OAuth2Client } from 'google-auth-library';
import { google } from 'googleapis';
import dotenv from 'dotenv';
dotenv.config();
interface GoogleCallbackQuery {
code: string;
}
export async function googleAuthRoutes(fastify: FastifyInstance) {
const client = new OAuth2Client(
process.env.GOOGLE_CLIENT_ID!,
process.env.GOOGLE_CLIENT_SECRET!,
process.env.GOOGLE_REDIRECT_URI!
);
fastify.get('/auth/google', async (_request, reply) => {
const url = client.generateAuthUrl({
access_type: 'offline',
scope: ['profile', 'email'],
prompt: 'consent',
});
reply.redirect(url);
});
fastify.get('/auth/google/callback', async (
request: FastifyRequest<{ Querystring: GoogleCallbackQuery }>,
reply
) => {
const code = request.query.code;
const { tokens } = await client.getToken(code);
client.setCredentials(tokens);
const oauth2 = google.oauth2({
auth: client,
version: 'v2',
});
const userInfoResponse = await oauth2.userinfo.get();
const userInfo = userInfoResponse.data;
const query = new URLSearchParams({
name: userInfo.name ?? '',
email: userInfo.email ?? '',
});
reply.redirect(`http://localhost:3000/profile?${query.toString()}`);
});
}
import { FastifyInstance } from 'fastify';
import { verifyToken } from '../middlewares/authenticate';
export async function profileRoutes(fastify: FastifyInstance) {
fastify.get('/profile', { preHandler: verifyToken }, async (request, reply) => {
const user = (request as any).user;
return { user };
});
}
\ No newline at end of file
import { FastifyInstance } from 'fastify';
import { verifyToken } from '../middlewares/authenticate';
export async function userRoutes(fastify: FastifyInstance) {
fastify.get('/profile', { preHandler: [verifyToken] }, async (request, reply) => {
const user = (request as any).user;
return { user };
});
}
import { FastifyPluginAsync } from 'fastify';
import { z } from 'zod';
import { User } from '../models/User';
const userSchema = z.object({
name: z.string(),
email: z.string().email()
});
const userRoutes: FastifyPluginAsync = async (fastify) => {
fastify.post('/', async (req, res) => {
const parsed = userSchema.safeParse(req.body);
if (!parsed.success) {
return res.status(400).send(parsed.error);
}
const user = await User.create(parsed.data);
res.send(user);
});
};
export default userRoutes;
import 'fastify';
declare module 'fastify' {
interface FastifyRequest {
user: {
id: number;
email: string;
role: string;
};
}
}
{
"compilerOptions": {
"target": "ES2022", // Or ES2020+, avoid ESNext for now
"target": "ES2022",
"module": "CommonJS",
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"strict": true,
"moduleResolution": "node",
"esModuleInterop": true,
"skipLibCheck": true
}
}
"skipLibCheck": true,
"typeRoots": ["./src/types", "./node_modules/@types"]
},
"include": ["src", "src/index.ts"]
}
\ No newline at end of file
import { Sequelize } from 'sequelize';
import dotenv from 'dotenv';
dotenv.config();
const { DB_HOST, DB_USER, DB_PASSWORD, DB_NAME, DB_DIALECT, DB_PORT } = process.env;
export const sequelize = new Sequelize(DB_NAME as string, DB_USER as string, DB_PASSWORD as string, {
host: DB_HOST,
dialect: DB_DIALECT as 'mysql',
port: Number(DB_PORT),
logging: false,
});
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment