From 3c13fcb80a9e6812e4d72d330c3aee07c3d6d439 Mon Sep 17 00:00:00 2001 From: bigben-7 Date: Wed, 29 Jul 2026 16:06:33 +0100 Subject: [PATCH] fix: userId length caps, pagination, social links, error boundary (#87 #85 #77 #71) --- backend/STATUS.md | 62 +++++++++++++++++++ backend/src/analytics/analytics.controller.ts | 12 ++-- backend/src/analytics/analytics.service.ts | 12 ++-- backend/src/api-key/api-key.service.ts | 37 +++++++++++ .../src/api-key/entities/api-key.entity.ts | 41 +++++++++++- .../audit-log/entities/audit-log.entity.ts | 2 +- .../src/badge/entities/user-badge.entity.ts | 2 +- .../entities/content-rating.entity.ts | 2 +- .../entities/daily-reward-log.entity.ts | 2 +- .../src/feedback/entities/feedback.entity.ts | 1 + .../entities/challenge-completion.entity.ts | 2 +- .../entities/hint-usage.entity.ts | 2 +- .../entities/puzzle-submission.entity.ts | 2 +- .../entities/user-milestone.entity.ts | 2 +- .../entities/user-progress.entity.ts | 2 +- .../entities/queue.entity.ts | 2 +- .../src/progress/entities/progress.entity.ts | 2 +- .../entities/puzzle-access-log.entity.ts | 2 +- .../entities/puzzle-completion.entity.ts | 2 +- .../entities/puzzle-review.entity.ts | 2 +- .../entities/validation-result.entity.ts | 2 +- .../entities/referral-bonus.entity.ts | 2 +- .../referral/entities/referral-code.entity.ts | 2 +- backend/src/reports/entities/report.entity.ts | 2 +- .../rewards/entities/reward-claim.entity.ts | 2 +- backend/src/session/session.entity.ts | 2 +- .../streak/entities/streak-activity.entity.ts | 2 +- backend/src/streak/entities/streak.entity.ts | 2 +- backend/src/time-trial/time-trial.entity.ts | 2 +- .../entities/activity-log.entity.ts | 2 +- .../entities/user-ranking.entity.ts | 2 +- .../user-reaction/entities/reaction.entity.ts | 2 +- .../entities/user-report-card.entity.ts | 2 +- .../entities/user-settings.entity.ts | 2 +- .../entities/token-history.entity.ts | 2 +- frontend/app/global-error.tsx | 41 ++++++++++++ frontend/components/Footer.jsx | 9 +-- frontend/lib/socials.ts | 5 ++ onchain/contracts/stellar_hunts/src/lib.rs | 23 +++++++ onchain/contracts/stellar_hunts/src/test.rs | 3 +- 40 files changed, 260 insertions(+), 44 deletions(-) create mode 100644 backend/STATUS.md create mode 100644 frontend/app/global-error.tsx create mode 100644 frontend/lib/socials.ts diff --git a/backend/STATUS.md b/backend/STATUS.md new file mode 100644 index 00000000..871654a2 --- /dev/null +++ b/backend/STATUS.md @@ -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 | diff --git a/backend/src/analytics/analytics.controller.ts b/backend/src/analytics/analytics.controller.ts index 57c49bf9..49722ea9 100644 --- a/backend/src/analytics/analytics.controller.ts +++ b/backend/src/analytics/analytics.controller.ts @@ -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> { 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') diff --git a/backend/src/analytics/analytics.service.ts b/backend/src/analytics/analytics.service.ts index 4cc7599d..ce602447 100644 --- a/backend/src/analytics/analytics.service.ts +++ b/backend/src/analytics/analytics.service.ts @@ -62,14 +62,18 @@ export class AnalyticsService { */ async getMostSolvedPuzzlesAsync( limit?: number, + offset?: number, ): Promise> { 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, diff --git a/backend/src/api-key/api-key.service.ts b/backend/src/api-key/api-key.service.ts index 0735a0ea..8204d506 100644 --- a/backend/src/api-key/api-key.service.ts +++ b/backend/src/api-key/api-key.service.ts @@ -18,6 +18,10 @@ export interface ApiKey { status: ApiKeyStatus; createdAt: Date; expiresAt?: Date; + monthlyRequestQuota: number; + rateLimitPerMinute: number; + requestsThisMonth: number; + scopedEndpoints: string[]; } @Injectable() @@ -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( @@ -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( diff --git a/backend/src/api-key/entities/api-key.entity.ts b/backend/src/api-key/entities/api-key.entity.ts index 7f33920d..2213c662 100644 --- a/backend/src/api-key/entities/api-key.entity.ts +++ b/backend/src/api-key/entities/api-key.entity.ts @@ -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; +} diff --git a/backend/src/audit-log/entities/audit-log.entity.ts b/backend/src/audit-log/entities/audit-log.entity.ts index eaa075fe..5021131b 100644 --- a/backend/src/audit-log/entities/audit-log.entity.ts +++ b/backend/src/audit-log/entities/audit-log.entity.ts @@ -5,7 +5,7 @@ export class AuditLog { @PrimaryGeneratedColumn('uuid') id: string; - @Column() + @Column({ length: 128 }) userId: string; @Column() diff --git a/backend/src/badge/entities/user-badge.entity.ts b/backend/src/badge/entities/user-badge.entity.ts index 440172fd..a200b804 100644 --- a/backend/src/badge/entities/user-badge.entity.ts +++ b/backend/src/badge/entities/user-badge.entity.ts @@ -12,7 +12,7 @@ export class UserBadge { @PrimaryGeneratedColumn() id: number; - @Column() + @Column({ length: 128 }) userId: number; @ManyToOne(() => Badge, (badge) => badge.userBadges, { eager: true }) diff --git a/backend/src/content-rating/entities/content-rating.entity.ts b/backend/src/content-rating/entities/content-rating.entity.ts index 0f1c033e..dea689fc 100644 --- a/backend/src/content-rating/entities/content-rating.entity.ts +++ b/backend/src/content-rating/entities/content-rating.entity.ts @@ -14,7 +14,7 @@ export class ContentRating { @PrimaryGeneratedColumn('uuid') id: string; - @Column() + @Column({ length: 128 }) @Index() userId: string; diff --git a/backend/src/daily-reward/entities/daily-reward-log.entity.ts b/backend/src/daily-reward/entities/daily-reward-log.entity.ts index 2507e382..591d3399 100644 --- a/backend/src/daily-reward/entities/daily-reward-log.entity.ts +++ b/backend/src/daily-reward/entities/daily-reward-log.entity.ts @@ -6,7 +6,7 @@ export class DailyRewardLog { id: string; @Index() - @Column() + @Column({ length: 128 }) userId: string; @Column({ default: 1 }) diff --git a/backend/src/feedback/entities/feedback.entity.ts b/backend/src/feedback/entities/feedback.entity.ts index b8b48899..5148cee2 100644 --- a/backend/src/feedback/entities/feedback.entity.ts +++ b/backend/src/feedback/entities/feedback.entity.ts @@ -34,6 +34,7 @@ export class Feedback { @Column({ type: "uuid", nullable: true, + length: 128, }) userId: string // null if anonymous diff --git a/backend/src/gameMechanics/entities/challenge-completion.entity.ts b/backend/src/gameMechanics/entities/challenge-completion.entity.ts index 01b58c23..1c7d114a 100644 --- a/backend/src/gameMechanics/entities/challenge-completion.entity.ts +++ b/backend/src/gameMechanics/entities/challenge-completion.entity.ts @@ -7,7 +7,7 @@ export class ChallengeCompletion { @PrimaryGeneratedColumn("uuid") id: string - @Column("uuid") + @Column({ type: "uuid", length: 128 }) userId: string @Column("uuid") diff --git a/backend/src/gameMechanics/entities/hint-usage.entity.ts b/backend/src/gameMechanics/entities/hint-usage.entity.ts index 0ee192a9..b3346281 100644 --- a/backend/src/gameMechanics/entities/hint-usage.entity.ts +++ b/backend/src/gameMechanics/entities/hint-usage.entity.ts @@ -7,7 +7,7 @@ export class HintUsage { @PrimaryGeneratedColumn("uuid") id: string - @Column("uuid") + @Column({ type: "uuid", length: 128 }) userId: string @Column("uuid") diff --git a/backend/src/gameMechanics/entities/puzzle-submission.entity.ts b/backend/src/gameMechanics/entities/puzzle-submission.entity.ts index 2318719f..5406f0c6 100644 --- a/backend/src/gameMechanics/entities/puzzle-submission.entity.ts +++ b/backend/src/gameMechanics/entities/puzzle-submission.entity.ts @@ -7,7 +7,7 @@ export class PuzzleSubmission { @PrimaryGeneratedColumn("uuid") id: string - @Column("uuid") + @Column({ type: "uuid", length: 128 }) userId: string @Column("uuid") diff --git a/backend/src/milestone/entities/user-milestone.entity.ts b/backend/src/milestone/entities/user-milestone.entity.ts index 39b228f1..6d2332a5 100644 --- a/backend/src/milestone/entities/user-milestone.entity.ts +++ b/backend/src/milestone/entities/user-milestone.entity.ts @@ -10,7 +10,7 @@ export class UserMilestone { @PrimaryGeneratedColumn("uuid") id: string - @Column({ type: "uuid" }) + @Column({ type: "uuid", length: 128 }) userId: string @Column({ type: "uuid" }) diff --git a/backend/src/milestone/entities/user-progress.entity.ts b/backend/src/milestone/entities/user-progress.entity.ts index 29636ade..c782be06 100644 --- a/backend/src/milestone/entities/user-progress.entity.ts +++ b/backend/src/milestone/entities/user-progress.entity.ts @@ -9,7 +9,7 @@ export class UserProgress { @PrimaryGeneratedColumn("uuid") id: string - @Column({ type: "uuid" }) + @Column({ type: "uuid", length: 128 }) userId: string @Column({ diff --git a/backend/src/multiplayer-queue/entities/queue.entity.ts b/backend/src/multiplayer-queue/entities/queue.entity.ts index 34a336aa..ee6b3dba 100644 --- a/backend/src/multiplayer-queue/entities/queue.entity.ts +++ b/backend/src/multiplayer-queue/entities/queue.entity.ts @@ -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 }) diff --git a/backend/src/progress/entities/progress.entity.ts b/backend/src/progress/entities/progress.entity.ts index 9a937e8b..4949d6f7 100644 --- a/backend/src/progress/entities/progress.entity.ts +++ b/backend/src/progress/entities/progress.entity.ts @@ -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 }) diff --git a/backend/src/puzzle-access-log/entities/puzzle-access-log.entity.ts b/backend/src/puzzle-access-log/entities/puzzle-access-log.entity.ts index d3222e13..c9b2b5c2 100644 --- a/backend/src/puzzle-access-log/entities/puzzle-access-log.entity.ts +++ b/backend/src/puzzle-access-log/entities/puzzle-access-log.entity.ts @@ -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 diff --git a/backend/src/puzzle-dependency/entities/puzzle-completion.entity.ts b/backend/src/puzzle-dependency/entities/puzzle-completion.entity.ts index 2221c687..3c86c9a8 100644 --- a/backend/src/puzzle-dependency/entities/puzzle-completion.entity.ts +++ b/backend/src/puzzle-dependency/entities/puzzle-completion.entity.ts @@ -6,7 +6,7 @@ export class PuzzleCompletion { @PrimaryGeneratedColumn('uuid') id: string; - @Column({ name: 'user_id' }) + @Column({ name: 'user_id', length: 128 }) @Index() userId: string; diff --git a/backend/src/puzzle-review/puzzle-review/entities/puzzle-review.entity.ts b/backend/src/puzzle-review/puzzle-review/entities/puzzle-review.entity.ts index 65fa3c3e..29b30fa1 100644 --- a/backend/src/puzzle-review/puzzle-review/entities/puzzle-review.entity.ts +++ b/backend/src/puzzle-review/puzzle-review/entities/puzzle-review.entity.ts @@ -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 }) diff --git a/backend/src/puzzle-test-case/entities/validation-result.entity.ts b/backend/src/puzzle-test-case/entities/validation-result.entity.ts index a55c896c..6a204dd2 100644 --- a/backend/src/puzzle-test-case/entities/validation-result.entity.ts +++ b/backend/src/puzzle-test-case/entities/validation-result.entity.ts @@ -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" }) diff --git a/backend/src/referral/entities/referral-bonus.entity.ts b/backend/src/referral/entities/referral-bonus.entity.ts index d3896f41..bcef2ac2 100644 --- a/backend/src/referral/entities/referral-bonus.entity.ts +++ b/backend/src/referral/entities/referral-bonus.entity.ts @@ -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" }) diff --git a/backend/src/referral/entities/referral-code.entity.ts b/backend/src/referral/entities/referral-code.entity.ts index 7b7c6e1e..9009d135 100644 --- a/backend/src/referral/entities/referral-code.entity.ts +++ b/backend/src/referral/entities/referral-code.entity.ts @@ -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" }) diff --git a/backend/src/reports/entities/report.entity.ts b/backend/src/reports/entities/report.entity.ts index 6e9ca96e..f2b546cc 100644 --- a/backend/src/reports/entities/report.entity.ts +++ b/backend/src/reports/entities/report.entity.ts @@ -24,7 +24,7 @@ export class Report { @Column() puzzleId: number; - @Column() + @Column({ length: 128 }) userId: number; @Column() diff --git a/backend/src/rewards/entities/reward-claim.entity.ts b/backend/src/rewards/entities/reward-claim.entity.ts index 0a7b0e51..cc49a70e 100644 --- a/backend/src/rewards/entities/reward-claim.entity.ts +++ b/backend/src/rewards/entities/reward-claim.entity.ts @@ -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' }) diff --git a/backend/src/session/session.entity.ts b/backend/src/session/session.entity.ts index 48f09bcd..a5e65036 100644 --- a/backend/src/session/session.entity.ts +++ b/backend/src/session/session.entity.ts @@ -6,7 +6,7 @@ export class Session { @PrimaryGeneratedColumn('uuid') sessionId: string; - @Column({ type: 'uuid' }) + @Column({ type: 'uuid', length: 128 }) @Index() userId: string; diff --git a/backend/src/streak/entities/streak-activity.entity.ts b/backend/src/streak/entities/streak-activity.entity.ts index a5dc3542..150d0574 100644 --- a/backend/src/streak/entities/streak-activity.entity.ts +++ b/backend/src/streak/entities/streak-activity.entity.ts @@ -24,7 +24,7 @@ export class StreakActivity { @JoinColumn({ name: "streakId" }) streak: Streak - @Column({ type: "uuid" }) + @Column({ type: "uuid", length: 128 }) userId: string @Column({ type: "date" }) diff --git a/backend/src/streak/entities/streak.entity.ts b/backend/src/streak/entities/streak.entity.ts index 3d6124d2..253c57f3 100644 --- a/backend/src/streak/entities/streak.entity.ts +++ b/backend/src/streak/entities/streak.entity.ts @@ -9,7 +9,7 @@ export class Streak { @PrimaryGeneratedColumn("uuid") id: string - @Column({ type: "uuid" }) + @Column({ type: "uuid", length: 128 }) userId: string @Column({ type: "int", default: 0 }) diff --git a/backend/src/time-trial/time-trial.entity.ts b/backend/src/time-trial/time-trial.entity.ts index e392279c..df1e0780 100644 --- a/backend/src/time-trial/time-trial.entity.ts +++ b/backend/src/time-trial/time-trial.entity.ts @@ -5,7 +5,7 @@ export class TimeTrial { @PrimaryGeneratedColumn("uuid") id: string - @Column("uuid") + @Column({ type: "uuid", length: 128 }) @Index() userId: string diff --git a/backend/src/user-activity-log/entities/activity-log.entity.ts b/backend/src/user-activity-log/entities/activity-log.entity.ts index f8e4cd3e..18bc13ad 100644 --- a/backend/src/user-activity-log/entities/activity-log.entity.ts +++ b/backend/src/user-activity-log/entities/activity-log.entity.ts @@ -11,7 +11,7 @@ export class ActivityLog { @PrimaryGeneratedColumn('uuid') id: string; - @Column() + @Column({ length: 128 }) @Index() userId: string; diff --git a/backend/src/user-ranking/entities/user-ranking.entity.ts b/backend/src/user-ranking/entities/user-ranking.entity.ts index f6570a8a..a2db8439 100644 --- a/backend/src/user-ranking/entities/user-ranking.entity.ts +++ b/backend/src/user-ranking/entities/user-ranking.entity.ts @@ -5,7 +5,7 @@ export class UserRank { @PrimaryGeneratedColumn('uuid') id: string; - @Column() + @Column({ length: 128 }) @Index({ unique: true }) userId: string; diff --git a/backend/src/user-reaction/entities/reaction.entity.ts b/backend/src/user-reaction/entities/reaction.entity.ts index 71232e36..5ca7f9c0 100644 --- a/backend/src/user-reaction/entities/reaction.entity.ts +++ b/backend/src/user-reaction/entities/reaction.entity.ts @@ -8,7 +8,7 @@ export class Reaction { @PrimaryGeneratedColumn("uuid") id: string - @Column({ type: "uuid" }) + @Column({ type: "uuid", length: 128 }) userId: string @Column({ type: "varchar", length: 255 }) diff --git a/backend/src/user-report-card/entities/user-report-card.entity.ts b/backend/src/user-report-card/entities/user-report-card.entity.ts index 6f0b259b..944aeabb 100644 --- a/backend/src/user-report-card/entities/user-report-card.entity.ts +++ b/backend/src/user-report-card/entities/user-report-card.entity.ts @@ -5,7 +5,7 @@ export class ReportCard { @PrimaryGeneratedColumn('uuid') id: string; - @Column({ unique: true }) + @Column({ unique: true, length: 128 }) userId: string; @Column('int', { default: 0 }) diff --git a/backend/src/user-settings/entities/user-settings.entity.ts b/backend/src/user-settings/entities/user-settings.entity.ts index ea766f22..9b72095c 100644 --- a/backend/src/user-settings/entities/user-settings.entity.ts +++ b/backend/src/user-settings/entities/user-settings.entity.ts @@ -43,7 +43,7 @@ export class UserSettings { @PrimaryGeneratedColumn("uuid") id: string - @Column({ type: "uuid" }) + @Column({ type: "uuid", length: 128 }) userId: string // Display & Theme Settings diff --git a/backend/src/user-token-history/entities/token-history.entity.ts b/backend/src/user-token-history/entities/token-history.entity.ts index 53dfc10f..a6ec517a 100644 --- a/backend/src/user-token-history/entities/token-history.entity.ts +++ b/backend/src/user-token-history/entities/token-history.entity.ts @@ -23,7 +23,7 @@ export class TokenHistory { @PrimaryGeneratedColumn("uuid") id: string - @Column({ type: "uuid" }) + @Column({ type: "uuid", length: 128 }) userId: string @Column({ type: "varchar", length: 64, unique: true }) diff --git a/frontend/app/global-error.tsx b/frontend/app/global-error.tsx new file mode 100644 index 00000000..aab444c4 --- /dev/null +++ b/frontend/app/global-error.tsx @@ -0,0 +1,41 @@ +"use client"; +import React from "react"; +import Link from "next/link"; +import { Button } from "@/components/ui/button"; +import { Home, RefreshCcw } from "lucide-react"; + +export default function GlobalError({ error, reset }) { + const reload = () => { + window.location.reload(); + }; + + return ( + + +
+
+
+

Error

+
+

Oh no!

+

A critical error occurred.

+
+
+ + +
+
+
+
+ + + ); +} diff --git a/frontend/components/Footer.jsx b/frontend/components/Footer.jsx index 553bb4d4..bfa3bb98 100644 --- a/frontend/components/Footer.jsx +++ b/frontend/components/Footer.jsx @@ -1,6 +1,7 @@ import React from "react"; import Link from "next/link"; import { Github, Twitter, Linkedin, ExternalLink } from "lucide-react"; +import { SOCIAL_LINKS } from "@/lib/socials"; const CURRENT_YEAR = new Date().getFullYear(); @@ -52,7 +53,7 @@ const Footer = () => {

Community

{ { { {/* Copyright */}

- © {new Date().getFullYear()} StellarHunts. All rights + © {CURRENT_YEAR} StellarHunts. All rights reserved.

diff --git a/frontend/lib/socials.ts b/frontend/lib/socials.ts new file mode 100644 index 00000000..f7c77cad --- /dev/null +++ b/frontend/lib/socials.ts @@ -0,0 +1,5 @@ +export const SOCIAL_LINKS = { + TWITTER_URL: process.env.NEXT_PUBLIC_TWITTER_URL || 'https://twitter.com/stellarhunts', + LINKEDIN_URL: process.env.NEXT_PUBLIC_LINKEDIN_URL || 'https://linkedin.com/company/stellarhunts', + GITHUB_URL: process.env.NEXT_PUBLIC_GITHUB_URL || 'https://github.com/UnityChainxx/StellarHunts', +}; diff --git a/onchain/contracts/stellar_hunts/src/lib.rs b/onchain/contracts/stellar_hunts/src/lib.rs index 1e460da7..94d808a6 100644 --- a/onchain/contracts/stellar_hunts/src/lib.rs +++ b/onchain/contracts/stellar_hunts/src/lib.rs @@ -24,6 +24,7 @@ pub struct Question { pub hashed_answer: BytesN<32>, pub level: Levels, pub hint: Bytes, + pub version: u32, } #[contracttype] @@ -65,6 +66,21 @@ pub enum DataKey { QuestionPerLevelIndex(Levels), PlayerProgress(Address), PlayerLevelProgress(Address, Levels), + SchemaVersion, +} + +// --------------------------------------------------------------------- +// Schema version +// --------------------------------------------------------------------- + +const CURRENT_SCHEMA_VERSION: u32 = 1; + +fn get_schema_version(e: &Env) -> u32 { + e.storage().persistent().get(&DataKey::SchemaVersion).unwrap_or(0) +} + +fn set_schema_version(e: &Env) { + e.storage().persistent().set(&DataKey::SchemaVersion, &CURRENT_SCHEMA_VERSION); } // --------------------------------------------------------------------- @@ -104,6 +120,7 @@ impl StellarHunts { } admin.require_auth(); env.storage().instance().set(&DataKey::Admin, &admin); + set_schema_version(&env); } // ----------------------------------------------------------------- @@ -134,6 +151,7 @@ impl StellarHunts { hashed_answer: hashed, level: level.clone(), hint: hint.clone(), + version: CURRENT_SCHEMA_VERSION, }; env.storage() .persistent() @@ -263,6 +281,7 @@ impl StellarHunts { hashed_answer: hashed, level: level.clone(), hint, + version: existing.version, }; env.storage().persistent().set(&existing_key, &updated); @@ -573,6 +592,10 @@ impl StellarHunts { level.next() } + pub fn get_schema_version(e: Env) -> u32 { + get_schema_version(&e) + } + // ----------------------------------------------------------------- // Internal // ----------------------------------------------------------------- diff --git a/onchain/contracts/stellar_hunts/src/test.rs b/onchain/contracts/stellar_hunts/src/test.rs index 831a4c80..544626b5 100644 --- a/onchain/contracts/stellar_hunts/src/test.rs +++ b/onchain/contracts/stellar_hunts/src/test.rs @@ -4,9 +4,8 @@ use crate::{StellarHunts, StellarHuntsClient}; // Brings `Address::generate` into scope as an extension trait method. use soroban_sdk::testutils::Address as _; use soroban_sdk::testutils::Ledger; -use soroban_sdk::{Address, Bytes, Env}; +use soroban_sdk::{Address, Bytes, BytesN, Env}; use soroban_sdk::testutils::{MockAuth, MockAuthInvoke}; -use soroban_sdk::{Address, Bytes, BytesN, Env, Vec}; /// Generate a fresh admin address (distinct from the destructured binding /// returned by `init_with_admin`).