Skip to content
Open
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
9 changes: 9 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -103,3 +103,12 @@ REFERRAL_MAX_REWARDS_PER_REFERRER=50
AFRICASTALKING_USERNAME=sandbox
AFRICASTALKING_API_KEY=your-africastalking-api-key
AFRICASTALKING_SENDER_ID=Hamplard

# Email Verification
EMAIL_VERIFICATION_EXPIRES_IN=86400
EMAIL_VERIFICATION_BASE_URL=http://localhost:3000/verify-email

# GDPR Data Export
DATA_EXPORT_DIR=./data-exports
DATA_EXPORT_RATE_LIMIT_WINDOW_MINUTES=60
DATA_EXPORT_RATE_LIMIT_MAX=5
1 change: 1 addition & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
-- AlterTable: Add emailVerifiedAt to users and create data_export_jobs
ALTER TABLE "users" ADD COLUMN "emailVerifiedAt" TIMESTAMP(3);

-- CreateTable
CREATE TABLE "data_export_jobs" (
"id" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"status" "DataExportStatus" NOT NULL DEFAULT 'PENDING',
"filePath" TEXT,
"fileSize" INTEGER,
"errorMessage" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"completedAt" TIMESTAMP(3),

CONSTRAINT "data_export_jobs_pkey" PRIMARY KEY ("id")
);

-- CreateIndex
CREATE INDEX "data_export_jobs_userId_idx" ON "data_export_jobs"("userId");

-- CreateIndex
CREATE INDEX "data_export_jobs_status_idx" ON "data_export_jobs"("status");

-- AddForeignKey
ALTER TABLE "data_export_jobs" ADD CONSTRAINT "data_export_jobs_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
3 changes: 3 additions & 0 deletions prisma/migrations/migration_lock.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (i.e. Git)
provider = "postgresql"
34 changes: 32 additions & 2 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -249,8 +249,9 @@ model User {
bio String?
avatarUrl String?
role UserRole @default(STUDENT)
isVerified Boolean @default(false)
isBanned Boolean @default(false)
isVerified Boolean @default(false)
emailVerifiedAt DateTime?
isBanned Boolean @default(false)
bannedAt DateTime?
banReason String?
isSuspended Boolean @default(false)
Expand Down Expand Up @@ -301,6 +302,7 @@ model User {
questionsAsked Question[] @relation("QuestionsAsked")
answersGiven Answer[] @relation("AnswersGiven")
courseDrafts CourseDraft[] @relation("InstructorCourseDrafts")
dataExports DataExportJob[]

@@map("users")
}
Expand Down Expand Up @@ -1359,3 +1361,31 @@ model CourseDraft {
@@map("course_drafts")
}

// ============================================================
// GDPR DATA EXPORT
// ============================================================

enum DataExportStatus {
PENDING
PROCESSING
COMPLETED
FAILED
}

model DataExportJob {
id String @id @default(uuid())
userId String
status DataExportStatus @default(PENDING)
filePath String?
fileSize Int?
errorMessage String?
createdAt DateTime @default(now())
completedAt DateTime?

user User @relation(fields: [userId], references: [id])

@@index([userId])
@@index([status])
@@map("data_export_jobs")
}

2 changes: 2 additions & 0 deletions src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import { AnalyticsModule } from './modules/analytics/analytics.module';
import { BackupsModule } from './modules/backups/backups.module';
import { UploadsModule } from './modules/uploads/uploads.module';
import { TagsModule } from './modules/tags/tags.module';
import { PrivacyModule } from './modules/privacy/privacy.module';

@Module({
imports: [
Expand Down Expand Up @@ -80,6 +81,7 @@ import { TagsModule } from './modules/tags/tags.module';
BackupsModule,
UploadsModule,
TagsModule,
PrivacyModule,
],
})
export class AppModule {}
21 changes: 19 additions & 2 deletions src/modules/auth/auth.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@ import { GoogleAuthController } from './google-auth.controller';
import { GoogleAuthService } from './google-auth.service';
import { GoogleStrategy } from './google.strategy';
import { GoogleAuthGuard } from './google-auth.guard';
import { EmailVerificationController } from './email-verification.controller';
import { EmailVerificationService } from './email-verification.service';
import { ReferralsModule } from '../referrals/referrals.module';
import { NotificationsModule } from '../notifications/notifications.module';
import { PrismaModule } from '../../common/prisma/prisma.module';

@Module({
imports: [
Expand All @@ -24,9 +28,22 @@ import { ReferralsModule } from '../referrals/referrals.module';
}),
}),
ReferralsModule,
NotificationsModule,
PrismaModule,
],
controllers: [
AuthController,
GoogleAuthController,
EmailVerificationController,
],
providers: [
AuthService,
JwtStrategy,
GoogleAuthService,
GoogleStrategy,
GoogleAuthGuard,
EmailVerificationService,
],
controllers: [AuthController, GoogleAuthController],
providers: [AuthService, JwtStrategy, GoogleAuthService, GoogleStrategy, GoogleAuthGuard],
exports: [AuthService],
})
export class AuthModule {}
45 changes: 45 additions & 0 deletions src/modules/auth/email-verification.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import {
Controller,
Get,
Post,
Query,
UseGuards,
HttpCode,
HttpStatus,
} from '@nestjs/common';
import { ApiTags, ApiOperation, ApiBearerAuth, ApiQuery } from '@nestjs/swagger';
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
import { CurrentUser } from '../../common/decorators/current-user.decorator';
import { EmailVerificationService } from './email-verification.service';

@ApiTags('auth')
@Controller('auth/email-verification')
export class EmailVerificationController {
constructor(
private readonly verificationService: EmailVerificationService,
) {}

@Post('request')
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
@HttpCode(HttpStatus.OK)
@ApiOperation({ summary: 'Request email verification link' })
requestVerification(@CurrentUser('id') userId: string) {
return this.verificationService.requestVerification(userId);
}

@Get('confirm')
@ApiOperation({ summary: 'Confirm email verification via token' })
@ApiQuery({ name: 'token', description: 'Signed verification token' })
confirmVerification(@Query('token') token: string) {
return this.verificationService.confirmVerification(token);
}

@Get('status')
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
@ApiOperation({ summary: 'Get email verification status' })
getVerificationStatus(@CurrentUser('id') userId: string) {
return this.verificationService.getVerificationStatus(userId);
}
}
Loading