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
62 changes: 62 additions & 0 deletions backend/STATUS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# Backend Module Status

| Module | Status |
|--------|--------|
| achievements | ✅ Live |
| activity | ✅ Live |
| admin | ✅ Live |
| analytics | ✅ Live |
| api-key | ✅ Live |
| audit-log | ✅ Live |
| auth | ✅ Live |
| badge | ✅ Live |
| cache | ✅ Live |
| common | ✅ Live |
| config | ✅ Live |
| content | ✅ Live |
| content-rating | ✅ Live |
| daily-reward | ✅ Live |
| feedback | ✅ Live |
| gameMechanics | ✅ Live |
| geostats | ✅ Live |
| hint | ✅ Live |
| in-app-notifications | ✅ Live |
| maintenance-mode | ✅ Live |
| migration | ✅ Live |
| milestone | ✅ Live |
| multiplayer-queue | ✅ Live |
| nft-claim | ✅ Live |
| nft-marketplace-stub | ✅ Live |
| progress | ✅ Live |
| promo-code | ✅ Live |
| puzzle | ✅ Live |
| puzzle-access-log | ✅ Live |
| puzzle-category | ✅ Live |
| puzzle-comment | ✅ Live |
| puzzle-dependency | ✅ Live |
| puzzle-draft | ✅ Live |
| puzzle-fork | ✅ Live |
| puzzle-review | ✅ Live |
| puzzle-submission | ✅ Live |
| puzzle-test-case | ✅ Live |
| puzzle-translation | ✅ Live |
| puzzle-versioning | ✅ Live |
| quiz | ✅ Live |
| rate-limiter | ✅ Live |
| referral | ✅ Live |
| reports | ✅ Live |
| reward-shop | ✅ Live |
| rewards | ✅ Live |
| session | ✅ Live |
| streak | ✅ Live |
| time-trial | ✅ Live |
| token-verification | ✅ Live |
| user | ✅ Live |
| user-activity-log | ✅ Live |
| user-inventory | ✅ Live |
| user-ranking | ✅ Live |
| user-reaction | ✅ Live |
| user-report-card | ✅ Live |
| user-settings | ✅ Live |
| user-token-history | ✅ Live |
| wallet | ✅ Live |
12 changes: 8 additions & 4 deletions backend/src/analytics/analytics.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,11 +51,15 @@ export class AnalyticsController implements OnModuleInit {
}

@Get('puzzles/most-solved')
async getMostSolvedPuzzles(): Promise<
Array<{ puzzleId: string; solveCount: number }>
> {
async getMostSolvedPuzzles(
@Query('limit') limit?: string,
@Query('offset') offset?: string,
): Promise<Array<{ puzzleId: string; solveCount: number }>> {
this.logger.log('Handling request for most solved puzzles.');
return this.analyticsService.getMostSolvedPuzzlesAsync();
return this.analyticsService.getMostSolvedPuzzlesAsync(
limit ? Number(limit) : undefined,
offset ? Number(offset) : undefined,
);
}

@Get('puzzles/:puzzleId/average-solve-time')
Expand Down
12 changes: 8 additions & 4 deletions backend/src/analytics/analytics.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,14 +62,18 @@ export class AnalyticsService {
*/
async getMostSolvedPuzzlesAsync(
limit?: number,
offset?: number,
): Promise<Array<{ puzzleId: string; solveCount: number }>> {
this.logger.log('Fetching most solved puzzles...');
const sql = limit
? `SELECT puzzle_id, solve_count FROM puzzle_stats_mv
ORDER BY solve_count DESC LIMIT $1`
: `SELECT puzzle_id, solve_count FROM puzzle_stats_mv
ORDER BY solve_count DESC`;
const params = limit ? [limit] : [];
ORDER BY solve_count DESC LIMIT $1 OFFSET $2`
: offset
? `SELECT puzzle_id, solve_count FROM puzzle_stats_mv
ORDER BY solve_count DESC OFFSET $1`
: `SELECT puzzle_id, solve_count FROM puzzle_stats_mv
ORDER BY solve_count DESC`;
const params = limit ? [limit, offset ?? 0] : offset ? [offset] : [];
const { rows } = await this.pool.query(sql, params);
return rows.map((r) => ({
puzzleId: r.puzzle_id as string,
Expand Down
37 changes: 37 additions & 0 deletions backend/src/api-key/api-key.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ export interface ApiKey {
status: ApiKeyStatus;
createdAt: Date;
expiresAt?: Date;
monthlyRequestQuota: number;
rateLimitPerMinute: number;
requestsThisMonth: number;
scopedEndpoints: string[];
}

@Injectable()
Expand Down Expand Up @@ -45,6 +49,9 @@ export class ApiKeyService {
ownerLabel: string,
isAdmin: boolean,
expiresAt?: Date,
monthlyRequestQuota = 1000,
rateLimitPerMinute = 100,
scopedEndpoints: string[] = [],
): ApiKey {
if (!isAdmin) {
throw new UnauthorizedException(
Expand All @@ -62,12 +69,42 @@ export class ApiKeyService {
status: ApiKeyStatus.ACTIVE,
createdAt: new Date(),
expiresAt,
monthlyRequestQuota,
rateLimitPerMinute,
requestsThisMonth: 0,
scopedEndpoints,
};
this.apiKeys.set(newKey, apiKey);
this.logger.log(`Generated new API key for ${ownerLabel}: ${newKey}`);
return apiKey;
}

checkQuota(key: string): boolean {
const apiKey = this.apiKeys.get(key);
if (!apiKey) return false;
return apiKey.requestsThisMonth < apiKey.monthlyRequestQuota;
}

incrementRequestCount(key: string): void {
const apiKey = this.apiKeys.get(key);
if (apiKey) {
apiKey.requestsThisMonth += 1;
this.apiKeys.set(key, apiKey);
}
}

getQuotaUsage(key: string): { used: number; limit: number; remaining: number } {
const apiKey = this.apiKeys.get(key);
if (!apiKey) {
return { used: 0, limit: 0, remaining: 0 };
}
return {
used: apiKey.requestsThisMonth,
limit: apiKey.monthlyRequestQuota,
remaining: Math.max(0, apiKey.monthlyRequestQuota - apiKey.requestsThisMonth),
};
}

revokeApiKey(key: string, isAdmin: boolean): ApiKey {
if (!isAdmin) {
throw new UnauthorizedException(
Expand Down
41 changes: 40 additions & 1 deletion backend/src/api-key/entities/api-key.entity.ts
Original file line number Diff line number Diff line change
@@ -1 +1,40 @@
export class ApiKey {}
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from 'typeorm';

@Entity('api_keys')
export class ApiKey {
@PrimaryGeneratedColumn('uuid')
id: string;

@Column({ unique: true })
key: string;

@Column()
ownerLabel: string;

@Column({ default: 'active' })
status: string;

@Column({ nullable: true })
expiresAt: Date;

@Column({ default: 1000 })
monthlyRequestQuota: number;

@Column({ default: 0 })
requestsThisMonth: number;

@Column({ default: 100 })
rateLimitPerMinute: number;

@Column({ type: 'text', nullable: true })
scopedEndpoints: string;

@Column({ default: false })
isAdmin: boolean;

@CreateDateColumn()
createdAt: Date;

@UpdateDateColumn()
updatedAt: Date;
}
2 changes: 1 addition & 1 deletion backend/src/audit-log/entities/audit-log.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ export class AuditLog {
@PrimaryGeneratedColumn('uuid')
id: string;

@Column()
@Column({ length: 128 })
userId: string;

@Column()
Expand Down
2 changes: 1 addition & 1 deletion backend/src/badge/entities/user-badge.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export class UserBadge {
@PrimaryGeneratedColumn()
id: number;

@Column()
@Column({ length: 128 })
userId: number;

@ManyToOne(() => Badge, (badge) => badge.userBadges, { eager: true })
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export class ContentRating {
@PrimaryGeneratedColumn('uuid')
id: string;

@Column()
@Column({ length: 128 })
@Index()
userId: string;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export class DailyRewardLog {
id: string;

@Index()
@Column()
@Column({ length: 128 })
userId: string;

@Column({ default: 1 })
Expand Down
1 change: 1 addition & 0 deletions backend/src/feedback/entities/feedback.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export class Feedback {
@Column({
type: "uuid",
nullable: true,
length: 128,
})
userId: string // null if anonymous

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export class ChallengeCompletion {
@PrimaryGeneratedColumn("uuid")
id: string

@Column("uuid")
@Column({ type: "uuid", length: 128 })
userId: string

@Column("uuid")
Expand Down
2 changes: 1 addition & 1 deletion backend/src/gameMechanics/entities/hint-usage.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export class HintUsage {
@PrimaryGeneratedColumn("uuid")
id: string

@Column("uuid")
@Column({ type: "uuid", length: 128 })
userId: string

@Column("uuid")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export class PuzzleSubmission {
@PrimaryGeneratedColumn("uuid")
id: string

@Column("uuid")
@Column({ type: "uuid", length: 128 })
userId: string

@Column("uuid")
Expand Down
2 changes: 1 addition & 1 deletion backend/src/milestone/entities/user-milestone.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export class UserMilestone {
@PrimaryGeneratedColumn("uuid")
id: string

@Column({ type: "uuid" })
@Column({ type: "uuid", length: 128 })
userId: string

@Column({ type: "uuid" })
Expand Down
2 changes: 1 addition & 1 deletion backend/src/milestone/entities/user-progress.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export class UserProgress {
@PrimaryGeneratedColumn("uuid")
id: string

@Column({ type: "uuid" })
@Column({ type: "uuid", length: 128 })
userId: string

@Column({
Expand Down
2 changes: 1 addition & 1 deletion backend/src/multiplayer-queue/entities/queue.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export class Queue {
@PrimaryGeneratedColumn("uuid")
id: string

@Column({ type: "uuid" })
@Column({ type: "uuid", length: 128 })
userId: string

@Column({ type: "varchar", length: 100 })
Expand Down
2 changes: 1 addition & 1 deletion backend/src/progress/entities/progress.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ export class Progress {
@PrimaryGeneratedColumn('uuid')
id: string;

@Column({ type: 'uuid', unique: true })
@Column({ type: 'uuid', unique: true, length: 128 })
userId: string;

@Column({ default: 0 })
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export class PuzzleAccessLog {
id: string;

@Index() // Index for faster user-specific queries
@Column()
@Column({ length: 128 })
userId: string;

@Index() // Index for faster puzzle-specific queries
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export class PuzzleCompletion {
@PrimaryGeneratedColumn('uuid')
id: string;

@Column({ name: 'user_id' })
@Column({ name: 'user_id', length: 128 })
@Index()
userId: string;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export class PuzzleReview {
@Column({ type: "uuid" })
puzzleId: string

@Column({ type: "uuid", nullable: true })
@Column({ type: "uuid", nullable: true, length: 128 })
userId: string // null for anonymous reviews

@Column({ type: "varchar", length: 100, nullable: true })
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export class ValidationResult {
@Column({ type: "uuid" })
puzzleId: string

@Column({ type: "uuid", nullable: true })
@Column({ type: "uuid", nullable: true, length: 128 })
userId: string // null for anonymous validations

@Column({ type: "uuid" })
Expand Down
2 changes: 1 addition & 1 deletion backend/src/referral/entities/referral-bonus.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export class ReferralBonus {
@PrimaryGeneratedColumn("uuid")
id: string

@Column({ type: "uuid" })
@Column({ type: "uuid", length: 128 })
userId: string

@ManyToOne(() => User, { onDelete: "CASCADE" })
Expand Down
2 changes: 1 addition & 1 deletion backend/src/referral/entities/referral-code.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export class ReferralCode {
@Column({ type: "varchar", length: 20, unique: true })
code: string

@Column({ type: "uuid" })
@Column({ type: "uuid", length: 128 })
userId: string

@ManyToOne(() => User, { onDelete: "CASCADE" })
Expand Down
2 changes: 1 addition & 1 deletion backend/src/reports/entities/report.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export class Report {
@Column()
puzzleId: number;

@Column()
@Column({ length: 128 })
userId: number;

@Column()
Expand Down
2 changes: 1 addition & 1 deletion backend/src/rewards/entities/reward-claim.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export class RewardClaim {
id: string;

@ApiProperty({ description: 'User ID who claimed the reward' })
@Column({ type: 'varchar', length: 255 })
@Column({ type: 'varchar', length: 128 })
userId: string;

@ApiProperty({ description: 'Reward ID that was claimed' })
Expand Down
2 changes: 1 addition & 1 deletion backend/src/session/session.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export class Session {
@PrimaryGeneratedColumn('uuid')
sessionId: string;

@Column({ type: 'uuid' })
@Column({ type: 'uuid', length: 128 })
@Index()
userId: string;

Expand Down
Loading
Loading