Step‐by‐Step Implementation Guide - Wiz-DevTech/prettygirllz GitHub Wiki
I've analyzed your repository code for the Identity & Access Module. Based on your directory structure and the code snippets you've shared, I'll guide you through implementing this module step by step with PostgreSQL integration.
Step-by-Step Implementation Guide
1. Set Up Project Structure
First, let's initialize the project with the necessary directory structure and configuration files:
mkdir -p identity-access/.github/workflows
mkdir -p identity-access/config
mkdir -p identity-access/core/auth/strategies
mkdir -p identity-access/core/authz
mkdir -p identity-access/core/token
mkdir -p identity-access/adapters/http
mkdir -p identity-access/adapters/grpc
mkdir -p identity-access/adapters/database/models
mkdir -p identity-access/lib
2. Configure PostgreSQL Connection
Let's implement the database configuration in config/database.js
:
// config/database.js
require('dotenv').config();
module.exports = {
development: {
uri: process.env.POSTGRES_URI,
ssl: false,
pool: {
max: 5,
min: 0,
idle: 10000
}
},
production: {
uri: process.env.POSTGRES_URI,
ssl: true,
pool: {
max: 10,
min: 2,
idle: 10000
}
},
test: {
uri: process.env.TEST_POSTGRES_URI || 'postgresql://postgres:postgres@localhost:5432/testdb',
ssl: false,
pool: {
max: 3,
min: 0,
idle: 10000
}
}
};
3. Create Encryption Layer
Security is crucial, so let's implement field-level encryption:
// lib/encryption.js
const crypto = require('crypto');
class Encryption {
constructor(key) {
this.algorithm = 'aes-256-cbc';
this.key = Buffer.from(key, 'utf8');
if (this.key.length !== 32) {
throw new Error('Encryption key must be 32 characters');
}
}
encrypt(text) {
if (!text) return null;
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv(this.algorithm, this.key, iv);
let encrypted = cipher.update(text, 'utf8', 'hex');
encrypted += cipher.final('hex');
return `${iv.toString('hex')}:${encrypted}`;
}
decrypt(text) {
if (!text) return null;
const [ivHex, encryptedText] = text.split(':');
if (!ivHex || !encryptedText) return null;
try {
const iv = Buffer.from(ivHex, 'hex');
const decipher = crypto.createDecipheriv(this.algorithm, this.key, iv);
let decrypted = decipher.update(encryptedText, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
} catch (err) {
console.error('Decryption error:', err.message);
return null;
}
}
}
module.exports = (key) => new Encryption(key);
4. Set Up PostgreSQL Models
Now, let's create the Sequelize models for our user data:
// adapters/database/models/user.js
const { DataTypes, Model } = require('sequelize');
module.exports = (sequelize, encryption) => {
class User extends Model {
// Class methods
static associate(models) {
// Define associations here
}
}
User.init({
id: {
type: DataTypes.INTEGER,
autoIncrement: true,
primaryKey: true
},
email: {
type: DataTypes.TEXT,
allowNull: false,
unique: true,
set(value) {
this.setDataValue('email', encryption.encrypt(value));
},
get() {
const encrypted = this.getDataValue('email');
return encrypted ? encryption.decrypt(encrypted) : null;
}
},
password_hash: {
type: DataTypes.TEXT,
allowNull: false
},
sensitive_data: {
type: DataTypes.TEXT,
allowNull: true,
set(value) {
if (value) {
this.setDataValue('sensitive_data', encryption.encrypt(value));
}
},
get() {
const encrypted = this.getDataValue('sensitive_data');
return encrypted ? encryption.decrypt(encrypted) : null;
}
},
roles: {
type: DataTypes.ARRAY(DataTypes.TEXT),
defaultValue: []
},
created_at: {
type: DataTypes.DATE,
defaultValue: DataTypes.NOW
},
updated_at: {
type: DataTypes.DATE,
defaultValue: DataTypes.NOW
}
}, {
sequelize,
modelName: 'User',
tableName: 'users',
timestamps: true,
underscored: true,
hooks: {
beforeCreate: async (user) => {
// Any pre-save hooks can go here
},
beforeUpdate: async (user) => {
user.updated_at = new Date();
}
}
});
return User;
};
5. Implement PostgreSQL Repository
Let's create the repository layer with transaction support:
// adapters/database/user-repository.js
const bcrypt = require('bcrypt');
class UserRepository {
constructor({ sequelize, models, encryption }) {
this.sequelize = sequelize;
this.User = models.User;
this.encryption = encryption;
}
async create(userData) {
const { password, ...rest } = userData;
// Use transactions for data integrity
const transaction = await this.sequelize.transaction();
try {
const passwordHash = await bcrypt.hash(password, 10);
const user = await this.User.create({
...rest,
password_hash: passwordHash
}, { transaction });
await transaction.commit();
return user;
} catch (error) {
await transaction.rollback();
throw error;
}
}
async findByEmail(email) {
// No need to encrypt email manually, the model getter/setter handles it
const user = await this.User.findOne({
where: { email }
});
return user;
}
async findById(id) {
return await this.User.findByPk(id);
}
async update(id, data) {
const transaction = await this.sequelize.transaction();
try {
const user = await this.User.findByPk(id, { transaction });
if (!user) throw new Error('User not found');
await user.update(data, { transaction });
await transaction.commit();
return user;
} catch (error) {
await transaction.rollback();
throw error;
}
}
async verifyPassword(user, password) {
return await bcrypt.compare(password, user.password_hash);
}
}
module.exports = (deps) => new UserRepository(deps);
6. Implement Database Connection
Let's create the database connection module:
// adapters/database/index.js
const { Sequelize } = require('sequelize');
const userModel = require('./models/user');
class Database {
constructor({ config, encryption }) {
const env = process.env.NODE_ENV || 'development';
const dbConfig = config.database[env];
this.sequelize = new Sequelize(dbConfig.uri, {
dialect: 'postgres',
logging: env === 'development' ? console.log : false,
ssl: dbConfig.ssl,
pool: dbConfig.pool,
dialectOptions: dbConfig.ssl ? {
ssl: {
require: true,
rejectUnauthorized: false
}
} : {}
});
// Initialize models
this.models = {
User: userModel(this.sequelize, encryption)
};
}
async connect() {
try {
await this.sequelize.authenticate();
console.log('Database connection established successfully');
return true;
} catch (error) {
console.error('Unable to connect to database:', error);
throw error;
}
}
async sync() {
// Only sync in development or test
if (process.env.NODE_ENV !== 'production') {
await this.sequelize.sync();
}
}
async close() {
await this.sequelize.close();
}
}
module.exports = (deps) => new Database(deps);
7. Implement JWT Strategy
Now let's create the authentication strategy:
// core/auth/strategies/jwt.js
const jwt = require('jsonwebtoken');
class JwtStrategy {
constructor({ config, userRepository }) {
this.secret = config.jwt.secret;
this.expiresIn = config.jwt.expiresIn || '1h';
this.issuer = config.jwt.issuer || 'identity-access';
this.userRepo = userRepository;
}
async verify(token) {
try {
const decoded = jwt.verify(token, this.secret, {
issuer: this.issuer
});
const user = await this.userRepo.findById(decoded.sub);
if (!user) {
return { isValid: false };
}
return {
isValid: true,
credentials: {
id: user.id,
email: user.email,
roles: user.roles
}
};
} catch (err) {
return { isValid: false };
}
}
generateToken(user) {
const payload = {
sub: user.id,
email: user.email,
roles: user.roles || []
};
return jwt.sign(payload, this.secret, {
expiresIn: this.expiresIn,
issuer: this.issuer
});
}
}
module.exports = (deps) => new JwtStrategy(deps);
8. Create Main Authentication Controller
Let's implement the core authentication logic:
// core/auth/authenticator.js
class Authenticator {
constructor({ userRepository, jwtStrategy }) {
this.userRepo = userRepository;
this.jwtStrategy = jwtStrategy;
}
async authenticate(email, password) {
const user = await this.userRepo.findByEmail(email);
if (!user) {
return { success: false, message: 'Invalid credentials' };
}
const isValid = await this.userRepo.verifyPassword(user, password);
if (!isValid) {
return { success: false, message: 'Invalid credentials' };
}
const token = this.jwtStrategy.generateToken(user);
return {
success: true,
token,
user: {
id: user.id,
email: user.email,
roles: user.roles
}
};
}
async validateToken(token) {
return await this.jwtStrategy.verify(token);
}
}
module.exports = (deps) => new Authenticator(deps);
9. Implement HTTP Routes
Let's create the REST API routes:
// adapters/http/auth-router.js
const express = require('express');
module.exports = ({ authenticator }) => {
const router = express.Router();
// Login endpoint
router.post('/login', async (req, res) => {
try {
const { email, password } = req.body;
if (!email || !password) {
return res.status(400).json({
success: false,
message: 'Email and password are required'
});
}
const result = await authenticator.authenticate(email, password);
if (!result.success) {
return res.status(401).json(result);
}
res.json(result);
} catch (error) {
console.error('Login error:', error);
res.status(500).json({
success: false,
message: 'Internal server error'
});
}
});
// Validate token endpoint
router.post('/verify', async (req, res) => {
try {
const { token } = req.body;
if (!token) {
return res.status(400).json({
success: false,
message: 'Token is required'
});
}
const result = await authenticator.validateToken(token);
res.json({
success: result.isValid,
user: result.isValid ? result.credentials : null
});
} catch (error) {
console.error('Token verification error:', error);
res.status(500).json({
success: false,
message: 'Internal server error'
});
}
});
return router;
};
10. Implement gRPC Service
Let's define the gRPC proto file and service:
// adapters/grpc/auth.proto
syntax = "proto3";
package auth;
service AuthService {
rpc Login (LoginRequest) returns (LoginResponse);
rpc VerifyToken (VerifyTokenRequest) returns (VerifyTokenResponse);
}
message LoginRequest {
string email = 1;
string password = 2;
}
message LoginResponse {
bool success = 1;
string message = 2;
string token = 3;
User user = 4;
}
message User {
int32 id = 1;
string email = 2;
repeated string roles = 3;
}
message VerifyTokenRequest {
string token = 1;
}
message VerifyTokenResponse {
bool isValid = 1;
User user = 2;
}
Now let's implement the gRPC server:
// adapters/grpc/server.js
const grpc = require('@grpc/grpc-js');
const protoLoader = require('@grpc/proto-loader');
const path = require('path');
module.exports = ({ authenticator, config }) => {
const protoPath = path.join(__dirname, 'auth.proto');
const packageDefinition = protoLoader.loadSync(protoPath);
const authProto = grpc.loadPackageDefinition(packageDefinition).auth;
const server = new grpc.Server();
server.addService(authProto.AuthService.service, {
login: async (call, callback) => {
try {
const { email, password } = call.request;
const result = await authenticator.authenticate(email, password);
callback(null, result);
} catch (error) {
console.error('gRPC login error:', error);
callback({
code: grpc.status.INTERNAL,
message: 'Internal server error'
});
}
},
verifyToken: async (call, callback) => {
try {
const { token } = call.request;
const result = await authenticator.validateToken(token);
callback(null, {
isValid: result.isValid,
user: result.isValid ? result.credentials : null
});
} catch (error) {
console.error('gRPC verify token error:', error);
callback({
code: grpc.status.INTERNAL,
message: 'Internal server error'
});
}
}
});
return {
start: () => {
return new Promise((resolve, reject) => {
server.bindAsync(
`0.0.0.0:${config.grpc.port}`,
grpc.ServerCredentials.createInsecure(),
(err, port) => {
if (err) {
reject(err);
return;
}
server.start();
console.log(`gRPC server running on port ${port}`);
resolve(port);
}
);
});
},
stop: () => {
return new Promise((resolve) => {
server.tryShutdown(resolve);
});
}
};
};
11. Main Application Setup
Let's create the main entry point:
// index.js
require('dotenv').config();
const express = require('express');
const { Server } = require('http');
const cors = require('cors');
const helmet = require('helmet');
// Configuration
const config = {
database: require('./config/database'),
jwt: {
secret: process.env.JWT_SECRET,
expiresIn: '1h',
issuer: 'identity-access'
},
http: {
port: process.env.HTTP_PORT || 3000
},
grpc: {
port: process.env.GRPC_PORT || 50051
},
env: process.env.NODE_ENV || 'development'
};
// Initializations
const encryption = require('./lib/encryption')(process.env.ENCRYPTION_KEY);
const database = require('./adapters/database')({ config, encryption });
const userRepository = require('./adapters/database/user-repository')({
sequelize: database.sequelize,
models: database.models,
encryption
});
const jwtStrategy = require('./core/auth/strategies/jwt')({ config, userRepository });
const authenticator = require('./core/auth/authenticator')({ userRepository, jwtStrategy });
const authRouter = require('./adapters/http/auth-router')({ authenticator });
const grpcServer = require('./adapters/grpc/server')({ authenticator, config });
// Express app setup
const app = express();
app.use(helmet());
app.use(cors());
app.use(express.json());
// Routes
app.use('/auth', authRouter);
// Health check
app.get('/health', (req, res) => {
res.json({ status: 'ok', timestamp: new Date() });
});
// Error handler
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).json({
success: false,
message: 'Internal server error'
});
});
// Startup
async function start() {
try {
// Connect to database
await database.connect();
await database.sync();
// Start HTTP server
const server = Server(app);
server.listen(config.http.port, () => {
console.log(`HTTP server running on port ${config.http.port}`);
});
// Start gRPC server
await grpcServer.start();
// Handle shutdown
const shutdown = async () => {
console.log('Shutting down services...');
await grpcServer.stop();
server.close();
await database.close();
console.log('All services stopped');
process.exit(0);
};
process.on('SIGINT', shutdown);
process.on('SIGTERM', shutdown);
} catch (error) {
console.error('Failed to start services:', error);
process.exit(1);
}
}
start();
// Export for testing
module.exports = app;
12. Create Dockerfile
# Dockerfile
FROM node:18-alpine
WORKDIR /app
# Install dependencies
COPY package*.json ./
RUN npm ci --only=production
# Copy app source
COPY . .
# Expose ports
EXPOSE 3000
EXPOSE 50051
# Run the application
CMD ["node", "index.js"]
13. Create package.json
{
"name": "@yourorg/identity-access",
"version": "1.0.0",
"description": "Reusable authentication/authorization module with PostgreSQL integration",
"main": "index.js",
"scripts": {
"start": "node index.js",
"dev": "nodemon index.js",
"test": "jest --coverage",
"test:integration": "jest --config jest.integration.config.js",
"coverage": "jest --coverage && coveralls < coverage/lcov.info"
},
"dependencies": {
"@grpc/grpc-js": "^1.8.0",
"@grpc/proto-loader": "^0.7.0",
"bcrypt": "^5.1.0",
"cors": "^2.8.5",
"dotenv": "^16.0.3",
"express": "^4.18.2",
"helmet": "^6.0.1",
"jsonwebtoken": "^9.0.0",
"pg": "^8.11.3",
"pg-connection-string": "^2.5.0",
"sequelize": "^6.37.1"
},
"devDependencies": {
"coveralls": "^3.1.1",
"jest": "^29.5.0",
"nodemon": "^2.0.22",
"supertest": "^6.3.3"
},
"jest": {
"collectCoverageFrom": [
"**/*.js",
"!node_modules/**",
"!coverage/**",
"!jest.*.js"
]
},
"engines": {
"node": ">=14.0.0"
},
"license": "MIT"
}
14. Create Unit Tests
Let's add test files for our core modules:
// core/auth/auth.spec.js
const Authenticator = require('./authenticator');
describe('Authenticator', () => {
let authenticator;
let mockUserRepo;
let mockJwtStrategy;
beforeEach(() => {
mockUserRepo = {
findByEmail: jest.fn(),
verifyPassword: jest.fn()
};
mockJwtStrategy = {
generateToken: jest.fn(),
verify: jest.fn()
};
authenticator = Authenticator({
userRepository: mockUserRepo,
jwtStrategy: mockJwtStrategy
});
});
describe('authenticate', () => {
it('should return success with token for valid credentials', async () => {
// Arrange
const mockUser = {
id: 1,
email: '[email protected]',
roles: ['user']
};
mockUserRepo.findByEmail.mockResolvedValue(mockUser);
mockUserRepo.verifyPassword.mockResolvedValue(true);
mockJwtStrategy.generateToken.mockReturnValue('mock-token');
// Act
const result = await authenticator.authenticate('[email protected]', 'password');
// Assert
expect(result.success).toBe(true);
expect(result.token).toBe('mock-token');
expect(result.user).toEqual({
id: 1,
email: '[email protected]',
roles: ['user']
});
});
it('should return failure for invalid email', async () => {
// Arrange
mockUserRepo.findByEmail.mockResolvedValue(null);
// Act
const result = await authenticator.authenticate('[email protected]', 'password');
// Assert
expect(result.success).toBe(false);
expect(result.message).toBe('Invalid credentials');
});
it('should return failure for invalid password', async () => {
// Arrange
const mockUser = { id: 1, email: '[email protected]' };
mockUserRepo.findByEmail.mockResolvedValue(mockUser);
mockUserRepo.verifyPassword.mockResolvedValue(false);
// Act
const result = await authenticator.authenticate('[email protected]', 'wrong');
// Assert
expect(result.success).toBe(false);
expect(result.message).toBe('Invalid credentials');
});
});
describe('validateToken', () => {
it('should return valid result for valid token', async () => {
// Arrange
const mockResult = {
isValid: true,
credentials: { id: 1, email: '[email protected]' }
};
mockJwtStrategy.verify.mockResolvedValue(mockResult);
// Act
const result = await authenticator.validateToken('valid-token');
// Assert
expect(result).toEqual(mockResult);
});
it('should return invalid result for invalid token', async () => {
// Arrange
mockJwtStrategy.verify.mockResolvedValue({ isValid: false });
// Act
const result = await authenticator.validateToken('invalid-token');
// Assert
expect(result.isValid).toBe(false);
});
});
});
15. Create CI/CD Pipeline
# .github/workflows/pipeline.yml
name: CI/CD Pipeline
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:15
env:
POSTGRES_PASSWORD: postgres
POSTGRES_DB: testdb
ports: ["5432:5432"]
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test
env:
ENCRYPTION_KEY: '32_char_encryption_key_for_testing'
JWT_SECRET: 'jwt_secret_for_testing'
TEST_POSTGRES_URI: 'postgresql://postgres:postgres@localhost:5432/testdb'
- name: Coveralls
uses: coverallsapp/github-action@master
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
deploy:
needs: test
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
registry-url: 'https://registry.npmjs.org'
- name: Install dependencies
run: npm ci
- name: Publish to NPM
run: npm publish --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Docker Build
run: docker build -t yourorg/identity-access:${{ github.sha }} .
- name: Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKER_USER }}
password: ${{ secrets.DOCKER_TOKEN }}
- name: Push to Docker Hub
run: |
docker tag yourorg/identity-access:${{ github.sha }} yourorg/identity-access:latest
docker push yourorg/identity-access:${{ github.sha }}
docker push yourorg/identity-access:latest
16. Create Environment Variables Template
# .env.example
# PostgreSQL Connection
POSTGRES_URI=postgresql://user:password@localhost:5432/dbname
# Security
JWT_SECRET=your_secure_jwt_secret_key
ENCRYPTION_KEY=32_char_encryption_key_for_security
# Server Configuration
HTTP_PORT=3000
GRPC_PORT=50051
NODE_ENV=development
Database Migration Script
Let's create a migration script for database setup:
// scripts/migrate.js
const { Sequelize } = require('sequelize');
require('dotenv').config();
async function migrate() {
const sequelize = new Sequelize(process.env.POSTGRES_URI, {
dialect: 'postgres',
logging: console.log
});
try {
await sequelize.authenticate();
console.log('Connected to database');
// Create tables
await sequelize.query(`
CREATE TABLE IF NOT EXISTS users (
id SERIAL PRIMARY KEY,
email TEXT NOT NULL UNIQUE,
password_hash TEXT NOT NULL,
sensitive_data TEXT,
roles TEXT[],
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
`);
// Create indexes
await sequelize.query(`
CREATE INDEX IF NOT EXISTS idx_users_email ON users USING hash(email);
CREATE INDEX IF NOT EXISTS idx_users_created_at ON users (created_at);
`);
console.log('Migration completed successfully');
} catch (error) {
console.error('Migration failed:', error);
} finally {
await sequelize.close();
}
}
migrate();
Usage Guide
To use this module in your project:
-
Install the package:
npm install @yourorg/identity-access
-
Create a
.env
file with required environment variables (see.env.example
). -
Basic usage:
const { authenticator } = require('@yourorg/identity-access'); // Authenticate a user const result = await authenticator.authenticate('[email protected]', 'password'); if (result.success) { // User authenticated, use result.token for authorization } // Verify a token const verification = await authenticator.validateToken(token); if (verification.isValid) { // Token valid, use verification.credentials for user info }
-
Or use the HTTP/gRPC interfaces directly.
PostgreSQL-specific Optimizations
-
For larger deployments, consider connection pooling:
const { Pool } = require('pg'); const pool = new Pool({ connectionString: process.env.POSTGRES_URI, max: 20, idleTimeoutMillis: 30000, connectionTimeoutMillis: 2000 });
-
For high-load scenarios, implement read replicas:
const masterClient = new Client(process.env.MASTER_POSTGRES_URI); const replicaClient = new Client(process.env.REPLICA_POSTGRES_URI);
-
Add specific PostgreSQL extensions:
CREATE EXTENSION IF NOT EXISTS pgcrypto; CREATE EXTENSION IF NOT EXISTS pg_trgm; -- For text search
Would you like me to provide any additional components or explain any specific part in more detail?