Let's go through all the steps systematically, making sure all code is correct and addressing any potential issues. Below is the complete and corrected code for building the Ionic Angular app with NextJS as the backend, MSSQL as the database, JWT authentication, role-based access control (RBAC), employee CRUD, and form validation.
Final Application: Full Guide
Backend - NextJS with MSSQL, TypeORM, and JWT
Step 1: Set Up the Backend Environment
1.1. Install Necessary Packages
npm install next express typeorm reflect-metadata mssql jsonwebtoken bcryptjs cors body-parser class-validator class-transformer
Step 2: Database and ORM Setup
2.1. Create a TypeORM Data Source
// backend/data-source.ts
import { DataSource } from "typeorm";
import { Employee } from "./entities/Employee";
export const AppDataSource = new DataSource({
type: "mssql",
host: "localhost",
username: "your_db_user",
password: "your_db_password",
database: "your_db_name",
synchronize: true, // Automatically synchronize schema during development
entities: [Employee],
});
Step 3: Employee Entity
Create the Employee entity with validation using class-validator:
// backend/entities/Employee.ts
import { Entity, PrimaryGeneratedColumn, Column } from "typeorm";
import { IsEmail, IsNotEmpty, MinLength, Matches } from "class-validator";
@Entity()
export class Employee {
@PrimaryGeneratedColumn()
id: number;
@Column()
@IsNotEmpty({ message: "Name is required" })
name: string;
@Column()
@IsEmail({}, { message: "Invalid email" })
email: string;
@Column()
@IsNotEmpty({ message: "Password is required" })
@MinLength(8, { message: "Password must be at least 8 characters" })
@Matches(/^(?=.*[0-9])(?=.*[a-zA-Z])([a-zA-Z0-9]+)$/, {
message: "Password must contain both letters and numbers",
})
password: string;
@Column()
@IsNotEmpty({ message: "Position is required" })
position: string;
@Column({ default: true })
isActive: boolean;
@Column({ nullable: true })
resetToken: string;
@Column({ nullable: true })
resetTokenExpires: Date;
@Column({ default: 'User' })
role: string;
}
Step 4: JWT Utility Functions
Create helper functions to generate and verify JWT tokens:
// backend/utils/jwt.ts
import jwt from "jsonwebtoken";
const SECRET_KEY = "your_secret_key";
export const generateToken = (employee) => {
return jwt.sign(
{ id: employee.id, email: employee.email, role: employee.role },
SECRET_KEY,
{ expiresIn: "1h" }
);
};
export const verifyToken = (token) => {
return jwt.verify(token, SECRET_KEY);
};
Step 5: JWT Middleware
Verify JWT tokens and implement role-based access control:
// backend/utils/verifyToken.ts
import { NextApiRequest, NextApiResponse } from "next";
import { verifyToken } from "./jwt";
export const verifyJwt = (handler) => async (req: NextApiRequest, res: NextApiResponse) => {
const token = req.headers.authorization?.split(" ")[1];
if (!token) {
return res.status(401).json({ message: "No token provided" });
}
try {
const decoded = verifyToken(token);
req.user = decoded;
return handler(req, res);
} catch (error) {
return res.status(401).json({ message: "Invalid token" });
}
};
export const verifyRole = (handler, role) => async (req: NextApiRequest, res: NextApiResponse) => {
const token = req.headers.authorization?.split(" ")[1];
if (!token) {
return res.status(401).json({ message: "No token provided" });
}
try {
const decoded = verifyToken(token);
if (decoded.role !== role) {
return res.status(403).json({ message: "Forbidden" });
}
req.user = decoded;
return handler(req, res);
} catch (error) {
return res.status(401).json({ message: "Invalid token" });
}
};
Step 6: Authentication API Routes
6.1. Registration API
// backend/pages/api/employees/register.ts
import { NextApiRequest, NextApiResponse } from "next";
import bcrypt from "bcryptjs";
import { AppDataSource } from "../../../data-source";
import { Employee } from "../../../entities/Employee";
import { validate } from "class-validator";
AppDataSource.initialize();
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method === "POST") {
const { name, email, password, position } = req.body;
const employeeRepository = AppDataSource.getRepository(Employee);
const newEmployee = new Employee();
newEmployee.name = name;
newEmployee.email = email;
newEmployee.password = bcrypt.hashSync(password, 8);
newEmployee.position = position;
const errors = await validate(newEmployee);
if (errors.length > 0) {
return res.status(400).json({ errors });
}
await employeeRepository.save(newEmployee);
res.status(201).json(newEmployee);
} else {
res.status(405).json({ message: "Method Not Allowed" });
}
}
6.2. Login API
// backend/pages/api/employees/login.ts
import { NextApiRequest, NextApiResponse } from "next";
import bcrypt from "bcryptjs";
import { AppDataSource } from "../../../data-source";
import { Employee } from "../../../entities/Employee";
import { generateToken } from "../../../utils/jwt";
AppDataSource.initialize();
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method === "POST") {
const { email, password } = req.body;
const employeeRepository = AppDataSource.getRepository(Employee);
const employee = await employeeRepository.findOne({ where: { email } });
if (!employee || !bcrypt.compareSync(password, employee.password)) {
return res.status(400).json({ message: "Invalid email or password" });
}
const token = generateToken(employee);
res.status(200).json({ token });
} else {
res.status(405).json({ message: "Method Not Allowed" });
}
}
Step 7: Employee CRUD Operations
7.1. Fetch All Employees (JWT Protected)
// backend/pages/api/employees/index.ts
import { NextApiRequest, NextApiResponse } from "next";
import { AppDataSource } from "../../../data-source";
import { Employee } from "../../../entities/Employee";
import { verifyJwt } from "../../../utils/verifyToken";
AppDataSource.initialize();
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const employeeRepository = AppDataSource.getRepository(Employee);
if (req.method === "GET") {
const employees = await employeeRepository.find();
res.status(200).json(employees);
} else {
res.status(405).json({ message: "Method Not Allowed" });
}
};
export default verifyJwt(handler);
7.2. Edit and Delete Employee (Admin-Only)
// backend/pages/api/employees/[id].ts
import { NextApiRequest, NextApiResponse } from "next";
import { AppDataSource } from "../../../data-source";
import { Employee } from "../../../entities/Employee";
import { verifyRole } from "../../../utils/verifyToken";
AppDataSource.initialize();
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const employeeRepository = AppDataSource.getRepository(Employee);
const { id } = req.query;
if (req.method === "PUT") {
const { name, email, position, isActive } = req.body;
const employee = await employeeRepository.findOne({ where: { id: Number(id) } });
if (!employee) {
return res.status(404).json({ message: "Employee not found" });
}
employee.name = name || employee.name;
employee.email = email || employee.email;
employee.position = position || employee.position;
employee.isActive = isActive !== undefined ? isActive : employee.isActive;
await employeeRepository.save(employee);
res.status(200).json(employee);
}
if (req.method === "DELETE") {
const employee = await employeeRepository.findOne({ where: { id: Number(id) } });
if (!employee) {
return res.status(404).json({ message: "Employee not found" });
}
await employeeRepository.remove(employee);
res.status(204).send();
}
};
export default verifyRole(handler, "Admin");
Step 8: Frontend - Ionic Angular
8.1. Install Dependencies
npm install axios @angular/forms
8.2. Create `
EmployeeService` for API Interactions
// src/app/services/employee.service.ts
import { Injectable } from "@angular/core";
import axios from "axios";
@Injectable({
providedIn: "root",
})
export class EmployeeService {
private apiUrl = "http://localhost:3000/api/employees";
async login(email: string, password: string) {
const response = await axios.post(`${this.apiUrl}/login`, { email, password });
return response.data;
}
async getEmployees(token: string) {
const response = await axios.get(this.apiUrl, {
headers: { Authorization: `Bearer ${token}` },
});
return response.data;
}
async getEmployeeById(token: string, employeeId: number) {
const response = await axios.get(`${this.apiUrl}/${employeeId}`, {
headers: { Authorization: `Bearer ${token}` },
});
return response.data;
}
async addEmployee(token: string, employeeData: any) {
const response = await axios.post(this.apiUrl, employeeData, {
headers: { Authorization: `Bearer ${token}` },
});
return response.data;
}
async updateEmployee(token: string, employeeId: number, employeeData: any) {
const response = await axios.put(`${this.apiUrl}/${employeeId}`, employeeData, {
headers: { Authorization: `Bearer ${token}` },
});
return response.data;
}
async deleteEmployee(token: string, employeeId: number) {
const response = await axios.delete(`${this.apiUrl}/${employeeId}`, {
headers: { Authorization: `Bearer ${token}` },
});
return response.data;
}
async requestPasswordReset(email: string) {
const response = await axios.post(`${this.apiUrl}/reset-request`, { email });
return response.data;
}
async resetPassword(token: string, newPassword: string) {
const response = await axios.post(`${this.apiUrl}/reset-password`, { token, newPassword });
return response.data;
}
}
Step 9: Login Page
9.1. Create the Login Page
ionic generate page login
9.2. Login Page Template
<ion-header>
<ion-toolbar>
<ion-title>Login</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<form [formGroup]="loginForm" (ngSubmit)="login()">
<ion-item>
<ion-label position="floating">Email</ion-label>
<ion-input type="email" formControlName="email"></ion-input>
</ion-item>
<ion-note *ngIf="loginForm.get('email').hasError('required') && loginForm.get('email').touched">
Email is required
</ion-note>
<ion-note *ngIf="loginForm.get('email').hasError('email') && loginForm.get('email').touched">
Invalid email
</ion-note>
<ion-item>
<ion-label position="floating">Password</ion-label>
<ion-input type="password" formControlName="password"></ion-input>
</ion-item>
<ion-note *ngIf="loginForm.get('password').hasError('required') && loginForm.get('password').touched">
Password is required
</ion-note>
<ion-button expand="block" type="submit" [disabled]="loginForm.invalid">Login</ion-button>
</form>
</ion-content>
9.3. Login Page Logic
// src/app/pages/login/login.page.ts
import { Component, OnInit } from "@angular/core";
import { FormBuilder, FormGroup, Validators } from "@angular/forms";
import { EmployeeService } from "../../services/employee.service";
import { Router } from "@angular/router";
@Component({
selector: "app-login",
templateUrl: "./login.page.html",
styleUrls: ["./login.page.scss"],
})
export class LoginPage implements OnInit {
loginForm: FormGroup;
constructor(
private fb: FormBuilder,
private employeeService: EmployeeService,
private router: Router
) {}
ngOnInit() {
this.loginForm = this.fb.group({
email: ["", [Validators.required, Validators.email]],
password: ["", [Validators.required]],
});
}
async login() {
if (this.loginForm.valid) {
const { email, password } = this.loginForm.value;
try {
const { token } = await this.employeeService.login(email, password);
localStorage.setItem("token", token);
this.router.navigate(["/employee-list"]);
} catch (error) {
console.log("Login failed:", error);
}
}
}
}
Step 10: Employee List Page
10.1. Create the Employee List Page
ionic generate page employee-list
10.2. Employee List Template
<ion-header>
<ion-toolbar>
<ion-title>Employees</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-list>
<ion-item *ngFor="let employee of employees">
<ion-label>{{ employee.name }} ({{ employee.position }})</ion-label>
<ion-button color="primary" (click)="editEmployee(employee.id)">Edit</ion-button>
<ion-button *ngIf="isAdmin" color="danger" (click)="deleteEmployee(employee.id)">Delete</ion-button>
</ion-item>
</ion-list>
<ion-button expand="block" (click)="loadEmployees()">Refresh</ion-button>
</ion-content>
10.3. Employee List Logic
// src/app/pages/employee-list/employee-list.page.ts
import { Component, OnInit } from "@angular/core";
import { EmployeeService } from "../../services/employee.service";
import { Router } from "@angular/router";
@Component({
selector: "app-employee-list",
templateUrl: "./employee-list.page.html",
styleUrls: ["./employee-list.page.scss"],
})
export class EmployeeListPage implements OnInit {
employees = [];
isAdmin = false;
constructor(private employeeService: EmployeeService, private router: Router) {}
async ngOnInit() {
const token = localStorage.getItem("token");
const decodedToken = this.decodeToken(token);
this.isAdmin = decodedToken.role === "Admin";
this.loadEmployees();
}
decodeToken(token: string) {
const base64Url = token.split(".")[1];
const base64 = base64Url.replace("-", "+").replace("_", "/");
return JSON.parse(atob(base64));
}
async loadEmployees() {
const token = localStorage.getItem("token");
try {
this.employees = await this.employeeService.getEmployees(token);
} catch (error) {
console.log("Failed to load employees:", error);
}
}
async deleteEmployee(id: number) {
const token = localStorage.getItem("token");
try {
await this.employeeService.deleteEmployee(token, id);
this.loadEmployees();
} catch (error) {
console.log("Failed to delete employee:", error);
}
}
editEmployee(id: number) {
this.router.navigate([`/edit-employee/${id}`]);
}
}
Final Thoughts
This final application integrates login, employee listing with edit/delete options, admin access control, and JWT authentication. By following these steps, you can now build a full-featured app with a backend powered by NextJS and MSSQL, and a frontend with Ionic Angular. The forms have proper validation, and the app follows role-based access control for sensitive actions.