create-nightwing 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/LICENSE +5 -0
  2. package/README.md +51 -0
  3. package/bin/index.js +71 -0
  4. package/package.json +37 -0
  5. package/templates/employee-rbac/client/index.html +1 -0
  6. package/templates/employee-rbac/client/package.json +22 -0
  7. package/templates/employee-rbac/client/postcss.config.js +1 -0
  8. package/templates/employee-rbac/client/src/api/http.js +8 -0
  9. package/templates/employee-rbac/client/src/components/ui.jsx +7 -0
  10. package/templates/employee-rbac/client/src/index.css +5 -0
  11. package/templates/employee-rbac/client/src/layouts/AppLayout.jsx +18 -0
  12. package/templates/employee-rbac/client/src/main.jsx +5 -0
  13. package/templates/employee-rbac/client/src/pages/Accounts.jsx +14 -0
  14. package/templates/employee-rbac/client/src/pages/AdminDashboard.jsx +11 -0
  15. package/templates/employee-rbac/client/src/pages/Employees.jsx +14 -0
  16. package/templates/employee-rbac/client/src/pages/Login.jsx +9 -0
  17. package/templates/employee-rbac/client/src/pages/Templates.jsx +10 -0
  18. package/templates/employee-rbac/client/src/pages/UserDashboard.jsx +10 -0
  19. package/templates/employee-rbac/client/src/router/index.jsx +24 -0
  20. package/templates/employee-rbac/client/tailwind.config.js +1 -0
  21. package/templates/employee-rbac/docs/API.md +58 -0
  22. package/templates/employee-rbac/docs/SQL.md +55 -0
  23. package/templates/employee-rbac/docs/TAILWIND.md +34 -0
  24. package/templates/employee-rbac/instructions.md +39 -0
  25. package/templates/employee-rbac/schema.sql +45 -0
  26. package/templates/employee-rbac/server/package.json +19 -0
  27. package/templates/employee-rbac/server/src/app.js +22 -0
  28. package/templates/employee-rbac/server/src/config/db.js +13 -0
  29. package/templates/employee-rbac/server/src/controllers/authController.js +48 -0
  30. package/templates/employee-rbac/server/src/controllers/employeeController.js +28 -0
  31. package/templates/employee-rbac/server/src/controllers/userController.js +52 -0
  32. package/templates/employee-rbac/server/src/middleware/auth.js +12 -0
  33. package/templates/employee-rbac/server/src/routes/authRoutes.js +9 -0
  34. package/templates/employee-rbac/server/src/routes/employeeRoutes.js +10 -0
  35. package/templates/employee-rbac/server/src/routes/userRoutes.js +12 -0
  36. package/templates/employee-rbac/server/src/utils/security.js +3 -0
package/LICENSE ADDED
@@ -0,0 +1,5 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 House of Logic Ltd
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy...
package/README.md ADDED
@@ -0,0 +1,51 @@
1
+
2
+ # create-nightwing
3
+
4
+ A clean enterprise starter generator for student exams, internal tools, and real admin systems.
5
+
6
+ It generates one focused fullstack template:
7
+
8
+ - Admin login
9
+ - Employee management
10
+ - Employee-owned user accounts
11
+ - User login
12
+ - Session authentication
13
+ - Password hashing
14
+ - Protected routes
15
+ - Airbnb-inspired UI system
16
+ - Forms, tables, modal CRUD, drawer, stats cards, filters, badges, empty states
17
+ - MySQL schema and beginner-friendly docs
18
+
19
+ ## Usage
20
+
21
+ ```bash
22
+ npx create-nightwing my-app
23
+ cd my-app
24
+ mysql -u root -p < schema.sql
25
+ cd server && npm install && npm run dev
26
+ cd ../client && npm install && npm run dev
27
+ ```
28
+
29
+ ## Default admin
30
+
31
+ ```txt
32
+ username: admin
33
+ password: admin123
34
+ ```
35
+
36
+ ## Generated structure
37
+
38
+ ```txt
39
+ my-app/
40
+ server/ Express + MySQL + sessions
41
+ client/ React + Vite + Tailwind
42
+ docs/ SQL, Tailwind, API, testing docs
43
+ schema.sql Database setup and seed
44
+ ```
45
+
46
+ ## Publish this package
47
+
48
+ ```bash
49
+ npm login
50
+ npm publish
51
+ ```
package/bin/index.js ADDED
@@ -0,0 +1,71 @@
1
+ #!/usr/bin/env node
2
+ import fs from "fs-extra";
3
+ import path from "path";
4
+ import { input, confirm } from "@inquirer/prompts";
5
+ import kleur from "kleur";
6
+ import { spawnSync } from "child_process";
7
+ import { fileURLToPath } from "url";
8
+
9
+ const __filename = fileURLToPath(import.meta.url);
10
+ const __dirname = path.dirname(__filename);
11
+
12
+ const slugify = (value) => value.trim().toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-_]/g, "");
13
+ const dbSafe = (value) => value.trim().replace(/-/g, "_").replace(/[^a-zA-Z0-9_]/g, "_");
14
+ const validPort = (value) => { const n=Number(value); return Number.isInteger(n) && n>0 && n<=65535; };
15
+
16
+ const flags = new Set(process.argv.slice(2).filter(a => a.startsWith("--")));
17
+ const yes = flags.has("--yes") || flags.has("-y");
18
+ function argValue(name, fallback){ const raw=process.argv.find(a=>a.startsWith(`--${name}=`)); return raw ? raw.split("=").slice(1).join("=") : fallback; }
19
+
20
+ let projectName = process.argv.slice(2).find(a => !a.startsWith("--"));
21
+ if (!projectName) {
22
+ projectName = await input({ message: "Project name:", validate: v => slugify(v).length ? true : "Project name is required.", transformer: slugify });
23
+ }
24
+ projectName = slugify(projectName);
25
+ if (!projectName) { console.log(kleur.red("Project name is required.")); process.exit(1); }
26
+
27
+ const dbName = dbSafe(yes ? argValue("db", `${projectName.replace(/-/g,"_")}_db`) : await input({ message: "Database name:", default: `${projectName.replace(/-/g,"_")}_db`, validate: v => dbSafe(v).length ? true : "Database name is required." }));
28
+ const serverPort = yes ? argValue("server-port", "5000") : await input({ message: "Backend port:", default: "5000", validate: v => validPort(v) ? true : "Enter a valid port." });
29
+ const clientPort = yes ? argValue("client-port", "5173") : await input({ message: "Frontend port:", default: "5173", validate: v => validPort(v) ? true : "Enter a valid port." });
30
+ const installDeps = yes ? false : await confirm({ message: "Install dependencies now?", default: false });
31
+
32
+ const templateDir = path.join(__dirname, "..", "templates", "employee-rbac");
33
+ const targetDir = path.join(process.cwd(), projectName);
34
+ if (!fs.existsSync(templateDir)) { console.log(kleur.red("Template folder not found.")); process.exit(1); }
35
+ if (fs.existsSync(targetDir)) { console.log(kleur.red("Folder already exists. Choose another name.")); process.exit(1); }
36
+
37
+ await fs.copy(templateDir, targetDir);
38
+ const replacements = { __PROJECT_NAME__: projectName, __DB_NAME__: dbName, __SERVER_PORT__: String(serverPort), __CLIENT_PORT__: String(clientPort) };
39
+ async function replaceInFile(file){ let content=await fs.readFile(file,"utf8"); for(const [k,v] of Object.entries(replacements)) content=content.replaceAll(k,v); await fs.writeFile(file,content); }
40
+ async function walk(dir){ for(const item of await fs.readdir(dir)){ const full=path.join(dir,item); const stat=await fs.stat(full); if(stat.isDirectory()) await walk(full); else if(/\.(js|jsx|json|css|html|md|sql|env)$/.test(full)) await replaceInFile(full); } }
41
+ await walk(targetDir);
42
+
43
+ await fs.writeFile(path.join(targetDir,"server",".env"), `PORT=${serverPort}
44
+ DB_HOST=localhost
45
+ DB_USER=root
46
+ DB_PASSWORD=
47
+ DB_NAME=${dbName}
48
+ CLIENT_URL=http://localhost:${clientPort}
49
+ SESSION_SECRET=change_this_${projectName}_secret
50
+ NODE_ENV=development
51
+ `);
52
+ await fs.writeFile(path.join(targetDir,"client",".env"), `VITE_API_URL=http://localhost:${serverPort}
53
+ `);
54
+
55
+ if(installDeps){
56
+ console.log(kleur.cyan("Installing server dependencies..."));
57
+ spawnSync("npm",["install"],{cwd:path.join(targetDir,"server"),stdio:"inherit",shell:true});
58
+ console.log(kleur.cyan("Installing client dependencies..."));
59
+ spawnSync("npm",["install"],{cwd:path.join(targetDir,"client"),stdio:"inherit",shell:true});
60
+ }
61
+ console.log(kleur.green(`
62
+ Created ${projectName} successfully.`));
63
+ console.log(`
64
+ Next steps:
65
+ cd ${projectName}
66
+ mysql -u root -p < schema.sql
67
+ cd server && npm install && npm run dev
68
+ cd ../client && npm install && npm run dev
69
+
70
+ Default admin: admin / admin123
71
+ `);
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "create-nightwing",
3
+ "version": "1.0.0",
4
+ "description": "Enterprise-ready React + Express + MySQL employee RBAC starter generator.",
5
+ "type": "module",
6
+ "bin": {
7
+ "create-nightwing": "./bin/index.js"
8
+ },
9
+ "files": [
10
+ "bin",
11
+ "templates",
12
+ "README.md",
13
+ "LICENSE"
14
+ ],
15
+ "keywords": [
16
+ "cli",
17
+ "react",
18
+ "express",
19
+ "mysql",
20
+ "session",
21
+ "rbac",
22
+ "employee",
23
+ "dashboard",
24
+ "tailwind",
25
+ "starter"
26
+ ],
27
+ "author": "House of Logic Ltd",
28
+ "license": "MIT",
29
+ "engines": {
30
+ "node": ">=18"
31
+ },
32
+ "dependencies": {
33
+ "@inquirer/prompts": "^7.3.3",
34
+ "fs-extra": "^11.3.0",
35
+ "kleur": "^4.1.5"
36
+ }
37
+ }
@@ -0,0 +1 @@
1
+ <!doctype html><html><head><meta charset="UTF-8"/><meta name="viewport" content="width=device-width, initial-scale=1.0"/><title>__PROJECT_NAME__</title></head><body><div id="root"></div><script type="module" src="/src/main.jsx"></script></body></html>
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "__PROJECT_NAME__-client",
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "scripts": {
6
+ "dev": "vite --host 0.0.0.0 --port __CLIENT_PORT__",
7
+ "build": "vite build",
8
+ "preview": "vite preview"
9
+ },
10
+ "dependencies": {
11
+ "@vitejs/plugin-react": "latest",
12
+ "vite": "latest",
13
+ "react": "latest",
14
+ "react-dom": "latest",
15
+ "lucide-react": "^0.468.0"
16
+ },
17
+ "devDependencies": {
18
+ "tailwindcss": "3.4.17",
19
+ "postcss": "8.4.49",
20
+ "autoprefixer": "10.4.20"
21
+ }
22
+ }
@@ -0,0 +1 @@
1
+ export default { plugins: { tailwindcss: {}, autoprefixer: {} } };
@@ -0,0 +1,8 @@
1
+
2
+ const API_URL = import.meta.env.VITE_API_URL || "http://localhost:__SERVER_PORT__";
3
+ export async function request(path,{method="GET",body}={}){
4
+ const res=await fetch(`${API_URL}/api${path}`,{method,credentials:"include",headers:{"Content-Type":"application/json"},body:body?JSON.stringify(body):undefined});
5
+ const data=await res.json().catch(()=>({}));
6
+ if(!res.ok) throw new Error(data.message || "Request failed");
7
+ return data;
8
+ }
@@ -0,0 +1,7 @@
1
+
2
+ export function Card({children,className=""}){ return <div className={`rounded-[2rem] border border-slate-200 bg-white p-5 shadow-sm ${className}`}>{children}</div>; }
3
+ export function Button({children,className="",variant="primary",...props}){ const styles=variant==="primary"?"bg-[#FF385C] text-white hover:bg-[#E31C5F]":"border border-slate-200 bg-white text-slate-900 hover:bg-slate-50"; return <button className={`rounded-full px-5 py-3 text-sm font-black transition disabled:opacity-50 ${styles} ${className}`} {...props}>{children}</button>; }
4
+ export function Input(props){ return <input className="w-full rounded-2xl border border-slate-200 bg-white px-4 py-3 text-sm outline-none transition focus:border-[#FF385C]" {...props}/>; }
5
+ export function Select(props){ return <select className="w-full rounded-2xl border border-slate-200 bg-white px-4 py-3 text-sm outline-none transition focus:border-[#FF385C]" {...props}/>; }
6
+ export function Badge({children,tone="slate"}){ const map={green:"bg-emerald-50 text-emerald-700",red:"bg-rose-50 text-rose-700",amber:"bg-amber-50 text-amber-700",slate:"bg-slate-100 text-slate-700"}; return <span className={`rounded-full px-3 py-1 text-xs font-black ${map[tone]||map.slate}`}>{children}</span>; }
7
+ export function EmptyState({title,note}){ return <div className="rounded-[2rem] border border-dashed border-slate-200 bg-slate-50 p-8 text-center"><h3 className="font-black text-slate-900">{title}</h3><p className="mt-2 text-sm text-slate-500">{note}</p></div>; }
@@ -0,0 +1,5 @@
1
+ @tailwind base;
2
+ @tailwind components;
3
+ @tailwind utilities;
4
+ body{background:#F7F7F7;color:#0F172A;}
5
+ *{box-sizing:border-box;}
@@ -0,0 +1,18 @@
1
+
2
+ import { LogOut, Menu, Users, LayoutDashboard, FileText, UserRoundCog } from "lucide-react";
3
+ import { useState } from "react";
4
+ import { request } from "../api/http";
5
+ const adminNav=[['/admin','Dashboard',LayoutDashboard],['/employees','Employees',Users],['/accounts','Accounts',UserRoundCog],['/templates','Templates',FileText]];
6
+ const userNav=[['/user','My Dashboard',LayoutDashboard]];
7
+ export default function AppLayout({user,children}){
8
+ const [open,setOpen]=useState(false); const nav=user?.role==='admin'?adminNav:userNav;
9
+ async function logout(){ await request('/auth/logout',{method:'POST'}); location.href='/'; }
10
+ return <div className="min-h-screen bg-[#F7F7F7]">
11
+ <aside className={`fixed inset-y-0 left-0 z-40 w-72 border-r border-slate-200 bg-white p-5 transition md:translate-x-0 ${open?'translate-x-0':'-translate-x-full'}`}>
12
+ <div className="rounded-[2rem] bg-slate-950 p-5 text-white"><p className="text-xs font-black uppercase tracking-[0.2em] text-white/50">Nightwing</p><h1 className="mt-2 text-2xl font-black">__PROJECT_NAME__</h1></div>
13
+ <nav className="mt-6 grid gap-2">{nav.map(([href,label,Icon])=><a key={href} href={href} className="flex items-center gap-3 rounded-2xl px-4 py-3 text-sm font-bold text-slate-600 hover:bg-slate-100 hover:text-slate-950"><Icon size={18}/>{label}</a>)}</nav>
14
+ <button onClick={logout} className="absolute bottom-5 left-5 right-5 flex items-center justify-center gap-2 rounded-full border border-slate-200 px-4 py-3 text-sm font-black"><LogOut size={18}/> Logout</button>
15
+ </aside>
16
+ <main className="md:pl-72"><header className="sticky top-0 z-30 border-b border-slate-200 bg-white/80 px-4 py-3 backdrop-blur md:px-8"><div className="flex items-center justify-between"><button onClick={()=>setOpen(!open)} className="rounded-full border p-3 md:hidden"><Menu size={18}/></button><div><p className="text-xs font-black uppercase tracking-[0.2em] text-slate-400">Logged in as</p><p className="font-black">{user?.username} <span className="text-slate-400">/{user?.role}</span></p></div></div></header><section className="p-4 md:p-8">{children}</section></main>
17
+ </div>;
18
+ }
@@ -0,0 +1,5 @@
1
+ import React from "react";
2
+ import { createRoot } from "react-dom/client";
3
+ import "./index.css";
4
+ import AppRouter from "./router";
5
+ createRoot(document.getElementById("root")).render(<AppRouter/>);
@@ -0,0 +1,14 @@
1
+
2
+ import { useEffect, useState } from "react";
3
+ import { request } from "../api/http";
4
+ import { Button, Card, Input, Select, Badge } from "../components/ui";
5
+ export default function Accounts(){
6
+ const [employees,setEmployees]=useState([]),[users,setUsers]=useState([]),[form,setForm]=useState({employeeId:'',username:'',password:'admin123',status:'Active'}),[msg,setMsg]=useState(''),[reset,setReset]=useState({id:'',password:''});
7
+ const load=()=>{request('/employees').then(d=>setEmployees(d.employees));request('/users').then(d=>setUsers(d.users));}; useEffect(()=>{load()},[]);
8
+ async function create(e){e.preventDefault();setMsg('');try{await request('/users',{method:'POST',body:form});setForm({employeeId:'',username:'',password:'admin123',status:'Active'});setMsg('Account created');load();}catch(err){setMsg(err.message)}}
9
+ async function update(u,status){await request(`/users/${u.id}`,{method:'PUT',body:{username:u.username,status}});load();}
10
+ async function del(id){if(confirm('Delete this account?')){await request(`/users/${id}`,{method:'DELETE'});load();}}
11
+ async function resetPass(e){e.preventDefault();await request(`/users/${reset.id}/password`,{method:'PATCH',body:{password:reset.password}});setReset({id:'',password:''});setMsg('Password reset');}
12
+ const free=employees.filter(e=>!users.some(u=>u.employee_id===e.id));
13
+ return <div className="grid gap-6 xl:grid-cols-[420px_1fr]"><div className="grid gap-6"><Card><h1 className="text-2xl font-black">Create Account</h1><p className="mt-2 text-sm text-slate-500">Only employees without an account appear here.</p><form onSubmit={create} className="mt-5 grid gap-3"><Select value={form.employeeId} onChange={e=>setForm({...form,employeeId:e.target.value})}><option value="">Choose employee</option>{free.map(e=><option key={e.id} value={e.id}>{e.full_name}</option>)}</Select><Input placeholder="Username" value={form.username} onChange={e=>setForm({...form,username:e.target.value})}/><Input placeholder="Temporary password" value={form.password} onChange={e=>setForm({...form,password:e.target.value})}/><Select value={form.status} onChange={e=>setForm({...form,status:e.target.value})}><option>Active</option><option>Suspended</option></Select>{msg&&<p className="text-sm font-bold text-slate-500">{msg}</p>}<Button>Create login account</Button></form></Card>{reset.id&&<Card><h2 className="text-xl font-black">Reset Password</h2><form onSubmit={resetPass} className="mt-4 grid gap-3"><Input placeholder="New password" value={reset.password} onChange={e=>setReset({...reset,password:e.target.value})}/><Button>Save password</Button></form></Card>}</div><Card><h2 className="text-2xl font-black">User Accounts</h2><div className="mt-5 overflow-x-auto"><table className="w-full text-left text-sm"><thead><tr className="border-b text-slate-400"><th className="py-3">Account</th><th>Employee</th><th>Status</th><th>Actions</th></tr></thead><tbody>{users.map(u=><tr className="border-b" key={u.id}><td className="py-4 font-black">{u.username}<p className="font-normal text-slate-500">{u.email}</p></td><td>{u.employeeName}<p className="text-slate-500">{u.position}</p></td><td><Badge tone={u.status==='Active'?'green':'red'}>{u.status}</Badge></td><td className="space-x-2"><button className="font-black text-[#FF385C]" onClick={()=>update(u,u.status==='Active'?'Suspended':'Active')}>{u.status==='Active'?'Suspend':'Activate'}</button><button className="font-black" onClick={()=>setReset({id:u.id,password:'newpass123'})}>Reset</button><button className="font-black text-rose-700" onClick={()=>del(u.id)}>Delete</button></td></tr>)}</tbody></table></div></Card></div>
14
+ }
@@ -0,0 +1,11 @@
1
+
2
+ import { useEffect, useState } from "react";
3
+ import { Users, UserRoundCog, ShieldCheck, Sparkles } from "lucide-react";
4
+ import { request } from "../api/http";
5
+ import { Card, Badge } from "../components/ui";
6
+ export default function AdminDashboard(){
7
+ const [employees,setEmployees]=useState([]),[users,setUsers]=useState([]);
8
+ useEffect(()=>{request('/employees').then(d=>setEmployees(d.employees));request('/users').then(d=>setUsers(d.users));},[]);
9
+ const stats=[['Employees',employees.length,Users],['Accounts',users.length,UserRoundCog],['Active Employees',employees.filter(e=>e.status==='Active').length,ShieldCheck],['Template Blocks',12,Sparkles]];
10
+ return <div className="grid gap-6"><section className="rounded-[2.5rem] bg-slate-950 p-8 text-white"><p className="text-xs font-black uppercase tracking-[0.2em] text-white/50">Admin Command Center</p><h1 className="mt-3 text-4xl font-black md:text-6xl">Manage people, access, and workflows.</h1></section><div className="grid gap-4 md:grid-cols-2 xl:grid-cols-4">{stats.map(([t,v,Icon])=><Card key={t}><Icon className="text-[#FF385C]"/><p className="mt-5 text-sm font-bold text-slate-500">{t}</p><p className="text-4xl font-black">{v}</p></Card>)}</div><Card><div className="flex items-center justify-between"><h2 className="text-xl font-black">Recent Employees</h2><a className="text-sm font-black text-[#FF385C]" href="/employees">Manage</a></div><div className="mt-4 overflow-x-auto"><table className="w-full text-left text-sm"><tbody>{employees.slice(0,5).map(e=><tr className="border-t" key={e.id}><td className="py-4 font-black">{e.full_name}</td><td>{e.position}</td><td><Badge tone={e.status==='Active'?'green':e.status==='On Leave'?'amber':'red'}>{e.status}</Badge></td></tr>)}</tbody></table></div></Card></div>
11
+ }
@@ -0,0 +1,14 @@
1
+
2
+ import { useEffect, useState } from "react";
3
+ import { request } from "../api/http";
4
+ import { Button, Card, Input, Select, Badge } from "../components/ui";
5
+ const empty={fullName:'',email:'',phone:'',department:'',position:'',status:'Active',hiredAt:new Date().toISOString().slice(0,10)};
6
+ export default function Employees(){
7
+ const [items,setItems]=useState([]),[form,setForm]=useState(empty),[editing,setEditing]=useState(null),[q,setQ]=useState(''),[msg,setMsg]=useState('');
8
+ const load=()=>request('/employees').then(d=>setItems(d.employees)); useEffect(()=>{load()},[]);
9
+ async function save(e){e.preventDefault(); setMsg(''); const body=form; try{ if(editing) await request(`/employees/${editing}`,{method:'PUT',body}); else await request('/employees',{method:'POST',body}); setForm(empty); setEditing(null); setMsg('Saved successfully'); load(); }catch(err){setMsg(err.message)} }
10
+ function edit(x){setEditing(x.id);setForm({fullName:x.full_name,email:x.email,phone:x.phone,department:x.department,position:x.position,status:x.status,hiredAt:String(x.hired_at).slice(0,10)})}
11
+ async function del(id){ if(confirm('Delete employee and linked account?')){await request(`/employees/${id}`,{method:'DELETE'});load();}}
12
+ const filtered=items.filter(x=>(x.full_name+x.email+x.position).toLowerCase().includes(q.toLowerCase()));
13
+ return <div className="grid gap-6 xl:grid-cols-[420px_1fr]"><Card><h1 className="text-2xl font-black">{editing?'Edit':'Create'} Employee</h1><form onSubmit={save} className="mt-5 grid gap-3"><Input placeholder="Full name" value={form.fullName} onChange={e=>setForm({...form,fullName:e.target.value})}/><Input placeholder="Email" value={form.email} onChange={e=>setForm({...form,email:e.target.value})}/><Input placeholder="Phone" value={form.phone} onChange={e=>setForm({...form,phone:e.target.value})}/><Input placeholder="Department" value={form.department} onChange={e=>setForm({...form,department:e.target.value})}/><Input placeholder="Position" value={form.position} onChange={e=>setForm({...form,position:e.target.value})}/><Select value={form.status} onChange={e=>setForm({...form,status:e.target.value})}><option>Active</option><option>On Leave</option><option>Inactive</option></Select><Input type="date" value={form.hiredAt} onChange={e=>setForm({...form,hiredAt:e.target.value})}/>{msg && <p className="text-sm font-bold text-slate-500">{msg}</p>}<Button>{editing?'Update':'Create'} employee</Button></form></Card><Card><div className="flex flex-col gap-3 md:flex-row md:items-center md:justify-between"><h2 className="text-2xl font-black">Employees</h2><Input placeholder="Search people..." value={q} onChange={e=>setQ(e.target.value)} /></div><div className="mt-5 overflow-x-auto"><table className="w-full text-left text-sm"><thead><tr className="border-b text-slate-400"><th className="py-3">Name</th><th>Department</th><th>Position</th><th>Status</th><th>Actions</th></tr></thead><tbody>{filtered.map(x=><tr className="border-b" key={x.id}><td className="py-4 font-black">{x.full_name}<p className="font-normal text-slate-500">{x.email}</p></td><td>{x.department}</td><td>{x.position}</td><td><Badge tone={x.status==='Active'?'green':x.status==='On Leave'?'amber':'red'}>{x.status}</Badge></td><td className="space-x-2"><button onClick={()=>edit(x)} className="font-black text-[#FF385C]">Edit</button><button onClick={()=>del(x.id)} className="font-black text-rose-700">Delete</button></td></tr>)}</tbody></table></div></Card></div>
14
+ }
@@ -0,0 +1,9 @@
1
+
2
+ import { useState } from "react";
3
+ import { request } from "../api/http";
4
+ import { Button, Card, Input } from "../components/ui";
5
+ export default function Login(){
6
+ const [portal,setPortal]=useState('admin'); const [form,setForm]=useState({username:'admin',password:'admin123'}); const [error,setError]=useState('');
7
+ async function submit(e){e.preventDefault(); setError(''); try{await request(`/auth/${portal}/login`,{method:'POST',body:form}); location.href=portal==='admin'?'/admin':'/user';}catch(err){setError(err.message)}}
8
+ return <main className="min-h-screen bg-[#F7F7F7] p-4 md:p-8"><div className="mx-auto grid min-h-[calc(100vh-4rem)] max-w-6xl items-center gap-8 md:grid-cols-2"><section><div className="inline-flex rounded-full bg-white p-1 shadow-sm"><button onClick={()=>{setPortal('admin');setForm({username:'admin',password:'admin123'})}} className={`rounded-full px-5 py-3 text-sm font-black ${portal==='admin'?'bg-[#FF385C] text-white':'text-slate-500'}`}>Admin</button><button onClick={()=>{setPortal('user');setForm({username:'aline',password:'admin123'})}} className={`rounded-full px-5 py-3 text-sm font-black ${portal==='user'?'bg-[#FF385C] text-white':'text-slate-500'}`}>User</button></div><h1 className="mt-8 text-5xl font-black tracking-tight text-slate-950 md:text-7xl">A cleaner way to start serious systems.</h1><p className="mt-5 max-w-xl text-lg leading-8 text-slate-500">Employee records, account ownership, session auth, and polished admin UX in one focused template.</p></section><Card className="p-8"><p className="text-xs font-black uppercase tracking-[0.2em] text-[#FF385C]">{portal} portal</p><h2 className="mt-3 text-3xl font-black">Welcome back</h2><form onSubmit={submit} className="mt-6 grid gap-4"><Input placeholder="username" value={form.username} onChange={e=>setForm({...form,username:e.target.value})}/><Input type="password" placeholder="password" value={form.password} onChange={e=>setForm({...form,password:e.target.value})}/>{error && <p className="rounded-2xl bg-rose-50 p-3 text-sm font-bold text-rose-700">{error}</p>}<Button>Sign in</Button></form><p className="mt-4 text-sm text-slate-500">Default password: <b>admin123</b></p></Card></div></main>
9
+ }
@@ -0,0 +1,10 @@
1
+
2
+ import { useState } from "react";
3
+ import { Search, Download, Plus, SlidersHorizontal, X } from "lucide-react";
4
+ import { Button, Card, Input, Select, Badge, EmptyState } from "../components/ui";
5
+ const rows=[{id:1,name:'Aline Uwase',team:'Operations',status:'Active',score:96},{id:2,name:'Eric Nshimiyimana',team:'Engineering',status:'Active',score:88},{id:3,name:'Diane Mutoni',team:'Finance',status:'Paused',score:73}];
6
+ export default function Templates(){
7
+ const [modal,setModal]=useState(false),[drawer,setDrawer]=useState(false),[q,setQ]=useState('');
8
+ const filtered=rows.filter(r=>r.name.toLowerCase().includes(q.toLowerCase())||r.team.toLowerCase().includes(q.toLowerCase()));
9
+ return <div className="grid gap-6"><section className="rounded-[2.5rem] bg-white p-8 shadow-sm"><p className="text-xs font-black uppercase tracking-[0.2em] text-[#FF385C]">Component Templates</p><h1 className="mt-3 text-4xl font-black md:text-6xl">Copy-ready building blocks.</h1><p className="mt-4 max-w-2xl text-slate-500">Forms, tables, modals, drawers, filters, badges, empty states, and action bars designed for exam speed and production polish.</p></section><div className="grid gap-6 xl:grid-cols-3"><Card><h2 className="text-xl font-black">Stacked Form</h2><div className="mt-4 grid gap-3"><Input placeholder="Full name"/><Input placeholder="Email address"/><Select><option>Active</option><option>Paused</option></Select><Button><Plus size={16}/> Create record</Button></div></Card><Card><h2 className="text-xl font-black">Filter Bar</h2><div className="mt-4 flex flex-col gap-3"><div className="relative"><Search className="absolute left-4 top-3.5 text-slate-400" size={18}/><input value={q} onChange={e=>setQ(e.target.value)} placeholder="Search..." className="w-full rounded-full border border-slate-200 py-3 pl-11 pr-4 text-sm outline-none focus:border-[#FF385C]"/></div><div className="flex gap-2"><Button variant="secondary" onClick={()=>setDrawer(true)}><SlidersHorizontal size={16}/> Filters</Button><Button variant="secondary"><Download size={16}/> Export</Button></div></div></Card><Card><h2 className="text-xl font-black">Action States</h2><div className="mt-4 flex flex-wrap gap-2"><Badge tone="green">Active</Badge><Badge tone="amber">Pending</Badge><Badge tone="red">Blocked</Badge><Badge>Draft</Badge></div><button onClick={()=>setModal(true)} className="mt-5 rounded-full bg-slate-950 px-5 py-3 text-sm font-black text-white">Open modal</button></Card></div><Card><div className="flex items-center justify-between"><h2 className="text-2xl font-black">Data Table Template</h2><Button onClick={()=>setModal(true)}>New item</Button></div><div className="mt-5 overflow-x-auto"><table className="w-full text-left text-sm"><thead><tr className="border-b text-slate-400"><th className="py-3">Name</th><th>Team</th><th>Status</th><th>Score</th><th>Actions</th></tr></thead><tbody>{filtered.map(r=><tr className="border-b" key={r.id}><td className="py-4 font-black">{r.name}</td><td>{r.team}</td><td><Badge tone={r.status==='Active'?'green':'amber'}>{r.status}</Badge></td><td>{r.score}%</td><td><button className="font-black text-[#FF385C]">Edit</button></td></tr>)}</tbody></table>{!filtered.length&&<div className="mt-5"><EmptyState title="No records found" note="Change the search query or create a new record."/></div>}</div></Card>{modal&&<div className="fixed inset-0 z-50 grid place-items-center bg-slate-950/40 p-4"><Card className="w-full max-w-lg"><div className="flex items-center justify-between"><h2 className="text-2xl font-black">Modal Form</h2><button onClick={()=>setModal(false)}><X/></button></div><div className="mt-5 grid gap-3"><Input placeholder="Record title"/><Input placeholder="Owner"/><Select><option>High priority</option><option>Normal</option></Select><Button onClick={()=>setModal(false)}>Save example</Button></div></Card></div>}{drawer&&<div className="fixed inset-0 z-50 bg-slate-950/30"><aside className="ml-auto h-full w-full max-w-md bg-white p-6 shadow-xl"><div className="flex items-center justify-between"><h2 className="text-2xl font-black">Drawer Filters</h2><button onClick={()=>setDrawer(false)}><X/></button></div><div className="mt-6 grid gap-3"><Select><option>All teams</option><option>Engineering</option></Select><Select><option>Any status</option><option>Active</option></Select><Button onClick={()=>setDrawer(false)}>Apply filters</Button></div></aside></div>}</div>
10
+ }
@@ -0,0 +1,10 @@
1
+
2
+ import { useEffect, useState } from "react";
3
+ import { request } from "../api/http";
4
+ import { Button, Card, Input, Badge } from "../components/ui";
5
+ export default function UserDashboard(){
6
+ const [profile,setProfile]=useState(null),[form,setForm]=useState({currentPassword:'',newPassword:''}),[msg,setMsg]=useState('');
7
+ useEffect(()=>{request('/users/me/profile').then(d=>setProfile(d.profile))},[]);
8
+ async function change(e){e.preventDefault();setMsg('');try{await request('/users/me/password',{method:'PATCH',body:form});setForm({currentPassword:'',newPassword:''});setMsg('Password changed successfully');}catch(err){setMsg(err.message)}}
9
+ return <div className="grid gap-6"><section className="rounded-[2.5rem] bg-white p-8 shadow-sm"><p className="text-xs font-black uppercase tracking-[0.2em] text-[#FF385C]">Employee Portal</p><h1 className="mt-3 text-4xl font-black">Welcome, {profile?.full_name || 'Employee'}.</h1><p className="mt-3 text-slate-500">View your account identity and manage your password.</p></section><div className="grid gap-6 lg:grid-cols-2"><Card><h2 className="text-2xl font-black">My Info</h2>{profile&&<div className="mt-5 grid gap-3 text-sm"><p><b>Name:</b> {profile.full_name}</p><p><b>Email:</b> {profile.email}</p><p><b>Phone:</b> {profile.phone}</p><p><b>Department:</b> {profile.department}</p><p><b>Position:</b> {profile.position}</p><p><b>Status:</b> <Badge tone="green">{profile.status}</Badge></p></div>}</Card><Card><h2 className="text-2xl font-black">Change Password</h2><form onSubmit={change} className="mt-5 grid gap-3"><Input type="password" placeholder="Current password" value={form.currentPassword} onChange={e=>setForm({...form,currentPassword:e.target.value})}/><Input type="password" placeholder="New password" value={form.newPassword} onChange={e=>setForm({...form,newPassword:e.target.value})}/>{msg&&<p className="text-sm font-bold text-slate-500">{msg}</p>}<Button>Update password</Button></form></Card></div></div>
10
+ }
@@ -0,0 +1,24 @@
1
+
2
+ import { useEffect, useState } from "react";
3
+ import Login from "../pages/Login";
4
+ import AdminDashboard from "../pages/AdminDashboard";
5
+ import Employees from "../pages/Employees";
6
+ import Accounts from "../pages/Accounts";
7
+ import Templates from "../pages/Templates";
8
+ import UserDashboard from "../pages/UserDashboard";
9
+ import AppLayout from "../layouts/AppLayout";
10
+ import { request } from "../api/http";
11
+ const path=()=>window.location.pathname;
12
+ export default function AppRouter(){
13
+ const [user,setUser]=useState(null); const [loading,setLoading]=useState(true);
14
+ useEffect(()=>{request('/auth/me').then(d=>setUser(d.user)).catch(()=>setUser(null)).finally(()=>setLoading(false));},[]);
15
+ if(loading) return <div className="grid min-h-screen place-items-center font-black">Loading Nightwing...</div>;
16
+ if(!user) return <Login/>;
17
+ const p=path();
18
+ let Page=user.role==='admin'?AdminDashboard:UserDashboard;
19
+ if(user.role==='admin' && p==='/employees') Page=Employees;
20
+ if(user.role==='admin' && p==='/accounts') Page=Accounts;
21
+ if(user.role==='admin' && p==='/templates') Page=Templates;
22
+ if(user.role==='user') Page=UserDashboard;
23
+ return <AppLayout user={user}><Page user={user}/></AppLayout>;
24
+ }
@@ -0,0 +1 @@
1
+ export default { content:["./index.html","./src/**/*.{js,jsx}"], theme:{ extend:{ fontFamily:{ sans:["Inter","ui-sans-serif","system-ui"] }, colors:{ nightwing:"#FF385C" } } }, plugins:[] };
@@ -0,0 +1,58 @@
1
+
2
+ # API Guide
3
+
4
+ Base URL: `http://localhost:__SERVER_PORT__/api`
5
+
6
+ ## Auth
7
+
8
+ Admin login:
9
+
10
+ ```http
11
+ POST /auth/admin/login
12
+ ```
13
+
14
+ ```json
15
+ { "username": "admin", "password": "admin123" }
16
+ ```
17
+
18
+ User login:
19
+
20
+ ```http
21
+ POST /auth/user/login
22
+ ```
23
+
24
+ ```json
25
+ { "username": "aline", "password": "admin123" }
26
+ ```
27
+
28
+ ## Employees
29
+
30
+ All employee routes require admin session.
31
+
32
+ ```http
33
+ GET /employees
34
+ POST /employees
35
+ PUT /employees/:id
36
+ DELETE /employees/:id
37
+ ```
38
+
39
+ ## Accounts
40
+
41
+ All account-management routes require admin session.
42
+
43
+ ```http
44
+ GET /users
45
+ POST /users
46
+ PUT /users/:id
47
+ PATCH /users/:id/password
48
+ DELETE /users/:id
49
+ ```
50
+
51
+ ## User self-service
52
+
53
+ Requires user session.
54
+
55
+ ```http
56
+ GET /users/me/profile
57
+ PATCH /users/me/password
58
+ ```
@@ -0,0 +1,55 @@
1
+
2
+ # SQL Cheatcode Book
3
+
4
+ This template uses three tables: `admins`, `employees`, and `users`.
5
+
6
+ ## Why this structure exists
7
+
8
+ Admins operate the system. Employees are real people in the organization. Users are login accounts owned by employees.
9
+
10
+ ```sql
11
+ users.employee_id -> employees.id
12
+ ```
13
+
14
+ That means an employee can exist without a login account, but a user account cannot exist without an employee.
15
+
16
+ ## Setup
17
+
18
+ ```bash
19
+ mysql -u root -p < schema.sql
20
+ ```
21
+
22
+ ## Default logins
23
+
24
+ Admin and seeded users use:
25
+
26
+ ```txt
27
+ password: admin123
28
+ ```
29
+
30
+ ## Useful exam queries
31
+
32
+ List accounts with employee names:
33
+
34
+ ```sql
35
+ SELECT u.id, u.username, e.full_name, e.position
36
+ FROM users u
37
+ JOIN employees e ON e.id = u.employee_id;
38
+ ```
39
+
40
+ Find employees without accounts:
41
+
42
+ ```sql
43
+ SELECT e.*
44
+ FROM employees e
45
+ LEFT JOIN users u ON u.employee_id = e.id
46
+ WHERE u.id IS NULL;
47
+ ```
48
+
49
+ Count active employees:
50
+
51
+ ```sql
52
+ SELECT COUNT(*) AS total_active
53
+ FROM employees
54
+ WHERE status = 'Active';
55
+ ```
@@ -0,0 +1,34 @@
1
+
2
+ # Tailwind Cheatcode Book
3
+
4
+ This UI uses an Airbnb-inspired system: white surfaces, soft borders, rounded cards, warm red accent, strong spacing, and readable typography.
5
+
6
+ ## Layout recipe
7
+
8
+ ```jsx
9
+ <div className="rounded-[2rem] border border-slate-200 bg-white p-6 shadow-sm">
10
+ ```
11
+
12
+ Use this for cards, panels, and forms.
13
+
14
+ ## Button recipe
15
+
16
+ ```jsx
17
+ <button className="rounded-full bg-[#FF385C] px-5 py-3 text-sm font-black text-white shadow-sm hover:bg-[#E31C5F]">
18
+ ```
19
+
20
+ ## Inputs
21
+
22
+ ```jsx
23
+ <input className="w-full rounded-2xl border border-slate-200 px-4 py-3 text-sm outline-none focus:border-[#FF385C]" />
24
+ ```
25
+
26
+ ## Responsive grids
27
+
28
+ ```jsx
29
+ <div className="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
30
+ ```
31
+
32
+ ## Rule
33
+
34
+ Keep color minimal. Use the accent only for actions and focus states.
@@ -0,0 +1,39 @@
1
+
2
+ # __PROJECT_NAME__ Instructions
3
+
4
+ ## Start
5
+
6
+ ```bash
7
+ mysql -u root -p < schema.sql
8
+ cd server
9
+ npm install
10
+ npm run dev
11
+ cd ../client
12
+ npm install
13
+ npm run dev
14
+ ```
15
+
16
+ ## Admin Login
17
+
18
+ ```txt
19
+ username: admin
20
+ password: admin123
21
+ ```
22
+
23
+ ## User Login
24
+
25
+ ```txt
26
+ username: aline
27
+ password: admin123
28
+ ```
29
+
30
+ ## What this project gives you
31
+
32
+ - Admin dashboard
33
+ - Employee CRUD
34
+ - Employee-owned user account CRUD
35
+ - User dashboard
36
+ - Change password
37
+ - Component templates page
38
+ - Session auth
39
+ - MySQL schema
@@ -0,0 +1,45 @@
1
+
2
+ DROP DATABASE IF EXISTS __DB_NAME__;
3
+ CREATE DATABASE __DB_NAME__;
4
+ USE __DB_NAME__;
5
+
6
+ CREATE TABLE admins (
7
+ id INT AUTO_INCREMENT PRIMARY KEY,
8
+ username VARCHAR(100) NOT NULL UNIQUE,
9
+ password VARCHAR(255) NOT NULL,
10
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
11
+ );
12
+
13
+ CREATE TABLE employees (
14
+ id INT AUTO_INCREMENT PRIMARY KEY,
15
+ full_name VARCHAR(150) NOT NULL,
16
+ email VARCHAR(150) NOT NULL UNIQUE,
17
+ phone VARCHAR(40) NOT NULL,
18
+ department VARCHAR(100) NOT NULL,
19
+ position VARCHAR(100) NOT NULL,
20
+ status ENUM('Active','On Leave','Inactive') NOT NULL DEFAULT 'Active',
21
+ hired_at DATE NOT NULL,
22
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
23
+ );
24
+
25
+ CREATE TABLE users (
26
+ id INT AUTO_INCREMENT PRIMARY KEY,
27
+ employee_id INT NOT NULL UNIQUE,
28
+ username VARCHAR(100) NOT NULL UNIQUE,
29
+ password VARCHAR(255) NOT NULL,
30
+ status ENUM('Active','Suspended') NOT NULL DEFAULT 'Active',
31
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
32
+ FOREIGN KEY (employee_id) REFERENCES employees(id) ON DELETE CASCADE
33
+ );
34
+
35
+ INSERT INTO admins(username,password) VALUES
36
+ ('admin','$2a$10$t7KDoPGfkg/lQUbCaU0I2.OwJdz7VJHt1XmVECDwSD2S02ULkq5FK');
37
+
38
+ INSERT INTO employees(full_name,email,phone,department,position,status,hired_at) VALUES
39
+ ('Aline Uwase','aline@nightwing.dev','0788000001','Operations','Operations Lead','Active','2025-01-10'),
40
+ ('Eric Nshimiyimana','eric@nightwing.dev','0788000002','Engineering','Frontend Developer','Active','2025-02-12'),
41
+ ('Diane Mutoni','diane@nightwing.dev','0788000003','Finance','Accountant','On Leave','2024-11-03');
42
+
43
+ INSERT INTO users(employee_id,username,password,status) VALUES
44
+ (1,'aline','$2a$10$t7KDoPGfkg/lQUbCaU0I2.OwJdz7VJHt1XmVECDwSD2S02ULkq5FK','Active'),
45
+ (2,'eric','$2a$10$t7KDoPGfkg/lQUbCaU0I2.OwJdz7VJHt1XmVECDwSD2S02ULkq5FK','Active');
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "__PROJECT_NAME__-server",
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "scripts": {
6
+ "dev": "node --watch src/app.js",
7
+ "start": "node src/app.js",
8
+ "check": "node --check src/app.js"
9
+ },
10
+ "dependencies": {
11
+ "bcryptjs": "^2.4.3",
12
+ "cors": "^2.8.5",
13
+ "dotenv": "^16.4.7",
14
+ "express": "^4.21.2",
15
+ "express-session": "^1.18.1",
16
+ "mysql2": "^3.12.0"
17
+ },
18
+ "devDependencies": {}
19
+ }
@@ -0,0 +1,22 @@
1
+
2
+ import express from "express";
3
+ import cors from "cors";
4
+ import session from "express-session";
5
+ import dotenv from "dotenv";
6
+ import authRoutes from "./routes/authRoutes.js";
7
+ import employeeRoutes from "./routes/employeeRoutes.js";
8
+ import userRoutes from "./routes/userRoutes.js";
9
+
10
+ dotenv.config();
11
+ const app=express();
12
+ const PORT=process.env.PORT || __SERVER_PORT__;
13
+ app.use(cors({origin:process.env.CLIENT_URL || "http://localhost:__CLIENT_PORT__",credentials:true}));
14
+ app.use(express.json());
15
+ app.use(session({name:"nightwing.sid",secret:process.env.SESSION_SECRET || "nightwing_dev_secret",resave:false,saveUninitialized:false,cookie:{httpOnly:true,secure:process.env.NODE_ENV==="production",sameSite:process.env.NODE_ENV==="production"?"none":"lax",maxAge:1000*60*60*24}}));
16
+ app.get("/api/health",(req,res)=>res.json({ok:true,app:"__PROJECT_NAME__"}));
17
+ app.use("/api/auth",authRoutes);
18
+ app.use("/api/employees",employeeRoutes);
19
+ app.use("/api/users",userRoutes);
20
+ app.use((req,res)=>res.status(404).json({message:"Route not found"}));
21
+ app.use((err,req,res,next)=>{ console.error(err); res.status(500).json({message:"Server error"}); });
22
+ app.listen(PORT,()=>console.log(`Nightwing server running on http://localhost:${PORT}`));
@@ -0,0 +1,13 @@
1
+
2
+ import mysql from "mysql2/promise";
3
+ import dotenv from "dotenv";
4
+ dotenv.config();
5
+ export const db = mysql.createPool({
6
+ host: process.env.DB_HOST || "localhost",
7
+ user: process.env.DB_USER || "root",
8
+ password: process.env.DB_PASSWORD || "",
9
+ database: process.env.DB_NAME || "__DB_NAME__",
10
+ waitForConnections: true,
11
+ connectionLimit: 10,
12
+ queueLimit: 0
13
+ });
@@ -0,0 +1,48 @@
1
+
2
+ import { db } from "../config/db.js";
3
+ import { comparePassword, hashPassword } from "../utils/security.js";
4
+
5
+ function clean(user){
6
+ return { id:user.id, username:user.username, role:user.role, employeeId:user.employee_id || null, employeeName:user.employeeName || null, status:user.status || null };
7
+ }
8
+
9
+ export async function adminLogin(req,res){
10
+ try{
11
+ const {username,password}=req.body;
12
+ const [rows]=await db.query("SELECT * FROM admins WHERE username=? LIMIT 1",[username]);
13
+ const admin=rows[0];
14
+ if(!admin || !(await comparePassword(password,admin.password))) return res.status(401).json({message:"Invalid admin credentials"});
15
+ req.session.user=clean({...admin,role:"admin",status:"Active"});
16
+ res.json({message:"Logged in",user:req.session.user});
17
+ }catch(error){ console.error(error); res.status(500).json({message:"Admin login failed"}); }
18
+ }
19
+
20
+ export async function userLogin(req,res){
21
+ try{
22
+ const {username,password}=req.body;
23
+ const [rows]=await db.query(`SELECT u.*, e.full_name AS employeeName FROM users u JOIN employees e ON e.id=u.employee_id WHERE u.username=? LIMIT 1`,[username]);
24
+ const user=rows[0];
25
+ if(!user || user.status!=="Active" || !(await comparePassword(password,user.password))) return res.status(401).json({message:"Invalid user credentials"});
26
+ req.session.user=clean({...user,role:"user"});
27
+ res.json({message:"Logged in",user:req.session.user});
28
+ }catch(error){ console.error(error); res.status(500).json({message:"User login failed"}); }
29
+ }
30
+
31
+ export async function registerFirstAdmin(req,res){
32
+ try{
33
+ const [countRows]=await db.query("SELECT COUNT(*) AS total FROM admins");
34
+ if(countRows[0].total>0) return res.status(403).json({message:"Admin registration is locked after setup"});
35
+ const {username,password}=req.body;
36
+ if(!username || !password) return res.status(400).json({message:"Username and password are required"});
37
+ const hashed=await hashPassword(password);
38
+ const [result]=await db.query("INSERT INTO admins(username,password) VALUES(?,?)",[username,hashed]);
39
+ req.session.user={id:result.insertId,username,role:"admin",status:"Active"};
40
+ res.status(201).json({message:"First admin created",user:req.session.user});
41
+ }catch(error){ console.error(error); res.status(500).json({message:"Registration failed"}); }
42
+ }
43
+
44
+ export function me(req,res){
45
+ if(!req.session.user) return res.status(401).json({message:"Not authenticated"});
46
+ res.json({user:req.session.user});
47
+ }
48
+ export function logout(req,res){ req.session.destroy(()=>res.clearCookie("nightwing.sid").json({message:"Logged out"})); }
@@ -0,0 +1,28 @@
1
+
2
+ import { db } from "../config/db.js";
3
+
4
+ export async function getEmployees(req,res){
5
+ const [rows]=await db.query("SELECT * FROM employees ORDER BY id DESC");
6
+ res.json({employees:rows});
7
+ }
8
+ export async function createEmployee(req,res){
9
+ try{
10
+ const {fullName,email,phone,department,position,status="Active",hiredAt}=req.body;
11
+ if(!fullName || !email || !phone || !department || !position || !hiredAt) return res.status(400).json({message:"All employee fields are required"});
12
+ const [result]=await db.query("INSERT INTO employees(full_name,email,phone,department,position,status,hired_at) VALUES(?,?,?,?,?,?,?)",[fullName,email,phone,department,position,status,hiredAt]);
13
+ res.status(201).json({message:"Employee created",id:result.insertId});
14
+ }catch(error){ console.error(error); res.status(500).json({message:error.code==="ER_DUP_ENTRY"?"Employee email already exists":"Failed to create employee"}); }
15
+ }
16
+ export async function updateEmployee(req,res){
17
+ try{
18
+ const {fullName,email,phone,department,position,status,hiredAt}=req.body;
19
+ const [result]=await db.query("UPDATE employees SET full_name=?,email=?,phone=?,department=?,position=?,status=?,hired_at=? WHERE id=?",[fullName,email,phone,department,position,status,hiredAt,req.params.id]);
20
+ if(!result.affectedRows) return res.status(404).json({message:"Employee not found"});
21
+ res.json({message:"Employee updated"});
22
+ }catch(error){ console.error(error); res.status(500).json({message:"Failed to update employee"}); }
23
+ }
24
+ export async function deleteEmployee(req,res){
25
+ const [result]=await db.query("DELETE FROM employees WHERE id=?",[req.params.id]);
26
+ if(!result.affectedRows) return res.status(404).json({message:"Employee not found"});
27
+ res.json({message:"Employee deleted"});
28
+ }
@@ -0,0 +1,52 @@
1
+
2
+ import { db } from "../config/db.js";
3
+ import { comparePassword, hashPassword } from "../utils/security.js";
4
+
5
+ export async function getUsers(req,res){
6
+ const [rows]=await db.query(`SELECT u.id,u.username,u.status,u.created_at,u.employee_id,e.full_name AS employeeName,e.email,e.position FROM users u JOIN employees e ON e.id=u.employee_id ORDER BY u.id DESC`);
7
+ res.json({users:rows});
8
+ }
9
+ export async function createUser(req,res){
10
+ try{
11
+ const {employeeId,username,password,status="Active"}=req.body;
12
+ if(!employeeId || !username || !password) return res.status(400).json({message:"Employee, username and password are required"});
13
+ const hashed=await hashPassword(password);
14
+ const [result]=await db.query("INSERT INTO users(employee_id,username,password,status) VALUES(?,?,?,?)",[employeeId,username,hashed,status]);
15
+ res.status(201).json({message:"User account created",id:result.insertId});
16
+ }catch(error){ console.error(error); res.status(500).json({message:error.code==="ER_DUP_ENTRY"?"Employee already has an account or username exists":"Failed to create account"}); }
17
+ }
18
+ export async function updateUser(req,res){
19
+ try{
20
+ const {username,status}=req.body;
21
+ const [result]=await db.query("UPDATE users SET username=?, status=? WHERE id=?",[username,status,req.params.id]);
22
+ if(!result.affectedRows) return res.status(404).json({message:"User not found"});
23
+ res.json({message:"User updated"});
24
+ }catch(error){ console.error(error); res.status(500).json({message:"Failed to update user"}); }
25
+ }
26
+ export async function resetPassword(req,res){
27
+ const {password}=req.body;
28
+ if(!password || password.length<6) return res.status(400).json({message:"Password must be at least 6 characters"});
29
+ const hashed=await hashPassword(password);
30
+ const [result]=await db.query("UPDATE users SET password=? WHERE id=?",[hashed,req.params.id]);
31
+ if(!result.affectedRows) return res.status(404).json({message:"User not found"});
32
+ res.json({message:"Password reset"});
33
+ }
34
+ export async function deleteUser(req,res){
35
+ const [result]=await db.query("DELETE FROM users WHERE id=?",[req.params.id]);
36
+ if(!result.affectedRows) return res.status(404).json({message:"User not found"});
37
+ res.json({message:"User deleted"});
38
+ }
39
+ export async function myProfile(req,res){
40
+ const [rows]=await db.query(`SELECT u.id,u.username,u.status,u.created_at,e.full_name,e.email,e.phone,e.department,e.position,e.hired_at FROM users u JOIN employees e ON e.id=u.employee_id WHERE u.id=? LIMIT 1`,[req.session.user.id]);
41
+ res.json({profile:rows[0]});
42
+ }
43
+ export async function changeMyPassword(req,res){
44
+ const {currentPassword,newPassword}=req.body;
45
+ if(!currentPassword || !newPassword) return res.status(400).json({message:"Both passwords are required"});
46
+ if(newPassword.length<6) return res.status(400).json({message:"New password must be at least 6 characters"});
47
+ const [rows]=await db.query("SELECT password FROM users WHERE id=? LIMIT 1",[req.session.user.id]);
48
+ const ok=rows[0] && await comparePassword(currentPassword,rows[0].password);
49
+ if(!ok) return res.status(401).json({message:"Current password is incorrect"});
50
+ await db.query("UPDATE users SET password=? WHERE id=?",[await hashPassword(newPassword),req.session.user.id]);
51
+ res.json({message:"Password changed"});
52
+ }
@@ -0,0 +1,12 @@
1
+
2
+ export function requireAuth(req,res,next){
3
+ if(!req.session.user) return res.status(401).json({message:"Not authenticated"});
4
+ next();
5
+ }
6
+ export function requireRole(role){
7
+ return (req,res,next)=>{
8
+ if(!req.session.user) return res.status(401).json({message:"Not authenticated"});
9
+ if(req.session.user.role!==role) return res.status(403).json({message:"Forbidden"});
10
+ next();
11
+ };
12
+ }
@@ -0,0 +1,9 @@
1
+ import { Router } from "express";
2
+ import { adminLogin,userLogin,registerFirstAdmin,me,logout } from "../controllers/authController.js";
3
+ const router=Router();
4
+ router.post("/setup/register",registerFirstAdmin);
5
+ router.post("/admin/login",adminLogin);
6
+ router.post("/user/login",userLogin);
7
+ router.get("/me",me);
8
+ router.post("/logout",logout);
9
+ export default router;
@@ -0,0 +1,10 @@
1
+ import { Router } from "express";
2
+ import { createEmployee,deleteEmployee,getEmployees,updateEmployee } from "../controllers/employeeController.js";
3
+ import { requireRole } from "../middleware/auth.js";
4
+ const router=Router();
5
+ router.use(requireRole("admin"));
6
+ router.get("/",getEmployees);
7
+ router.post("/",createEmployee);
8
+ router.put("/:id",updateEmployee);
9
+ router.delete("/:id",deleteEmployee);
10
+ export default router;
@@ -0,0 +1,12 @@
1
+ import { Router } from "express";
2
+ import { changeMyPassword,createUser,deleteUser,getUsers,myProfile,resetPassword,updateUser } from "../controllers/userController.js";
3
+ import { requireRole } from "../middleware/auth.js";
4
+ const router=Router();
5
+ router.get("/",requireRole("admin"),getUsers);
6
+ router.post("/",requireRole("admin"),createUser);
7
+ router.put("/:id",requireRole("admin"),updateUser);
8
+ router.patch("/:id/password",requireRole("admin"),resetPassword);
9
+ router.delete("/:id",requireRole("admin"),deleteUser);
10
+ router.get("/me/profile",requireRole("user"),myProfile);
11
+ router.patch("/me/password",requireRole("user"),changeMyPassword);
12
+ export default router;
@@ -0,0 +1,3 @@
1
+ import bcrypt from "bcryptjs";
2
+ export const hashPassword=(password)=>bcrypt.hash(String(password),10);
3
+ export const comparePassword=(password,hash)=>bcrypt.compare(String(password),hash);