Skip to content

Commit 4425c53

Browse files
authored
Merge pull request #163 from hack-the-6ix/jeffrey-volunteer-auth
otp auth for volunteers
2 parents 29e1284 + c93dfc4 commit 4425c53

7 files changed

Lines changed: 655 additions & 52 deletions

File tree

api.md

Lines changed: 112 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -587,7 +587,6 @@ Trigger a mailing list sync with ListMonk for a particular user.
587587
}
588588
```
589589

590-
591590
### POST - Verify Mailing List
592591

593592
`/api/action/verifyMailingList`
@@ -1027,6 +1026,118 @@ Remove the given check in notes to a user's check in notes. Returns all the user
10271026
}
10281027
```
10291028

1029+
### POST - Generate OTP (Organizer)
1030+
1031+
`/api/action/generate-otp`
1032+
1033+
Generate a 6-digit OTP code for a volunteer (external user). The OTP expires in 10 minutes and can only be used once.
1034+
1035+
#### Input Specification
1036+
1037+
```
1038+
{
1039+
email: "<email of external user>"
1040+
}
1041+
```
1042+
1043+
#### Output Specification
1044+
1045+
```
1046+
{
1047+
status: 200,
1048+
message: {
1049+
success: true,
1050+
message: "OTP generated successfully",
1051+
code: "123456",
1052+
expiration: "2024-01-01T12:00:00.000Z"
1053+
}
1054+
}
1055+
```
1056+
1057+
### POST - Verify OTP (Organizer)
1058+
1059+
`/api/action/verify-otp`
1060+
1061+
Verify a 6-digit OTP code for a volunteer. If valid, the OTP is marked as used and cannot be reused. Returns a JWT token for authentication.
1062+
1063+
#### Input Specification
1064+
1065+
```
1066+
{
1067+
code: "123456",
1068+
email: "<email of external user>"
1069+
}
1070+
```
1071+
1072+
#### Output Specification
1073+
1074+
```
1075+
{
1076+
status: 200,
1077+
message: {
1078+
success: true,
1079+
message: "OTP verified successfully",
1080+
user: {
1081+
// External user object
1082+
},
1083+
token: "<jwt_token_for_authentication>"
1084+
}
1085+
}
1086+
```
1087+
1088+
### GET - Get All OTPs (Organizer)
1089+
1090+
`/api/action/get-all-otps`
1091+
1092+
Retrieve all OTP codes with their usage tracking information.
1093+
1094+
#### Output Specification
1095+
1096+
```
1097+
{
1098+
status: 200,
1099+
message: {
1100+
success: true,
1101+
otps: [
1102+
{
1103+
id: "<otp_id>",
1104+
code: "123456",
1105+
email: "<email>",
1106+
used: true,
1107+
expiration: "2024-01-01T12:00:00.000Z",
1108+
createdAt: "2024-01-01T11:50:00.000Z",
1109+
usedBy: "<user_id>",
1110+
usedAt: "2024-01-01T11:55:00.000Z",
1111+
issuedBy: "<organizer_email>",
1112+
usedName: "John Doe"
1113+
}
1114+
]
1115+
}
1116+
}
1117+
```
1118+
1119+
### DELETE - Expire OTP (Organizer)
1120+
1121+
`/api/action/expire-otp/:otpId`
1122+
1123+
Expire a specific OTP code by setting its expiration time to the current time.
1124+
1125+
#### Input Specification
1126+
1127+
`otpId` - The ID of the OTP to expire (passed in URL)
1128+
1129+
#### Output Specification
1130+
1131+
```
1132+
{
1133+
status: 200,
1134+
message: {
1135+
success: true,
1136+
message: "OTP expired successfully"
1137+
}
1138+
}
1139+
```
1140+
10301141
### GET - Check in QR Code
10311142

10321143
`/api/action/checkInQR`

src/controller/OTPController.ts

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import { IUser } from '../models/user/fields';
2+
import OTP from '../models/otp/OTP';
3+
import ExternalUser from '../models/externaluser/ExternalUser';
4+
import { BadRequestError, NotFoundError } from '../types/errors';
5+
import { createJwt } from '../services/permissions';
6+
7+
export async function generateOTP(requestUser: IUser, email: string) {
8+
const externalUser = await ExternalUser.findOne({ email });
9+
if (!externalUser) {
10+
throw new BadRequestError('Email not found in external users');
11+
}
12+
13+
const code = Math.floor(100000 + Math.random() * 900000).toString();
14+
const expiration = Date.now() + 10 * 60 * 1000;
15+
16+
const otp = new OTP({
17+
code,
18+
email,
19+
expiration,
20+
used: false,
21+
issuedBy: requestUser.email,
22+
});
23+
24+
await otp.save();
25+
26+
return {
27+
success: true,
28+
message: 'OTP generated successfully',
29+
code,
30+
expiration: new Date(expiration),
31+
};
32+
}
33+
34+
export async function verifyOTP(
35+
requestUser: IUser | null,
36+
code: string,
37+
email: string,
38+
) {
39+
const externalUser = await ExternalUser.findOne({ email });
40+
if (!externalUser) {
41+
throw new BadRequestError('Email not found in external users');
42+
}
43+
44+
console.log('OTP.findOne', code, email);
45+
const otp = await OTP.findOne({ code, email });
46+
if (!otp) {
47+
throw new NotFoundError('Invalid OTP code');
48+
}
49+
50+
if (otp.used) {
51+
throw new BadRequestError('OTP code already used');
52+
}
53+
54+
if (Date.now() > otp.expiration) {
55+
throw new BadRequestError('OTP code expired');
56+
}
57+
58+
otp.used = true;
59+
otp.usedBy = externalUser._id.toString();
60+
otp.usedAt = new Date();
61+
otp.usedName = externalUser.firstName + ' ' + externalUser.lastName;
62+
await otp.save();
63+
64+
const token = createJwt({
65+
id: externalUser._id,
66+
idpLinkID: `OTP-${externalUser._id}`,
67+
roles: {
68+
volunteer: true,
69+
},
70+
});
71+
72+
return {
73+
success: true,
74+
message: 'OTP verified successfully',
75+
user: externalUser,
76+
token: token,
77+
};
78+
}
79+
80+
export async function getAllOTPs(requestUser: IUser) {
81+
const otps = await OTP.find({}).sort({ createdAt: -1 });
82+
83+
return {
84+
success: true,
85+
otps: otps.map((otp) => ({
86+
id: otp._id,
87+
code: otp.code,
88+
email: otp.email,
89+
used: otp.used,
90+
expiration: new Date(otp.expiration),
91+
createdAt: otp.createdAt,
92+
usedBy: otp.usedBy,
93+
usedAt: otp.usedAt,
94+
issuedBy: otp.issuedBy,
95+
usedName: otp.usedName,
96+
})),
97+
};
98+
}
99+
100+
export async function expireOTP(requestUser: IUser, otpId: string) {
101+
const otp = await OTP.findById(otpId);
102+
if (!otp) {
103+
throw new NotFoundError('OTP not found');
104+
}
105+
106+
otp.expiration = Date.now();
107+
await otp.save();
108+
109+
return {
110+
success: true,
111+
message: 'OTP expired successfully',
112+
};
113+
}

src/models/otp/OTP.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import mongoose from 'mongoose';
2+
import { extractFields } from '../util';
3+
import { fields, IOTP } from './fields';
4+
5+
const schema = new mongoose.Schema(extractFields(fields), {
6+
toObject: {
7+
virtuals: true,
8+
},
9+
toJSON: {
10+
virtuals: true,
11+
},
12+
});
13+
14+
export default mongoose.model<IOTP>('OTP', schema);

src/models/otp/fields.ts

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import {
2+
CreateCheckRequest,
3+
DeleteCheckRequest,
4+
ReadCheckRequest,
5+
WriteCheckRequest,
6+
} from '../../types/checker';
7+
import { BasicUser } from '../../types/types';
8+
import { isOrganizer } from '../validator';
9+
10+
export const fields = {
11+
createCheck: (request: CreateCheckRequest<any, any>) =>
12+
isOrganizer(request.requestUser),
13+
readCheck: (request: ReadCheckRequest<any>) =>
14+
isOrganizer(request.requestUser),
15+
deleteCheck: (request: DeleteCheckRequest<any>) =>
16+
isOrganizer(request.requestUser),
17+
writeCheck: (request: WriteCheckRequest<any, any>) =>
18+
isOrganizer(request.requestUser),
19+
FIELDS: {
20+
_id: {
21+
virtual: true,
22+
readCheck: true,
23+
},
24+
code: {
25+
type: String,
26+
required: true,
27+
readCheck: true,
28+
writeCheck: true,
29+
},
30+
email: {
31+
type: String,
32+
required: true,
33+
index: true,
34+
readCheck: true,
35+
writeCheck: true,
36+
},
37+
expiration: {
38+
type: Number,
39+
required: true,
40+
readCheck: true,
41+
writeCheck: true,
42+
},
43+
used: {
44+
type: Boolean,
45+
required: true,
46+
default: false,
47+
readCheck: true,
48+
writeCheck: true,
49+
},
50+
usedBy: {
51+
type: String,
52+
default: null,
53+
readCheck: true,
54+
writeCheck: true,
55+
},
56+
usedAt: {
57+
type: Date,
58+
default: null,
59+
readCheck: true,
60+
writeCheck: true,
61+
},
62+
issuedBy: {
63+
type: String,
64+
required: true,
65+
readCheck: true,
66+
writeCheck: true,
67+
},
68+
usedName: {
69+
type: String,
70+
default: null,
71+
readCheck: true,
72+
writeCheck: true,
73+
},
74+
createdAt: {
75+
type: Date,
76+
default: Date.now,
77+
readCheck: true,
78+
},
79+
},
80+
};
81+
82+
export interface IOTP extends BasicUser {
83+
code: string;
84+
email: string;
85+
expiration: number;
86+
used: boolean;
87+
createdAt: Date;
88+
usedBy?: string | null;
89+
usedAt?: Date | null;
90+
issuedBy: string;
91+
usedName?: string | null;
92+
}

0 commit comments

Comments
 (0)