@organizasyon/meeting-nanaman-app-backend 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.
- package/dist/controllers/auth/index.d.ts +65 -0
- package/dist/controllers/auth/index.js +525 -0
- package/dist/controllers/employees/index.d.ts +38 -0
- package/dist/controllers/employees/index.js +185 -0
- package/dist/controllers/health/index.d.ts +9 -0
- package/dist/controllers/health/index.js +42 -0
- package/dist/controllers/index.d.ts +16 -0
- package/dist/controllers/index.js +19 -0
- package/dist/controllers/meetings/index.d.ts +23 -0
- package/dist/controllers/meetings/index.js +233 -0
- package/dist/controllers/modules/index.d.ts +5 -0
- package/dist/controllers/modules/index.js +104 -0
- package/dist/controllers/users/index.d.ts +103 -0
- package/dist/controllers/users/index.js +841 -0
- package/dist/data/modules.json +94 -0
- package/dist/database/config/index.d.ts +2 -0
- package/dist/database/config/index.js +32 -0
- package/dist/database/index.d.ts +9 -0
- package/dist/database/index.js +9 -0
- package/dist/database/seeder/employees/index.d.ts +1 -0
- package/dist/database/seeder/employees/index.js +40 -0
- package/dist/database/seeder/index.d.ts +4 -0
- package/dist/database/seeder/index.js +20 -0
- package/dist/database/seeder/users/index.d.ts +1 -0
- package/dist/database/seeder/users/index.js +46 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +23 -0
- package/dist/jobs/index.d.ts +1 -0
- package/dist/jobs/index.js +18 -0
- package/dist/jobs/mailer/index.d.ts +4 -0
- package/dist/jobs/mailer/index.js +186 -0
- package/dist/jobs/mailer/templates/auth.d.ts +11 -0
- package/dist/jobs/mailer/templates/auth.js +117 -0
- package/dist/jobs/mailer/templates/index.d.ts +1 -0
- package/dist/jobs/mailer/templates/index.js +17 -0
- package/dist/jobs/queues/index.d.ts +3 -0
- package/dist/jobs/queues/index.js +115 -0
- package/dist/middlewares/audit/index.d.ts +0 -0
- package/dist/middlewares/audit/index.js +1 -0
- package/dist/middlewares/guard/index.d.ts +11 -0
- package/dist/middlewares/guard/index.js +53 -0
- package/dist/middlewares/index.d.ts +2 -0
- package/dist/middlewares/index.js +7 -0
- package/dist/middlewares/meeting.d.ts +9 -0
- package/dist/middlewares/meeting.js +34 -0
- package/dist/models/employees/index.d.ts +83 -0
- package/dist/models/employees/index.js +70 -0
- package/dist/models/index.d.ts +570 -0
- package/dist/models/index.js +17 -0
- package/dist/models/meetings/index.d.ts +227 -0
- package/dist/models/meetings/index.js +112 -0
- package/dist/models/passkeys/index.d.ts +77 -0
- package/dist/models/passkeys/index.js +55 -0
- package/dist/models/queues/index.d.ts +77 -0
- package/dist/models/queues/index.js +57 -0
- package/dist/models/users/index.d.ts +107 -0
- package/dist/models/users/index.js +92 -0
- package/dist/queues/index.d.ts +1 -0
- package/dist/queues/index.js +17 -0
- package/dist/queues/mailer/index.d.ts +4 -0
- package/dist/queues/mailer/index.js +74 -0
- package/dist/types/index.d.ts +33 -0
- package/dist/types/index.js +2 -0
- package/dist/utils/notifications.d.ts +2 -0
- package/dist/utils/notifications.js +51 -0
- package/package.json +39 -0
- package/public/health.html +215 -0
- package/src/controllers/auth/index.ts +609 -0
- package/src/controllers/employees/index.ts +210 -0
- package/src/controllers/health/index.ts +41 -0
- package/src/controllers/index.ts +9 -0
- package/src/controllers/meetings/index.ts +251 -0
- package/src/controllers/modules/index.ts +74 -0
- package/src/controllers/users/index.ts +981 -0
- package/src/data/modules.json +94 -0
- package/src/database/config/index.ts +26 -0
- package/src/database/index.ts +5 -0
- package/src/database/seeder/employees/index.ts +35 -0
- package/src/database/seeder/index.ts +18 -0
- package/src/database/seeder/users/index.ts +44 -0
- package/src/index.ts +10 -0
- package/src/jobs/index.ts +2 -0
- package/src/jobs/mailer/index.ts +154 -0
- package/src/jobs/mailer/templates/auth.ts +113 -0
- package/src/jobs/mailer/templates/index.ts +1 -0
- package/src/jobs/queues/index.ts +125 -0
- package/src/middlewares/audit/index.ts +0 -0
- package/src/middlewares/guard/index.ts +64 -0
- package/src/middlewares/index.ts +5 -0
- package/src/middlewares/meeting.ts +45 -0
- package/src/models/employees/index.ts +70 -0
- package/src/models/index.ts +8 -0
- package/src/models/meetings/index.ts +112 -0
- package/src/models/passkeys/index.ts +53 -0
- package/src/models/queues/index.ts +55 -0
- package/src/models/users/index.ts +92 -0
- package/src/queues/index.ts +1 -0
- package/src/queues/mailer/index.ts +80 -0
- package/src/types/index.ts +38 -0
- package/src/utils/notifications.ts +66 -0
- package/tsconfig.json +18 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { Request, Response, NextFunction } from 'express';
|
|
2
|
+
import jwt from 'jsonwebtoken';
|
|
3
|
+
|
|
4
|
+
interface AuthenticatedRequest extends Request {
|
|
5
|
+
user?: {
|
|
6
|
+
userId: string;
|
|
7
|
+
email: string;
|
|
8
|
+
role: string;
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const authenticateToken = (req: AuthenticatedRequest, res: Response, next: NextFunction): void => {
|
|
13
|
+
const authHeader = req.headers['authorization'];
|
|
14
|
+
const token = authHeader && authHeader.split(' ')[1]; // Bearer TOKEN
|
|
15
|
+
|
|
16
|
+
if (!token) {
|
|
17
|
+
res.status(401).json({ message: 'Access token required' });
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const jwtSecret = process.env.JWT_SECRET;
|
|
22
|
+
if (!jwtSecret) {
|
|
23
|
+
console.error('JWT_SECRET environment variable is required');
|
|
24
|
+
res.status(500).json({ message: 'Server configuration error' });
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
jwt.verify(token, jwtSecret, (err: any, decoded: any) => {
|
|
29
|
+
if (err) {
|
|
30
|
+
if (err.name === 'TokenExpiredError') {
|
|
31
|
+
res.status(401).json({ message: 'Token expired' });
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
res.status(403).json({ message: 'Invalid token' });
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
req.user = {
|
|
39
|
+
userId: decoded.userId,
|
|
40
|
+
email: decoded.email,
|
|
41
|
+
role: decoded.role
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
next();
|
|
45
|
+
});
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export const requireRole = (roles: string[]) => {
|
|
49
|
+
return (req: AuthenticatedRequest, res: Response, next: NextFunction): void => {
|
|
50
|
+
if (!req.user) {
|
|
51
|
+
res.status(401).json({ message: 'Authentication required' });
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (!roles.includes(req.user.role)) {
|
|
56
|
+
res.status(403).json({ message: 'Insufficient permissions' });
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
next();
|
|
61
|
+
};
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export default authenticateToken;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { Request, Response, NextFunction } from 'express';
|
|
2
|
+
import { Users as UserModel, Meetings as MeetingModel } from '../models';
|
|
3
|
+
|
|
4
|
+
export const canJoinMeeting = async (req: Request, res: Response, next: NextFunction) => {
|
|
5
|
+
try {
|
|
6
|
+
const { meetingId } = req.params;
|
|
7
|
+
const userId = (req as any).user?._id;
|
|
8
|
+
|
|
9
|
+
const meeting = await MeetingModel.findById(meetingId);
|
|
10
|
+
if (!meeting) {
|
|
11
|
+
return res.status(404).json({ error: 'Meeting not found' });
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (!meeting.is_active) {
|
|
15
|
+
return res.status(403).json({ error: 'Meeting is not active' });
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Check if user is host or participant
|
|
19
|
+
const isHost = meeting.host_id.toString() === userId;
|
|
20
|
+
const isParticipant = meeting.participants.some((p: any) => p.user_id.toString() === userId);
|
|
21
|
+
|
|
22
|
+
if (!isHost && !isParticipant) {
|
|
23
|
+
return res.status(403).json({ error: 'You are not authorized to join this meeting' });
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Check meeting capacity
|
|
27
|
+
if ((meeting as any).participant_count >= meeting.max_participants && !isHost) {
|
|
28
|
+
return res.status(403).json({ error: 'Meeting is full' });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
req.meeting = meeting;
|
|
32
|
+
next();
|
|
33
|
+
} catch (error) {
|
|
34
|
+
console.error('Error in canJoinMeeting middleware:', error);
|
|
35
|
+
res.status(500).json({ error: 'Internal server error' });
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
declare global {
|
|
40
|
+
namespace Express {
|
|
41
|
+
interface Request {
|
|
42
|
+
meeting?: any;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import mongoose from 'mongoose';
|
|
2
|
+
|
|
3
|
+
const employeeSchema = new mongoose.Schema({
|
|
4
|
+
first_name: {
|
|
5
|
+
type: String,
|
|
6
|
+
required: true,
|
|
7
|
+
trim: true,
|
|
8
|
+
},
|
|
9
|
+
middle_name: {
|
|
10
|
+
type: String,
|
|
11
|
+
trim: true,
|
|
12
|
+
},
|
|
13
|
+
last_name: {
|
|
14
|
+
type: String,
|
|
15
|
+
required: true,
|
|
16
|
+
trim: true,
|
|
17
|
+
},
|
|
18
|
+
address: {
|
|
19
|
+
type: String,
|
|
20
|
+
trim: true,
|
|
21
|
+
},
|
|
22
|
+
contact_number: {
|
|
23
|
+
type: String,
|
|
24
|
+
trim: true,
|
|
25
|
+
},
|
|
26
|
+
email: {
|
|
27
|
+
type: String,
|
|
28
|
+
trim: true,
|
|
29
|
+
index: true,
|
|
30
|
+
},
|
|
31
|
+
position: {
|
|
32
|
+
type: String,
|
|
33
|
+
trim: true,
|
|
34
|
+
},
|
|
35
|
+
department: {
|
|
36
|
+
type: String,
|
|
37
|
+
trim: true,
|
|
38
|
+
},
|
|
39
|
+
created_at: {
|
|
40
|
+
type: Number,
|
|
41
|
+
default: () => Date.now(),
|
|
42
|
+
},
|
|
43
|
+
updated_at: {
|
|
44
|
+
type: Number,
|
|
45
|
+
default: () => Date.now(),
|
|
46
|
+
},
|
|
47
|
+
deleted_at: {
|
|
48
|
+
type: Number,
|
|
49
|
+
default: null,
|
|
50
|
+
index: true,
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// Virtual for full name
|
|
55
|
+
employeeSchema.virtual('full_name').get(function() {
|
|
56
|
+
let fullName = `${this.first_name} ${this.last_name}`;
|
|
57
|
+
if (this.middle_name) {
|
|
58
|
+
fullName = `${this.first_name} ${this.middle_name} ${this.last_name}`;
|
|
59
|
+
}
|
|
60
|
+
return fullName;
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// Ensure virtual fields are included in JSON output
|
|
64
|
+
employeeSchema.set('toJSON', { virtuals: true });
|
|
65
|
+
employeeSchema.set('toObject', { virtuals: true });
|
|
66
|
+
|
|
67
|
+
// Compound indexes for better query performance
|
|
68
|
+
employeeSchema.index({ first_name: 1, last_name: 1 });
|
|
69
|
+
|
|
70
|
+
export default mongoose.model('_employees', employeeSchema);
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import Employees from './employees';
|
|
2
|
+
import Users from './users';
|
|
3
|
+
import Passkeys from './passkeys';
|
|
4
|
+
import Queues from './queues';
|
|
5
|
+
import Meetings from './meetings';
|
|
6
|
+
|
|
7
|
+
export { Employees, Users, Passkeys, Queues, Meetings };
|
|
8
|
+
export default { Employees, Users, Passkeys, Queues, Meetings };
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import mongoose from 'mongoose';
|
|
2
|
+
|
|
3
|
+
const meetingSchema = new mongoose.Schema({
|
|
4
|
+
title: {
|
|
5
|
+
type: String,
|
|
6
|
+
required: true,
|
|
7
|
+
trim: true,
|
|
8
|
+
},
|
|
9
|
+
description: {
|
|
10
|
+
type: String,
|
|
11
|
+
trim: true,
|
|
12
|
+
},
|
|
13
|
+
host_id: {
|
|
14
|
+
type: mongoose.Schema.Types.ObjectId,
|
|
15
|
+
ref: '_users',
|
|
16
|
+
required: true,
|
|
17
|
+
index: true,
|
|
18
|
+
},
|
|
19
|
+
participants: [{
|
|
20
|
+
user_id: {
|
|
21
|
+
type: mongoose.Schema.Types.ObjectId,
|
|
22
|
+
ref: '_users',
|
|
23
|
+
required: false,
|
|
24
|
+
},
|
|
25
|
+
email: {
|
|
26
|
+
type: String,
|
|
27
|
+
required: false,
|
|
28
|
+
},
|
|
29
|
+
joined_at: {
|
|
30
|
+
type: Number,
|
|
31
|
+
default: null,
|
|
32
|
+
},
|
|
33
|
+
left_at: {
|
|
34
|
+
type: Number,
|
|
35
|
+
default: null,
|
|
36
|
+
},
|
|
37
|
+
is_active: {
|
|
38
|
+
type: Boolean,
|
|
39
|
+
default: false,
|
|
40
|
+
},
|
|
41
|
+
status: {
|
|
42
|
+
type: String,
|
|
43
|
+
enum: ['pending', 'accepted'],
|
|
44
|
+
default: 'accepted',
|
|
45
|
+
},
|
|
46
|
+
}],
|
|
47
|
+
scheduled_at: {
|
|
48
|
+
type: Number,
|
|
49
|
+
default: () => Date.now(),
|
|
50
|
+
},
|
|
51
|
+
started_at: {
|
|
52
|
+
type: Number,
|
|
53
|
+
default: null,
|
|
54
|
+
},
|
|
55
|
+
ended_at: {
|
|
56
|
+
type: Number,
|
|
57
|
+
default: null,
|
|
58
|
+
},
|
|
59
|
+
meeting_type: {
|
|
60
|
+
type: String,
|
|
61
|
+
enum: ['video', 'audio'],
|
|
62
|
+
default: 'video',
|
|
63
|
+
},
|
|
64
|
+
meeting_url: {
|
|
65
|
+
type: String,
|
|
66
|
+
unique: true,
|
|
67
|
+
required: true,
|
|
68
|
+
},
|
|
69
|
+
max_participants: {
|
|
70
|
+
type: Number,
|
|
71
|
+
default: 50,
|
|
72
|
+
},
|
|
73
|
+
is_active: {
|
|
74
|
+
type: Boolean,
|
|
75
|
+
default: true,
|
|
76
|
+
},
|
|
77
|
+
created_at: {
|
|
78
|
+
type: Number,
|
|
79
|
+
default: () => Date.now(),
|
|
80
|
+
},
|
|
81
|
+
updated_at: {
|
|
82
|
+
type: Number,
|
|
83
|
+
default: () => Date.now(),
|
|
84
|
+
},
|
|
85
|
+
deleted_at: {
|
|
86
|
+
type: Number,
|
|
87
|
+
default: null,
|
|
88
|
+
index: true,
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// Virtual for participant count
|
|
93
|
+
meetingSchema.virtual('participant_count').get(function() {
|
|
94
|
+
return this.participants.filter(p => p.is_active).length;
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// Virtual for duration (in minutes)
|
|
98
|
+
meetingSchema.virtual('duration_minutes').get(function() {
|
|
99
|
+
if (!this.started_at) return 0;
|
|
100
|
+
const end = this.ended_at || Date.now();
|
|
101
|
+
return Math.floor((end - this.started_at) / (1000 * 60));
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// Ensure virtual fields are included in JSON output
|
|
105
|
+
meetingSchema.set('toJSON', { virtuals: true });
|
|
106
|
+
meetingSchema.set('toObject', { virtuals: true });
|
|
107
|
+
|
|
108
|
+
// Indexes for better query performance
|
|
109
|
+
meetingSchema.index({ host_id: 1, scheduled_at: -1 });
|
|
110
|
+
meetingSchema.index({ 'participants.user_id': 1 });
|
|
111
|
+
|
|
112
|
+
export default mongoose.model('_meetings', meetingSchema);
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import mongoose from 'mongoose';
|
|
2
|
+
|
|
3
|
+
const passkeySchema = new mongoose.Schema({
|
|
4
|
+
user_id: {
|
|
5
|
+
type: mongoose.Schema.Types.ObjectId,
|
|
6
|
+
ref: '_users',
|
|
7
|
+
required: true,
|
|
8
|
+
index: true,
|
|
9
|
+
},
|
|
10
|
+
credential_id: {
|
|
11
|
+
type: String,
|
|
12
|
+
required: true,
|
|
13
|
+
unique: true,
|
|
14
|
+
},
|
|
15
|
+
device_id: {
|
|
16
|
+
type: String,
|
|
17
|
+
required: true,
|
|
18
|
+
},
|
|
19
|
+
name: {
|
|
20
|
+
type: String,
|
|
21
|
+
required: true,
|
|
22
|
+
},
|
|
23
|
+
public_key: {
|
|
24
|
+
type: String,
|
|
25
|
+
required: true,
|
|
26
|
+
},
|
|
27
|
+
created_at: {
|
|
28
|
+
type: Number,
|
|
29
|
+
default: () => Date.now(),
|
|
30
|
+
},
|
|
31
|
+
updated_at: {
|
|
32
|
+
type: Number,
|
|
33
|
+
default: () => Date.now(),
|
|
34
|
+
},
|
|
35
|
+
last_used_at: {
|
|
36
|
+
type: Number,
|
|
37
|
+
default: null,
|
|
38
|
+
},
|
|
39
|
+
is_active: {
|
|
40
|
+
type: Boolean,
|
|
41
|
+
default: true,
|
|
42
|
+
},
|
|
43
|
+
deleted_at: {
|
|
44
|
+
type: Number,
|
|
45
|
+
default: null,
|
|
46
|
+
index: true,
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// Compound indexes for better query performance
|
|
51
|
+
passkeySchema.index({ user_id: 1, credential_id: 1 });
|
|
52
|
+
|
|
53
|
+
export default mongoose.model('_passkeys', passkeySchema);
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import mongoose from 'mongoose';
|
|
2
|
+
|
|
3
|
+
const queueSchema = new mongoose.Schema({
|
|
4
|
+
type: {
|
|
5
|
+
type: String,
|
|
6
|
+
required: true,
|
|
7
|
+
enum: ['email', 'password_reset', 'passkey'],
|
|
8
|
+
index: true,
|
|
9
|
+
},
|
|
10
|
+
data: {
|
|
11
|
+
type: mongoose.Schema.Types.Mixed,
|
|
12
|
+
required: true,
|
|
13
|
+
},
|
|
14
|
+
status: {
|
|
15
|
+
type: String,
|
|
16
|
+
required: true,
|
|
17
|
+
enum: ['pending', 'processing', 'completed', 'failed'],
|
|
18
|
+
default: 'pending',
|
|
19
|
+
index: true,
|
|
20
|
+
},
|
|
21
|
+
attempts: {
|
|
22
|
+
type: Number,
|
|
23
|
+
default: 0,
|
|
24
|
+
},
|
|
25
|
+
max_attempts: {
|
|
26
|
+
type: Number,
|
|
27
|
+
default: 3,
|
|
28
|
+
},
|
|
29
|
+
error_message: {
|
|
30
|
+
type: String,
|
|
31
|
+
default: null,
|
|
32
|
+
},
|
|
33
|
+
created_at: {
|
|
34
|
+
type: Number,
|
|
35
|
+
default: () => Date.now(),
|
|
36
|
+
},
|
|
37
|
+
updated_at: {
|
|
38
|
+
type: Number,
|
|
39
|
+
default: () => Date.now(),
|
|
40
|
+
},
|
|
41
|
+
processed_at: {
|
|
42
|
+
type: Number,
|
|
43
|
+
default: null,
|
|
44
|
+
},
|
|
45
|
+
deleted_at: {
|
|
46
|
+
type: Number,
|
|
47
|
+
default: null,
|
|
48
|
+
index: true,
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// Compound indexes for better query performance
|
|
53
|
+
queueSchema.index({ type: 1, status: 1, created_at: -1 });
|
|
54
|
+
|
|
55
|
+
export default mongoose.model('_queues', queueSchema);
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import mongoose from 'mongoose';
|
|
2
|
+
|
|
3
|
+
const userSchema = new mongoose.Schema({
|
|
4
|
+
email: {
|
|
5
|
+
type: String,
|
|
6
|
+
required: true,
|
|
7
|
+
unique: true,
|
|
8
|
+
trim: true,
|
|
9
|
+
lowercase: true,
|
|
10
|
+
index: true,
|
|
11
|
+
},
|
|
12
|
+
password: {
|
|
13
|
+
type: String,
|
|
14
|
+
required: true,
|
|
15
|
+
},
|
|
16
|
+
first_name: {
|
|
17
|
+
type: String,
|
|
18
|
+
required: true,
|
|
19
|
+
trim: true,
|
|
20
|
+
},
|
|
21
|
+
middle_name: {
|
|
22
|
+
type: String,
|
|
23
|
+
trim: true,
|
|
24
|
+
},
|
|
25
|
+
last_name: {
|
|
26
|
+
type: String,
|
|
27
|
+
required: true,
|
|
28
|
+
trim: true,
|
|
29
|
+
},
|
|
30
|
+
role: {
|
|
31
|
+
type: String,
|
|
32
|
+
enum: ['admin', 'manager', 'employee'],
|
|
33
|
+
default: 'employee',
|
|
34
|
+
},
|
|
35
|
+
is_active: {
|
|
36
|
+
type: Boolean,
|
|
37
|
+
default: true,
|
|
38
|
+
},
|
|
39
|
+
employee_id: {
|
|
40
|
+
type: mongoose.Schema.Types.ObjectId,
|
|
41
|
+
ref: '_employees',
|
|
42
|
+
default: null,
|
|
43
|
+
index: true,
|
|
44
|
+
},
|
|
45
|
+
created_at: {
|
|
46
|
+
type: Number,
|
|
47
|
+
default: () => Date.now(),
|
|
48
|
+
},
|
|
49
|
+
updated_at: {
|
|
50
|
+
type: Number,
|
|
51
|
+
default: () => Date.now(),
|
|
52
|
+
},
|
|
53
|
+
reset_token: {
|
|
54
|
+
type: String,
|
|
55
|
+
default: null,
|
|
56
|
+
},
|
|
57
|
+
reset_token_expires: {
|
|
58
|
+
type: Number,
|
|
59
|
+
default: null,
|
|
60
|
+
},
|
|
61
|
+
deleted_at: {
|
|
62
|
+
type: Number,
|
|
63
|
+
default: null,
|
|
64
|
+
index: true,
|
|
65
|
+
},
|
|
66
|
+
fcm_token: {
|
|
67
|
+
type: String,
|
|
68
|
+
default: null,
|
|
69
|
+
},
|
|
70
|
+
has_passkey: {
|
|
71
|
+
type: Boolean,
|
|
72
|
+
default: false,
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// Virtual for full name
|
|
77
|
+
userSchema.virtual('full_name').get(function() {
|
|
78
|
+
let fullName = `${this.first_name} ${this.last_name}`;
|
|
79
|
+
if (this.middle_name) {
|
|
80
|
+
fullName = `${this.first_name} ${this.middle_name} ${this.last_name}`;
|
|
81
|
+
}
|
|
82
|
+
return fullName;
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// Ensure virtual fields are included in JSON output
|
|
86
|
+
userSchema.set('toJSON', { virtuals: true });
|
|
87
|
+
userSchema.set('toObject', { virtuals: true });
|
|
88
|
+
|
|
89
|
+
// Additional compound indexes for better query performance
|
|
90
|
+
userSchema.index({ first_name: 1, last_name: 1 });
|
|
91
|
+
|
|
92
|
+
export default mongoose.model('_users', userSchema);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './mailer';
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { Queue, Worker } from 'bullmq';
|
|
2
|
+
import { Redis } from 'ioredis';
|
|
3
|
+
import { sendPasswordResetEmail, sendPasswordChangedEmail, sendPasskeyRegisteredEmail } from '../../jobs/mailer';
|
|
4
|
+
|
|
5
|
+
// Redis connection
|
|
6
|
+
const redisConnection = new Redis({
|
|
7
|
+
host: process.env.REDIS_HOST || 'localhost',
|
|
8
|
+
port: Number(process.env.REDIS_PORT) || 6379,
|
|
9
|
+
password: process.env.REDIS_PASSWORD || (process.env.NODE_ENV === 'production' ? undefined : ''),
|
|
10
|
+
maxRetriesPerRequest: null, // Required for BullMQ
|
|
11
|
+
lazyConnect: true,
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
// Email queue - create only when needed to avoid connection issues
|
|
15
|
+
let emailQueue: Queue | null = null;
|
|
16
|
+
|
|
17
|
+
const getEmailQueue = () => {
|
|
18
|
+
if (!emailQueue) {
|
|
19
|
+
emailQueue = new Queue('email', {
|
|
20
|
+
connection: redisConnection,
|
|
21
|
+
defaultJobOptions: {
|
|
22
|
+
removeOnComplete: 50,
|
|
23
|
+
removeOnFail: 100,
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
return emailQueue;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// Worker will be created only when explicitly started
|
|
31
|
+
let emailWorker: Worker | null = null;
|
|
32
|
+
|
|
33
|
+
export const startEmailWorker = () => {
|
|
34
|
+
if (emailWorker) return emailWorker;
|
|
35
|
+
|
|
36
|
+
emailWorker = new Worker('email', async (job) => {
|
|
37
|
+
const { type, data } = job.data;
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
switch (type) {
|
|
41
|
+
case 'password_reset':
|
|
42
|
+
await sendPasswordResetEmail(data.email, data.name, data.reset_token);
|
|
43
|
+
break;
|
|
44
|
+
case 'password_changed':
|
|
45
|
+
await sendPasswordChangedEmail(data.email, data.name);
|
|
46
|
+
break;
|
|
47
|
+
case 'passkey':
|
|
48
|
+
await sendPasskeyRegisteredEmail(data.email, data.name, data.device_name);
|
|
49
|
+
break;
|
|
50
|
+
default:
|
|
51
|
+
throw new Error(`Unknown email type: ${type}`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
console.log(`Email job ${job.id} processed successfully`);
|
|
55
|
+
} catch (error) {
|
|
56
|
+
console.error(`Email job ${job.id} failed:`, error);
|
|
57
|
+
throw error;
|
|
58
|
+
}
|
|
59
|
+
}, {
|
|
60
|
+
connection: redisConnection,
|
|
61
|
+
concurrency: 5, // Process up to 5 emails concurrently
|
|
62
|
+
limiter: {
|
|
63
|
+
max: 10, // Max 10 jobs per duration
|
|
64
|
+
duration: 1000, // Per 1 second
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// Event listeners
|
|
69
|
+
emailWorker.on('completed', (job) => {
|
|
70
|
+
console.log(`Email job ${job.id} completed`);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
emailWorker.on('failed', (job, err) => {
|
|
74
|
+
console.error(`Email job ${job?.id} failed:`, err);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
return emailWorker;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export { getEmailQueue };
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Request } from 'express'
|
|
2
|
+
import multer from 'multer'
|
|
3
|
+
|
|
4
|
+
export interface MulterRequest extends Request {
|
|
5
|
+
file?: Express.Multer.File
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface User {
|
|
9
|
+
id: string;
|
|
10
|
+
username: string;
|
|
11
|
+
email: string;
|
|
12
|
+
first_name: string;
|
|
13
|
+
middle_name?: string;
|
|
14
|
+
last_name: string;
|
|
15
|
+
role: 'admin' | 'manager' | 'employee';
|
|
16
|
+
employee_id?: string;
|
|
17
|
+
is_active: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface Employee {
|
|
21
|
+
id: string;
|
|
22
|
+
first_name: string;
|
|
23
|
+
middle_name?: string;
|
|
24
|
+
last_name: string;
|
|
25
|
+
email?: string;
|
|
26
|
+
contact_number?: string;
|
|
27
|
+
position?: string;
|
|
28
|
+
department?: string;
|
|
29
|
+
address?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface Module {
|
|
33
|
+
id: string;
|
|
34
|
+
name: string;
|
|
35
|
+
icon?: string;
|
|
36
|
+
path: string;
|
|
37
|
+
children?: Module[];
|
|
38
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import axios from 'axios';
|
|
2
|
+
|
|
3
|
+
const NOTIFICATIONS_SERVICE_URL = process.env.NOTIFICATIONS_SERVICE_URL || 'http://localhost:3003';
|
|
4
|
+
|
|
5
|
+
export const sendNotification = async (
|
|
6
|
+
userIds: string | string[],
|
|
7
|
+
title: string,
|
|
8
|
+
body: string,
|
|
9
|
+
data: any = {},
|
|
10
|
+
type: string = 'general'
|
|
11
|
+
) => {
|
|
12
|
+
try {
|
|
13
|
+
const payload = {
|
|
14
|
+
title,
|
|
15
|
+
body,
|
|
16
|
+
data,
|
|
17
|
+
type
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const userIdsArray = Array.isArray(userIds) ? userIds : [userIds];
|
|
21
|
+
|
|
22
|
+
// Send notification to each user
|
|
23
|
+
for (const userId of userIdsArray) {
|
|
24
|
+
await axios.post(`${NOTIFICATIONS_SERVICE_URL}/send-notification`,
|
|
25
|
+
{ ...payload, userId },
|
|
26
|
+
{
|
|
27
|
+
headers: {
|
|
28
|
+
'Authorization': `Bearer ${process.env.NOTIFICATIONS_SERVICE_TOKEN || 'service-token'}`
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
console.log(`Notifications sent to users: ${userIdsArray.join(', ')}`);
|
|
35
|
+
} catch (error) {
|
|
36
|
+
console.error('Error sending notifications:', error);
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export const sendMeetingInvitation = async (
|
|
41
|
+
meetingId: string,
|
|
42
|
+
participants: string[],
|
|
43
|
+
title: string,
|
|
44
|
+
description: string,
|
|
45
|
+
hostId: string
|
|
46
|
+
) => {
|
|
47
|
+
try {
|
|
48
|
+
await axios.post(`${NOTIFICATIONS_SERVICE_URL}/meeting-invitation`,
|
|
49
|
+
{
|
|
50
|
+
meetingId,
|
|
51
|
+
participants,
|
|
52
|
+
title,
|
|
53
|
+
description
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
headers: {
|
|
57
|
+
'Authorization': `Bearer ${process.env.NOTIFICATIONS_SERVICE_TOKEN || 'service-token'}`
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
console.log(`Meeting invitations sent for meeting: ${meetingId}`);
|
|
63
|
+
} catch (error) {
|
|
64
|
+
console.error('Error sending meeting invitations:', error);
|
|
65
|
+
}
|
|
66
|
+
};
|