Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
113 changes: 112 additions & 1 deletion api.md
Original file line number Diff line number Diff line change
Expand Up @@ -587,7 +587,6 @@ Trigger a mailing list sync with ListMonk for a particular user.
}
```


### POST - Verify Mailing List

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

### POST - Generate OTP (Organizer)

`/api/action/generate-otp`

Generate a 6-digit OTP code for a volunteer (external user). The OTP expires in 10 minutes and can only be used once.

#### Input Specification

```
{
email: "<email of external user>"
}
```

#### Output Specification

```
{
status: 200,
message: {
success: true,
message: "OTP generated successfully",
code: "123456",
expiration: "2024-01-01T12:00:00.000Z"
}
}
```

### POST - Verify OTP (Organizer)

`/api/action/verify-otp`

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.

#### Input Specification

```
{
code: "123456",
email: "<email of external user>"
}
```

#### Output Specification

```
{
status: 200,
message: {
success: true,
message: "OTP verified successfully",
user: {
// External user object
},
token: "<jwt_token_for_authentication>"
}
}
```

### GET - Get All OTPs (Organizer)

`/api/action/get-all-otps`

Retrieve all OTP codes with their usage tracking information.

#### Output Specification

```
{
status: 200,
message: {
success: true,
otps: [
{
id: "<otp_id>",
code: "123456",
email: "<email>",
used: true,
expiration: "2024-01-01T12:00:00.000Z",
createdAt: "2024-01-01T11:50:00.000Z",
usedBy: "<user_id>",
usedAt: "2024-01-01T11:55:00.000Z",
issuedBy: "<organizer_email>",
usedName: "John Doe"
}
]
}
}
```

### DELETE - Expire OTP (Organizer)

`/api/action/expire-otp/:otpId`

Expire a specific OTP code by setting its expiration time to the current time.

#### Input Specification

`otpId` - The ID of the OTP to expire (passed in URL)

#### Output Specification

```
{
status: 200,
message: {
success: true,
message: "OTP expired successfully"
}
}
```

### GET - Check in QR Code

`/api/action/checkInQR`
Expand Down
113 changes: 113 additions & 0 deletions src/controller/OTPController.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import { IUser } from '../models/user/fields';
import OTP from '../models/otp/OTP';
import ExternalUser from '../models/externaluser/ExternalUser';
import { BadRequestError, NotFoundError } from '../types/errors';
import { createJwt } from '../services/permissions';

export async function generateOTP(requestUser: IUser, email: string) {
const externalUser = await ExternalUser.findOne({ email });
if (!externalUser) {
throw new BadRequestError('Email not found in external users');
}

const code = Math.floor(100000 + Math.random() * 900000).toString();
const expiration = Date.now() + 10 * 60 * 1000;

const otp = new OTP({
code,
email,
expiration,
used: false,
issuedBy: requestUser.email,
});

await otp.save();

return {
success: true,
message: 'OTP generated successfully',
code,
expiration: new Date(expiration),
};
}

export async function verifyOTP(
requestUser: IUser | null,
code: string,
email: string,
) {
const externalUser = await ExternalUser.findOne({ email });
if (!externalUser) {
throw new BadRequestError('Email not found in external users');
}

console.log('OTP.findOne', code, email);
const otp = await OTP.findOne({ code, email });
if (!otp) {
throw new NotFoundError('Invalid OTP code');
}

if (otp.used) {
throw new BadRequestError('OTP code already used');
}

if (Date.now() > otp.expiration) {
throw new BadRequestError('OTP code expired');
}

otp.used = true;
otp.usedBy = externalUser._id.toString();
otp.usedAt = new Date();
otp.usedName = externalUser.firstName + ' ' + externalUser.lastName;
await otp.save();

const token = createJwt({
id: externalUser._id,
idpLinkID: `OTP-${externalUser._id}`,
roles: {
volunteer: true,
},
});

return {
success: true,
message: 'OTP verified successfully',
user: externalUser,
token: token,
};
}

export async function getAllOTPs(requestUser: IUser) {
const otps = await OTP.find({}).sort({ createdAt: -1 });

return {
success: true,
otps: otps.map((otp) => ({
id: otp._id,
code: otp.code,
email: otp.email,
used: otp.used,
expiration: new Date(otp.expiration),
createdAt: otp.createdAt,
usedBy: otp.usedBy,
usedAt: otp.usedAt,
issuedBy: otp.issuedBy,
usedName: otp.usedName,
})),
};
}

export async function expireOTP(requestUser: IUser, otpId: string) {
const otp = await OTP.findById(otpId);
if (!otp) {
throw new NotFoundError('OTP not found');
}

otp.expiration = Date.now();
await otp.save();

return {
success: true,
message: 'OTP expired successfully',
};
}
14 changes: 14 additions & 0 deletions src/models/otp/OTP.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import mongoose from 'mongoose';
import { extractFields } from '../util';
import { fields, IOTP } from './fields';

const schema = new mongoose.Schema(extractFields(fields), {
toObject: {
virtuals: true,
},
toJSON: {
virtuals: true,
},
});

export default mongoose.model<IOTP>('OTP', schema);
92 changes: 92 additions & 0 deletions src/models/otp/fields.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import {
CreateCheckRequest,
DeleteCheckRequest,
ReadCheckRequest,
WriteCheckRequest,
} from '../../types/checker';
import { BasicUser } from '../../types/types';
import { isOrganizer } from '../validator';

export const fields = {
createCheck: (request: CreateCheckRequest<any, any>) =>
isOrganizer(request.requestUser),
readCheck: (request: ReadCheckRequest<any>) =>
isOrganizer(request.requestUser),
deleteCheck: (request: DeleteCheckRequest<any>) =>
isOrganizer(request.requestUser),
writeCheck: (request: WriteCheckRequest<any, any>) =>
isOrganizer(request.requestUser),
FIELDS: {
_id: {
virtual: true,
readCheck: true,
},
code: {
type: String,
required: true,
readCheck: true,
writeCheck: true,
},
email: {
type: String,
required: true,
index: true,
readCheck: true,
writeCheck: true,
},
expiration: {
type: Number,
required: true,
readCheck: true,
writeCheck: true,
},
used: {
type: Boolean,
required: true,
default: false,
readCheck: true,
writeCheck: true,
},
usedBy: {
type: String,
default: null,
readCheck: true,
writeCheck: true,
},
usedAt: {
type: Date,
default: null,
readCheck: true,
writeCheck: true,
},
issuedBy: {
type: String,
required: true,
readCheck: true,
writeCheck: true,
},
usedName: {
type: String,
default: null,
readCheck: true,
writeCheck: true,
},
createdAt: {
type: Date,
default: Date.now,
readCheck: true,
},
},
};

export interface IOTP extends BasicUser {
code: string;
email: string;
expiration: number;
used: boolean;
createdAt: Date;
usedBy?: string | null;
usedAt?: Date | null;
issuedBy: string;
usedName?: string | null;
}
Loading
Loading