diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..f1d755c --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,89 @@ +name: CI + +on: + push: + branches: + - main + - develop + pull_request: + branches: + - main + - develop + +jobs: + lint-imports: + name: lint-imports + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Lint absolute imports + shell: bash + run: | + set -euo pipefail + pattern="^[[:space:]]*(import|export)[^;]*from[[:space:]]+['\"]src/|^[[:space:]]*import[[:space:]]+['\"]src/" + if command -v rg >/dev/null 2>&1; then + matches=$(rg -n \ + --glob '!**/node_modules/**' \ + --glob '!**/dist/**' \ + --glob '!**/build/**' \ + --glob '!**/.next/**' \ + --glob '!**/coverage/**' \ + --glob '!**/out/**' \ + --glob '!**/tmp/**' \ + --glob '!**/.turbo/**' \ + --glob '!**/.cache/**' \ + --glob '*.ts' \ + --glob '*.tsx' \ + "$pattern" . || true) + else + matches=$(grep -RIn \ + --include='*.ts' \ + --include='*.tsx' \ + --exclude-dir=node_modules \ + --exclude-dir=dist \ + --exclude-dir=build \ + --exclude-dir=.next \ + --exclude-dir=coverage \ + --exclude-dir=out \ + --exclude-dir=tmp \ + --exclude-dir=.turbo \ + --exclude-dir=.cache \ + -E "$pattern" . || true) + fi + if [ -n "$matches" ]; then + echo "ERROR: Absolute imports from \"src/\" are not allowed. Use relative paths instead." + echo "$matches" + exit 1 + fi + + build: + name: build + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 20.x + cache: npm + - name: Install dependencies + run: npm ci + - name: Build frontend + run: npm --workspace frontend run build + - name: Build backend + run: npm --workspace backend run build + + type-check: + name: type-check + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 18 + cache: npm + - name: Install dependencies + run: npm ci + - name: Type-check frontend + run: npm --workspace frontend exec -- tsc --noEmit -p tsconfig.json + - name: Type-check backend + run: npm --workspace backend exec -- tsc --noEmit -p tsconfig.json diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..2c556c9 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,35 @@ +# Contributing + +## Import Guidelines +Rule: Always use relative imports. + +Bad: +```ts +import X from "src/components/X"; +``` + +Good: +```ts +import X from "../../components/X"; +``` + +CI will reject PRs containing src/* imports. + +Issue/PR: https://github.com/MindBlockLabs/mindBlock_app/pull/0000 (placeholder) + +**MUST RUN** Local check to before submitting a pr: +```bash +npm ci +npm --workspace frontend run build +npm --workspace backend run build + +npm --workspace frontend run lint +npm --workspace backend run lint + +npm --workspace frontend exec -- tsc --noEmit -p tsconfig.json +npm --workspace backend exec -- tsc --noEmit -p tsconfig.json. +``` + +## Branch Protection +main and develop require status checks: lint-imports, build, type-check. +Require branches to be up-to-date before merging. diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts index d4d0100..7ef8089 100644 --- a/backend/src/app.module.ts +++ b/backend/src/app.module.ts @@ -32,8 +32,22 @@ import { CategoriesModule } from './categories/categories.module'; TypeOrmModule.forRootAsync({ imports: [ConfigModule], inject: [ConfigService], - useFactory: async (configService: ConfigService) => { - const dbConfig = configService.get('database'); + useFactory: (configService: ConfigService) => { + interface DatabaseConfig { + url?: string; + host?: string; + port?: number; + user?: string; + password?: string; + name?: string; + autoload?: boolean; + synchronize?: boolean; + } + const dbConfig = configService.get('database'); + + if (!dbConfig) { + throw new Error('Database configuration not found'); + } // If DATABASE_URL is set, use connection string (production) if (dbConfig.url) { diff --git a/backend/src/auth/authConfig/jwt.config.ts b/backend/src/auth/authConfig/jwt.config.ts index 8ecdcca..da089b5 100644 --- a/backend/src/auth/authConfig/jwt.config.ts +++ b/backend/src/auth/authConfig/jwt.config.ts @@ -9,4 +9,4 @@ export default registerAs('jwt', () => { issuer: process.env.JWT_TOKEN_ISSUER, ttl: parseInt(process.env.JWT_ACCESS_TOKEN_TTL ?? '3600'), }; -}); \ No newline at end of file +}); diff --git a/backend/src/auth/constants/auth.constant.ts b/backend/src/auth/constants/auth.constant.ts index 80c5207..8ae1eba 100644 --- a/backend/src/auth/constants/auth.constant.ts +++ b/backend/src/auth/constants/auth.constant.ts @@ -1,5 +1,5 @@ /**request user key */ -export const REQUEST_USER_KEY = 'user' +export const REQUEST_USER_KEY = 'user'; /**auth type key */ -export const AUTH_TYPE_KEY = 'auth' \ No newline at end of file +export const AUTH_TYPE_KEY = 'auth'; diff --git a/backend/src/auth/controllers/auth.controller.ts b/backend/src/auth/controllers/auth.controller.ts index ed06925..3982307 100644 --- a/backend/src/auth/controllers/auth.controller.ts +++ b/backend/src/auth/controllers/auth.controller.ts @@ -90,10 +90,10 @@ export class AuthController { status: 400, description: 'Invalid Stellar wallet address format', }) - public async generateStellarWalletNonce( + public generateStellarWalletNonce( @Query('walletAddress') walletAddress: string, - ): Promise { - return await this.authservice.generateNonce(walletAddress); + ): NonceResponseDto { + return this.authservice.generateNonce(walletAddress); } @Get('/stellar-wallet-nonce/status') @@ -127,8 +127,8 @@ export class AuthController { }, }, }) - public async checkNonceStatus(@Query('nonce') nonce: string) { - return await this.authservice.checkNonceStatus(nonce); + public checkNonceStatus(@Query('nonce') nonce: string) { + return this.authservice.checkNonceStatus(nonce); } @Post('/forgot-password') diff --git a/backend/src/auth/decorators/activeUser.decorator.ts b/backend/src/auth/decorators/activeUser.decorator.ts index 167257f..f18a678 100644 --- a/backend/src/auth/decorators/activeUser.decorator.ts +++ b/backend/src/auth/decorators/activeUser.decorator.ts @@ -1,4 +1,3 @@ - import { createParamDecorator, ExecutionContext } from '@nestjs/common'; import { REQUEST_USER_KEY } from '../constants/auth.constant'; import { ActiveUserData } from '../interfaces/activeInterface'; @@ -6,13 +5,13 @@ import { ActiveUserData } from '../interfaces/activeInterface'; /**Active user class */ export const ActiveUser = createParamDecorator( (field: keyof ActiveUserData | undefined, ctx: ExecutionContext) => { - const request = ctx.switchToHttp().getRequest(); + const request = ctx.switchToHttp().getRequest>(); - const user: ActiveUserData = request[REQUEST_USER_KEY] + const user = request[REQUEST_USER_KEY] as ActiveUserData; console.log('ActiveUser decorator - user:', user); console.log('ActiveUser decorator - field:', field); console.log('ActiveUser decorator - value:', field ? user?.[field] : user); - return field ? user?.[field] : user + return field ? user?.[field] : user; }, ); diff --git a/backend/src/auth/decorators/auth.decorator.ts b/backend/src/auth/decorators/auth.decorator.ts index e83c56d..74d0df4 100644 --- a/backend/src/auth/decorators/auth.decorator.ts +++ b/backend/src/auth/decorators/auth.decorator.ts @@ -9,4 +9,4 @@ export const Auth = (...authTypes: authType[]) => { SetMetadata(AUTH_TYPE_KEY, authTypes), UseGuards(AuthGuard('jwt')), // ← This applies the Passport JWT guard ); -}; \ No newline at end of file +}; diff --git a/backend/src/auth/decorators/role-decorator.ts b/backend/src/auth/decorators/role-decorator.ts index 6cc95f4..3643f10 100644 --- a/backend/src/auth/decorators/role-decorator.ts +++ b/backend/src/auth/decorators/role-decorator.ts @@ -3,4 +3,4 @@ import { Role } from '../enum/roles.enum'; export const ROLES_KEY = 'roles'; export const RoleDecorator = (...roles: [Role, ...Role[]]) => - SetMetadata(ROLES_KEY, roles); \ No newline at end of file + SetMetadata(ROLES_KEY, roles); diff --git a/backend/src/auth/dtos/login.dto.ts b/backend/src/auth/dtos/login.dto.ts index 89a5d12..44a7463 100644 --- a/backend/src/auth/dtos/login.dto.ts +++ b/backend/src/auth/dtos/login.dto.ts @@ -1,13 +1,13 @@ -import { ApiProperty } from "@nestjs/swagger"; -import { IsEmail, IsString, MinLength } from "class-validator"; +import { ApiProperty } from '@nestjs/swagger'; +import { IsEmail, IsString, MinLength } from 'class-validator'; export class LoginDto { - @ApiProperty({ example: 'user@example.com' }) - @IsEmail() - email: string; - - @ApiProperty({ example: 'SecurePassword123!' }) - @IsString() - @MinLength(8) - password: string; - } \ No newline at end of file + @ApiProperty({ example: 'user@example.com' }) + @IsEmail() + email: string; + + @ApiProperty({ example: 'SecurePassword123!' }) + @IsString() + @MinLength(8) + password: string; +} diff --git a/backend/src/auth/dtos/nonceResponse.dto.ts b/backend/src/auth/dtos/nonceResponse.dto.ts index 08f757c..4b56951 100644 --- a/backend/src/auth/dtos/nonceResponse.dto.ts +++ b/backend/src/auth/dtos/nonceResponse.dto.ts @@ -1,15 +1,15 @@ -import { ApiProperty } from "@nestjs/swagger"; +import { ApiProperty } from '@nestjs/swagger'; export class NonceResponseDto { - @ApiProperty({ + @ApiProperty({ example: 'nonce_1693123456789_abc123_456789', - description: 'Unique nonce to be signed by the wallet' + description: 'Unique nonce to be signed by the wallet', }) nonce: string; - @ApiProperty({ + @ApiProperty({ example: 1693123456789, - description: 'Unix timestamp when the nonce expires' + description: 'Unix timestamp when the nonce expires', }) expiresAt: number; -} \ No newline at end of file +} diff --git a/backend/src/auth/dtos/refreshTokenDto.ts b/backend/src/auth/dtos/refreshTokenDto.ts index e84a7dc..d2848f3 100644 --- a/backend/src/auth/dtos/refreshTokenDto.ts +++ b/backend/src/auth/dtos/refreshTokenDto.ts @@ -1,15 +1,18 @@ -import { IsNotEmpty, IsString } from "class-validator"; -import { ApiProperty } from "@nestjs/swagger"; +import { IsNotEmpty, IsString } from 'class-validator'; +import { ApiProperty } from '@nestjs/swagger'; /** * Data Transfer Object (DTO) for refresh token. */ export class RefreshTokenDto { - /** - * The refresh token used for authentication. - */ - @ApiProperty({ description: "The refresh token used for authentication.", example: "some-refresh-token" }) - @IsString() - @IsNotEmpty() - refreshToken: string; -} \ No newline at end of file + /** + * The refresh token used for authentication. + */ + @ApiProperty({ + description: 'The refresh token used for authentication.', + example: 'some-refresh-token', + }) + @IsString() + @IsNotEmpty() + refreshToken: string; +} diff --git a/backend/src/auth/dtos/register.dto.ts b/backend/src/auth/dtos/register.dto.ts index 1322f45..d72c2f6 100644 --- a/backend/src/auth/dtos/register.dto.ts +++ b/backend/src/auth/dtos/register.dto.ts @@ -1,23 +1,23 @@ -import { ApiProperty } from "@nestjs/swagger"; -import { IsEmail, IsOptional, IsString, MinLength } from "class-validator"; +import { ApiProperty } from '@nestjs/swagger'; +import { IsEmail, IsOptional, IsString, MinLength } from 'class-validator'; export class RegisterDto { - @ApiProperty({ example: 'user@example.com' }) - @IsEmail() - email: string; - - @ApiProperty({ example: 'SecurePassword123!' }) - @IsString() - @MinLength(8) - password: string; - - @ApiProperty({ example: '0xWalletAddress' }) - @IsOptional() - @IsString() - walletAddress?: string; - - @ApiProperty({ example: 'GoogleOAuthToken' }) - @IsOptional() - @IsString() - googleToken?: string; - } \ No newline at end of file + @ApiProperty({ example: 'user@example.com' }) + @IsEmail() + email: string; + + @ApiProperty({ example: 'SecurePassword123!' }) + @IsString() + @MinLength(8) + password: string; + + @ApiProperty({ example: '0xWalletAddress' }) + @IsOptional() + @IsString() + walletAddress?: string; + + @ApiProperty({ example: 'GoogleOAuthToken' }) + @IsOptional() + @IsString() + googleToken?: string; +} diff --git a/backend/src/auth/dtos/walletLogin.dto.ts b/backend/src/auth/dtos/walletLogin.dto.ts index 63dd9cf..b939c7a 100644 --- a/backend/src/auth/dtos/walletLogin.dto.ts +++ b/backend/src/auth/dtos/walletLogin.dto.ts @@ -12,27 +12,28 @@ export class StellarWalletLoginDto { }) walletAddress: string; - @ApiProperty({ + @ApiProperty({ example: 'base64SignatureString==', description: 'Base64 encoded ed25519 signature', }) @IsString() signature: string; - @ApiProperty({ + @ApiProperty({ example: 'stellar_nonce_1693123456789_abc123_BTODB4A', - description: 'Server-generated nonce for this authentication attempt' + description: 'Server-generated nonce for this authentication attempt', }) @IsString() nonce: string; - @ApiProperty({ + @ApiProperty({ example: 'GAHK7EEG2WWHVKDNT4CEQFZGKF2LGDSW2IVM4S5DP42RBW3K6BTODB4A', - description: 'Stellar public key (same as wallet address for account-based wallets)' + description: + 'Stellar public key (same as wallet address for account-based wallets)', }) @IsString() - @Matches(/^[GM][A-Z2-7]{55}$/, { - message: 'Invalid Stellar public key format' + @Matches(/^[GM][A-Z2-7]{55}$/, { + message: 'Invalid Stellar public key format', }) publicKey: string; -} \ No newline at end of file +} diff --git a/backend/src/auth/enum/authProvider.enum.ts b/backend/src/auth/enum/authProvider.enum.ts index 681904e..1bd5dce 100644 --- a/backend/src/auth/enum/authProvider.enum.ts +++ b/backend/src/auth/enum/authProvider.enum.ts @@ -2,4 +2,4 @@ export enum AuthProvider { LOCAL = 'local', GOOGLE = 'google', WALLET = 'wallet', -} \ No newline at end of file +} diff --git a/backend/src/auth/enum/roles.enum.ts b/backend/src/auth/enum/roles.enum.ts index fbb3a92..de82e84 100644 --- a/backend/src/auth/enum/roles.enum.ts +++ b/backend/src/auth/enum/roles.enum.ts @@ -2,4 +2,4 @@ export enum Role { Admin = 'admin', Moderator = 'moderator', User = 'user', -} \ No newline at end of file +} diff --git a/backend/src/auth/interfaces/activeInterface.ts b/backend/src/auth/interfaces/activeInterface.ts index b5619f7..ccb040a 100644 --- a/backend/src/auth/interfaces/activeInterface.ts +++ b/backend/src/auth/interfaces/activeInterface.ts @@ -1,9 +1,8 @@ /**Active user data interface */ export interface ActiveUserData { + /**sub of type number */ + sub: string; - /**sub of type number */ - sub: string, - - /**email of type string */ - email?: string -} \ No newline at end of file + /**email of type string */ + email?: string; +} diff --git a/backend/src/auth/providers/auth.service.ts b/backend/src/auth/providers/auth.service.ts index f3f0188..165eae7 100644 --- a/backend/src/auth/providers/auth.service.ts +++ b/backend/src/auth/providers/auth.service.ts @@ -62,7 +62,7 @@ export class AuthService { } // Generate nonce for wallet authentication - public async generateNonce(walletAddress: string): Promise { + public generateNonce(walletAddress: string): NonceResponseDto { // Validate wallet address format if (!walletAddress || !this.isValidStellarAddress(walletAddress)) { throw new BadRequestException('Invalid Stellar wallet address'); @@ -86,7 +86,7 @@ export class AuthService { } // Check nonce status (useful for debugging) - public async checkNonceStatus(nonce: string) { + public checkNonceStatus(nonce: string) { const nonceData = this.nonces.get(nonce); if (!nonceData) { @@ -109,10 +109,7 @@ export class AuthService { } // Verify and mark nonce as used (called by StellarWalletLoginProvider) - public async verifyAndUseNonce( - nonce: string, - walletAddress: string, - ): Promise { + public verifyAndUseNonce(nonce: string, walletAddress: string): void { const nonceData = this.nonces.get(nonce); if (!nonceData) { diff --git a/backend/src/auth/providers/bcrypt.provider.ts b/backend/src/auth/providers/bcrypt.provider.ts index e5d38e0..0210301 100644 --- a/backend/src/auth/providers/bcrypt.provider.ts +++ b/backend/src/auth/providers/bcrypt.provider.ts @@ -4,16 +4,19 @@ import { HashingProvider } from './hashing.provider'; @Injectable() export class BcryptProvider implements HashingProvider { - // hash - public async hashPassword(inpPassword: string | Buffer): Promise { - const saltRounds = 10 - const salt = await bcrypt.genSalt(saltRounds) - - return await bcrypt.hash(inpPassword.toLocaleString(), salt) - } + // hash + public async hashPassword(inpPassword: string | Buffer): Promise { + const saltRounds = 10; + const salt = await bcrypt.genSalt(saltRounds); - // compare - public async comparePasswords(password: string, encryPassword: string): Promise { - return await bcrypt.compare(password, encryPassword) - } -} \ No newline at end of file + return await bcrypt.hash(inpPassword.toLocaleString(), salt); + } + + // compare + public async comparePasswords( + password: string, + encryPassword: string, + ): Promise { + return await bcrypt.compare(password, encryPassword); + } +} diff --git a/backend/src/auth/providers/forgot-password.provider.ts b/backend/src/auth/providers/forgot-password.provider.ts index 5382068..73c5b84 100644 --- a/backend/src/auth/providers/forgot-password.provider.ts +++ b/backend/src/auth/providers/forgot-password.provider.ts @@ -10,7 +10,7 @@ import { Repository } from 'typeorm'; import { ForgotPasswordDto } from '../dtos/forgot-password.dto'; import { MailService } from './mail.service'; import * as crypto from 'crypto'; -import { User } from 'src/users/user.entity'; +import { User } from '../../users/user.entity'; @Injectable() export class ForgotPasswordProvider { diff --git a/backend/src/auth/providers/generate-tokens.provider.ts b/backend/src/auth/providers/generate-tokens.provider.ts index 91035ed..b2acfc0 100644 --- a/backend/src/auth/providers/generate-tokens.provider.ts +++ b/backend/src/auth/providers/generate-tokens.provider.ts @@ -1,5 +1,5 @@ /* eslint-disable prettier/prettier */ -import { forwardRef, Inject, Injectable, UnauthorizedException } from '@nestjs/common'; +import { forwardRef, Inject, Injectable } from '@nestjs/common'; import { ApiTags, ApiOperation } from '@nestjs/swagger'; import { JwtService } from '@nestjs/jwt'; import jwtConfig from '../authConfig/jwt.config'; diff --git a/backend/src/auth/providers/hashing.provider.ts b/backend/src/auth/providers/hashing.provider.ts index acb60ac..2fc987a 100644 --- a/backend/src/auth/providers/hashing.provider.ts +++ b/backend/src/auth/providers/hashing.provider.ts @@ -2,9 +2,12 @@ import { Injectable } from '@nestjs/common'; @Injectable() export abstract class HashingProvider { - // hashing during signUp - abstract hashPassword(inpPassword: string | Buffer): Promise + // hashing during signUp + abstract hashPassword(inpPassword: string | Buffer): Promise; - // comparison during signIn - abstract comparePasswords(password: string, encryPassword: string): Promise -} \ No newline at end of file + // comparison during signIn + abstract comparePasswords( + password: string, + encryPassword: string, + ): Promise; +} diff --git a/backend/src/auth/providers/mail.service.ts b/backend/src/auth/providers/mail.service.ts index 50a0ee4..b2bc2de 100644 --- a/backend/src/auth/providers/mail.service.ts +++ b/backend/src/auth/providers/mail.service.ts @@ -1,14 +1,14 @@ import { Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; -import * as nodemailer from 'nodemailer'; +import { createTransport, Transporter } from 'nodemailer'; @Injectable() export class MailService { - private transporter: nodemailer.Transporter; + private transporter: Transporter; constructor(private readonly configService: ConfigService) { // Configure email transporter - this.transporter = nodemailer.createTransport({ + this.transporter = createTransport({ host: this.configService.get('MAIL_HOST'), port: this.configService.get('MAIL_PORT'), secure: this.configService.get('MAIL_SECURE'), // true for 465, false for other ports diff --git a/backend/src/auth/providers/refreshTokensProvider.ts b/backend/src/auth/providers/refreshTokensProvider.ts index d5fe24d..ddaaa8a 100644 --- a/backend/src/auth/providers/refreshTokensProvider.ts +++ b/backend/src/auth/providers/refreshTokensProvider.ts @@ -51,7 +51,7 @@ export class RefreshTokensProvider { @ApiBody({ type: RefreshTokenDto }) public async refreshTokens(refreshTokenDto: RefreshTokenDto) { // Validate the refresh token using JWT - const { sub } = await this.jwtService.verifyAsync( + const payload = await this.jwtService.verifyAsync<{ sub: string }>( refreshTokenDto.refreshToken, { secret: this.jwtConfiguration.secret, @@ -60,6 +60,8 @@ export class RefreshTokensProvider { }, ); + const sub = payload.sub; + // Retrieve the user from the database const user = await this.userService.findOneByGoogleId(sub); diff --git a/backend/src/auth/providers/reset-password.provider.ts b/backend/src/auth/providers/reset-password.provider.ts index 912a8e1..554debe 100644 --- a/backend/src/auth/providers/reset-password.provider.ts +++ b/backend/src/auth/providers/reset-password.provider.ts @@ -1,7 +1,6 @@ import { Injectable, BadRequestException, - NotFoundException, InternalServerErrorException, Logger, } from '@nestjs/common'; @@ -10,7 +9,7 @@ import { Repository, MoreThan } from 'typeorm'; import { ResetPasswordDto } from '../dtos/reset-password.dto'; import * as crypto from 'crypto'; import * as bcrypt from 'bcryptjs'; -import { User } from 'src/users/user.entity'; +import { User } from '../../users/user.entity'; @Injectable() export class ResetPasswordProvider { diff --git a/backend/src/auth/providers/sign-in.provider.ts b/backend/src/auth/providers/sign-in.provider.ts index 684a558..703b5ac 100644 --- a/backend/src/auth/providers/sign-in.provider.ts +++ b/backend/src/auth/providers/sign-in.provider.ts @@ -33,7 +33,7 @@ export class SignInProvider { public async SignIn(signInDto: LoginDto) { // check if user exist in db // throw error if user doesnt exist - let user = await this.userService.GetOneByEmail(signInDto.email); + const user = await this.userService.GetOneByEmail(signInDto.email); // conpare password let isCheckedPassword: boolean = false; diff --git a/backend/src/auth/providers/wallet-login.provider.ts b/backend/src/auth/providers/wallet-login.provider.ts index 4c3c976..1a7d185 100644 --- a/backend/src/auth/providers/wallet-login.provider.ts +++ b/backend/src/auth/providers/wallet-login.provider.ts @@ -1,4 +1,9 @@ -import { forwardRef, Inject, Injectable, UnauthorizedException } from '@nestjs/common'; +import { + forwardRef, + Inject, + Injectable, + UnauthorizedException, +} from '@nestjs/common'; import { JwtService } from '@nestjs/jwt'; import { ConfigType } from '@nestjs/config'; import { UsersService } from '../../users/providers/users.service'; @@ -29,17 +34,17 @@ export class StellarWalletLoginProvider { public async StellarWalletLogin(dto: StellarWalletLoginDto) { try { - // 1. Verify nonce hasn't been used and use it - await this.authService.verifyAndUseNonce(dto.nonce, dto.walletAddress); + // 1. Verify nonce hasn't been used and use it (synchronous method) + this.authService.verifyAndUseNonce(dto.nonce, dto.walletAddress); // 2. Create proper message to sign const message = this.createLoginMessage(dto.walletAddress, dto.nonce); // 3. Verify the signature using Stellar's ed25519 verification - const isValid = await this.verifySignature( - message, - dto.signature, - dto.publicKey + const isValid = this.verifySignature( + message, + dto.signature, + dto.publicKey, ); if (!isValid) { @@ -47,10 +52,10 @@ export class StellarWalletLoginProvider { } // 4. Verify the public key belongs to the wallet address - await this.verifyPublicKeyOwnership(dto.walletAddress, dto.publicKey); - + this.verifyPublicKeyOwnership(dto.walletAddress, dto.publicKey); } catch (err) { - throw new UnauthorizedException('Authentication failed: ' + err.message); + const errorMessage = err instanceof Error ? err.message : 'Unknown error'; + throw new UnauthorizedException('Authentication failed: ' + errorMessage); } // Check if user exists in db @@ -65,7 +70,7 @@ export class StellarWalletLoginProvider { provider: 'stellar_wallet', challengeLevel: ChallengeLevel.BEGINNER, challengeTypes: [], - ageGroup: AgeGroup.TEENS + ageGroup: AgeGroup.TEENS, }); } @@ -89,18 +94,18 @@ export class StellarWalletLoginProvider { return `Login to MyApp\nWallet: ${walletAddress}\nNonce: ${nonce}\nTimestamp: ${Date.now()}`; } - private async verifySignature( - message: string, - signature: string, - publicKey: string - ): Promise { + private verifySignature( + message: string, + signature: string, + publicKey: string, + ): boolean { try { // Convert message to buffer const messageBuffer = Buffer.from(message, 'utf8'); - + // Convert signature from base64 to buffer const signatureBuffer = Buffer.from(signature, 'base64'); - + // Convert public key from Stellar format to PEM encoded ed25519 public key const stellarKeypair = StellarSdk.Keypair.fromPublicKey(publicKey); const publicKeyBuffer = stellarKeypair.rawPublicKey(); @@ -108,7 +113,10 @@ export class StellarWalletLoginProvider { // Convert raw public key to PEM format const publicKeyPem = '-----BEGIN PUBLIC KEY-----\n' + - Buffer.from(publicKeyBuffer).toString('base64').match(/.{1,64}/g)?.join('\n') + + Buffer.from(publicKeyBuffer) + .toString('base64') + .match(/.{1,64}/g) + ?.join('\n') + '\n-----END PUBLIC KEY-----\n'; // Verify signature using ed25519 @@ -118,9 +126,9 @@ export class StellarWalletLoginProvider { { key: publicKeyPem, format: 'pem', - type: 'spki' + type: 'spki', }, - signatureBuffer + signatureBuffer, ); return isValid; @@ -130,10 +138,10 @@ export class StellarWalletLoginProvider { } } - private async verifyPublicKeyOwnership( + private verifyPublicKeyOwnership( walletAddress: string, publicKey: string, - ): Promise { + ): void { try { // Verify that the public key matches the wallet address const keypair = StellarSdk.Keypair.fromPublicKey(publicKey); @@ -157,9 +165,11 @@ export class StellarWalletLoginProvider { `Public key ownership verification for ${walletAddress} with key ${publicKey} - validation passed`, ); } catch (error) { + const errorMessage = + error instanceof Error ? error.message : 'Unknown error'; throw new UnauthorizedException( - `Public key verification failed: ${error.message}`, + `Public key verification failed: ${errorMessage}`, ); } } -} \ No newline at end of file +} diff --git a/backend/src/auth/social/dtos/google-token.dto.ts b/backend/src/auth/social/dtos/google-token.dto.ts index e49ded6..0591a08 100644 --- a/backend/src/auth/social/dtos/google-token.dto.ts +++ b/backend/src/auth/social/dtos/google-token.dto.ts @@ -1,11 +1,14 @@ -import { ApiProperty } from '@nestjs/swagger' -import {IsNotEmpty} from 'class-validator' +import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty } from 'class-validator'; /**google token dto class */ export class GoogleTokenDto { - - /**token of type string */ - @ApiProperty({type:'string', example:'hfsde2345bvvv', description:'An auto generated strings when you log in with you google id'}) - @IsNotEmpty() - token: string -} \ No newline at end of file + /**token of type string */ + @ApiProperty({ + type: 'string', + example: 'hfsde2345bvvv', + description: 'An auto generated strings when you log in with you google id', + }) + @IsNotEmpty() + token: string; +} diff --git a/backend/src/auth/social/google-auth.controller.ts b/backend/src/auth/social/google-auth.controller.ts index cc1cd24..615c73a 100644 --- a/backend/src/auth/social/google-auth.controller.ts +++ b/backend/src/auth/social/google-auth.controller.ts @@ -5,16 +5,16 @@ import { GoogleAuthenticationService } from './providers/google-authentication.s /**Google authentication controller class */ @Controller('auth/google-authentication') export class GoogleAuthenticationController { - constructor( - /* - * inject googleAuthenticationService - */ - private readonly googleAuthenticationService: GoogleAuthenticationService - ) {} + constructor( + /* + * inject googleAuthenticationService + */ + private readonly googleAuthenticationService: GoogleAuthenticationService, + ) {} - /**Authenticate class with body parameter of type googletokendto */ - @Post() - public authenticate(@Body() googlTokenDto: GoogleTokenDto) { - return this.googleAuthenticationService.authenticate(googlTokenDto) - } -} \ No newline at end of file + /**Authenticate class with body parameter of type googletokendto */ + @Post() + public authenticate(@Body() googlTokenDto: GoogleTokenDto) { + return this.googleAuthenticationService.authenticate(googlTokenDto); + } +} diff --git a/backend/src/auth/social/interfaces/user.interface.ts b/backend/src/auth/social/interfaces/user.interface.ts index 539806b..8484435 100644 --- a/backend/src/auth/social/interfaces/user.interface.ts +++ b/backend/src/auth/social/interfaces/user.interface.ts @@ -1,10 +1,10 @@ /**Google interface */ export interface GoogleInterface { - /**email property of type string */ - email: string + /**email property of type string */ + email: string; - username?: string + username?: string; - /**googleid property of type string */ - googleId: string -} \ No newline at end of file + /**googleid property of type string */ + googleId: string; +} diff --git a/backend/src/auth/social/providers/google-authentication.service.ts b/backend/src/auth/social/providers/google-authentication.service.ts index 2cd67ce..f902f03 100644 --- a/backend/src/auth/social/providers/google-authentication.service.ts +++ b/backend/src/auth/social/providers/google-authentication.service.ts @@ -115,7 +115,7 @@ export class GoogleAuthenticationService implements OnModuleInit { const newUser = await this.userService.createGoogleUser({ email: email, username: given_name, - // eslint-disable-next-line prettier/prettier + googleId: googleId, }); return this.generateTokensProvider.generateTokens(newUser); diff --git a/backend/src/auth/strategies/jwt.strategy.ts b/backend/src/auth/strategies/jwt.strategy.ts index a5cc18a..066207e 100644 --- a/backend/src/auth/strategies/jwt.strategy.ts +++ b/backend/src/auth/strategies/jwt.strategy.ts @@ -1,10 +1,14 @@ -import { Injectable } from '@nestjs/common'; +import { Injectable, Inject } from '@nestjs/common'; import { PassportStrategy } from '@nestjs/passport'; import { ExtractJwt, Strategy } from 'passport-jwt'; import { ConfigType } from '@nestjs/config'; -import { Inject } from '@nestjs/common'; import jwtConfig from '../authConfig/jwt.config'; -import { REQUEST_USER_KEY } from '../constants/auth.constant'; + +interface JwtPayload { + sub: string; + email: string; + username: string; +} @Injectable() export class JwtStrategy extends PassportStrategy(Strategy) { @@ -26,7 +30,7 @@ export class JwtStrategy extends PassportStrategy(Strategy) { }); } - async validate(payload: any) { + validate(payload: JwtPayload) { // This is what gets attached to request[REQUEST_USER_KEY] or request.user console.log('JWT Strategy validate payload:', payload); diff --git a/backend/src/blockchain/blockchain.module.ts b/backend/src/blockchain/blockchain.module.ts index 7d69f76..a10e592 100644 --- a/backend/src/blockchain/blockchain.module.ts +++ b/backend/src/blockchain/blockchain.module.ts @@ -5,6 +5,6 @@ import { BlockchainService } from './provider/blockchain.service'; @Module({ controllers: [BlockchainController], providers: [BlockchainService], - exports: [BlockchainService] + exports: [BlockchainService], }) export class BlockchainModule {} diff --git a/backend/src/blockchain/controller/blockchain.controller.ts b/backend/src/blockchain/controller/blockchain.controller.ts index 0de2ccd..99ecd18 100644 --- a/backend/src/blockchain/controller/blockchain.controller.ts +++ b/backend/src/blockchain/controller/blockchain.controller.ts @@ -3,11 +3,11 @@ import { BlockchainService } from '../provider/blockchain.service'; @Controller('blockchain') export class BlockchainController { - constructor(private readonly blockchainService: BlockchainService) {} + constructor(private readonly blockchainService: BlockchainService) {} - @Get() - getHello(): string { - // Call a simple method in the service - return this.blockchainService.getHello(); - } + @Get() + getHello(): string { + // Call a simple method in the service + return this.blockchainService.getHello(); + } } diff --git a/backend/src/blockchain/dtos/blockchain.dto.ts b/backend/src/blockchain/dtos/blockchain.dto.ts index 5b8d873..08ec03e 100644 --- a/backend/src/blockchain/dtos/blockchain.dto.ts +++ b/backend/src/blockchain/dtos/blockchain.dto.ts @@ -1,4 +1 @@ -export class BlockchainDTO { - - - } \ No newline at end of file +export class BlockchainDTO {} diff --git a/backend/src/blockchain/entities/blockchain.entity.ts b/backend/src/blockchain/entities/blockchain.entity.ts index 8dc021c..33121ba 100644 --- a/backend/src/blockchain/entities/blockchain.entity.ts +++ b/backend/src/blockchain/entities/blockchain.entity.ts @@ -1,3 +1 @@ -export class BlockchainEntity { - - } \ No newline at end of file +export class BlockchainEntity {} diff --git a/backend/src/blockchain/provider/blockchain.service.ts b/backend/src/blockchain/provider/blockchain.service.ts index 45685a9..ed4dbbb 100644 --- a/backend/src/blockchain/provider/blockchain.service.ts +++ b/backend/src/blockchain/provider/blockchain.service.ts @@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common'; @Injectable() export class BlockchainService { - getHello(): string { - return 'Hello from Blockchain Service'; - } + getHello(): string { + return 'Hello from Blockchain Service'; + } } diff --git a/backend/src/categories/categories.controller.ts b/backend/src/categories/categories.controller.ts index 7ffede1..6a3190b 100644 --- a/backend/src/categories/categories.controller.ts +++ b/backend/src/categories/categories.controller.ts @@ -1,9 +1,22 @@ -import { Body, Controller, Get, HttpCode, HttpStatus, Post } from '@nestjs/common'; +import { + Body, + Controller, + Get, + HttpCode, + HttpStatus, + Post, +} from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { Category } from './entities/category.entity'; import { CategoriesService } from './providers/categories.service'; -import { ApiBody, ApiOperation, ApiQuery, ApiResponse, ApiTags } from '@nestjs/swagger'; +import { + ApiBody, + ApiOperation, + ApiQuery, + ApiResponse, + ApiTags, +} from '@nestjs/swagger'; import { CreateCategoryDto } from './dtos/create-category.dto'; @ApiTags('Categories') @@ -46,9 +59,10 @@ export class CategoriesController { } @Get() - @ApiOperation({ + @ApiOperation({ summary: 'Get all categories', - description: 'Retrieve a list of all categories. Optionally filter by active status.', + description: + 'Retrieve a list of all categories. Optionally filter by active status.', }) @ApiQuery({ name: 'isActive', @@ -79,11 +93,13 @@ export class CategoriesController { count: categories.length, }; } catch (error) { + const errorMessage = + error instanceof Error ? error.message : 'Unknown error'; return { success: false, message: 'Failed to retrieve categories', - error: error.message, + error: errorMessage, }; } } -} \ No newline at end of file +} diff --git a/backend/src/categories/dtos/create-category.dto.ts b/backend/src/categories/dtos/create-category.dto.ts index a28f875..28fa949 100644 --- a/backend/src/categories/dtos/create-category.dto.ts +++ b/backend/src/categories/dtos/create-category.dto.ts @@ -39,4 +39,4 @@ export class CreateCategoryDto { @IsOptional() @IsBoolean() isActive?: boolean; -} \ No newline at end of file +} diff --git a/backend/src/categories/providers/categories.provider.ts b/backend/src/categories/providers/categories.provider.ts new file mode 100644 index 0000000..e679028 --- /dev/null +++ b/backend/src/categories/providers/categories.provider.ts @@ -0,0 +1,10 @@ +import { DataSource } from 'typeorm'; +import { Category } from '../entities/category.entity'; + +export const categoriesProviders = [ + { + provide: 'CATEGORIES_REPOSITORY', + useFactory: (dataSource: DataSource) => dataSource.getRepository(Category), + inject: ['DATA_SOURCE'], + }, +]; diff --git a/backend/src/categories/providers/categories.service.ts b/backend/src/categories/providers/categories.service.ts index 1182a5d..bd74aa0 100644 --- a/backend/src/categories/providers/categories.service.ts +++ b/backend/src/categories/providers/categories.service.ts @@ -1,4 +1,8 @@ -import { ConflictException, Injectable, InternalServerErrorException } from '@nestjs/common'; +import { + ConflictException, + Injectable, + InternalServerErrorException, +} from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { Category } from '../entities/category.entity'; @@ -13,7 +17,7 @@ export class CategoriesService { async findAll(): Promise { return this.categoryRepository.find({ - order: { name: 'ASC' } + order: { name: 'ASC' }, }); } @@ -45,7 +49,13 @@ export class CategoriesService { return await this.categoryRepository.save(category); } catch (error) { // Handle unique constraint violation (in case of race condition) - if (error.code === '23505') { // PostgreSQL unique violation code + if ( + error instanceof Error && + typeof error === 'object' && + 'code' in error && + error.code === '23505' + ) { + // PostgreSQL unique violation code throw new ConflictException( `Category with name "${createCategoryDto.name}" already exists`, ); @@ -62,4 +72,4 @@ export class CategoriesService { async remove(id: string): Promise { await this.categoryRepository.delete(id); } -} \ No newline at end of file +} diff --git a/backend/src/common/filters/http-exception.filter.ts b/backend/src/common/filters/http-exception.filter.ts index 9fd29c3..2c89d0f 100644 --- a/backend/src/common/filters/http-exception.filter.ts +++ b/backend/src/common/filters/http-exception.filter.ts @@ -1,4 +1,9 @@ -import { ExceptionFilter, Catch, ArgumentsHost, HttpException } from '@nestjs/common'; +import { + ExceptionFilter, + Catch, + ArgumentsHost, + HttpException, +} from '@nestjs/common'; import { Response } from 'express'; @Catch(HttpException) diff --git a/backend/src/common/pagination/paginatedInterfaces.ts b/backend/src/common/pagination/paginatedInterfaces.ts index 2e894e1..e47a6b7 100644 --- a/backend/src/common/pagination/paginatedInterfaces.ts +++ b/backend/src/common/pagination/paginatedInterfaces.ts @@ -1,16 +1,16 @@ export class PaginatedInterface { - data: T[] - meta: { - itemsPerPage: number, - totalItems: number, - currentPage: number, - totalPages: number, - } - links: { - first: string, - last: string, - current: string, - previous: string, - next: string - } -} \ No newline at end of file + data: T[]; + meta: { + itemsPerPage: number; + totalItems: number; + currentPage: number; + totalPages: number; + }; + links: { + first: string; + last: string; + current: string; + previous: string; + next: string; + }; +} diff --git a/backend/src/common/pagination/pagination.module.ts b/backend/src/common/pagination/pagination.module.ts index 792d11c..6e77a8c 100644 --- a/backend/src/common/pagination/pagination.module.ts +++ b/backend/src/common/pagination/pagination.module.ts @@ -3,6 +3,6 @@ import { PaginationProvider } from './provider/pagination-provider'; @Module({ providers: [PaginationProvider], - exports: [PaginationProvider] + exports: [PaginationProvider], }) -export class PaginationModule {} \ No newline at end of file +export class PaginationModule {} diff --git a/backend/src/common/pagination/provider/pagination-provider.ts b/backend/src/common/pagination/provider/pagination-provider.ts index 108521f..53dd58b 100644 --- a/backend/src/common/pagination/provider/pagination-provider.ts +++ b/backend/src/common/pagination/provider/pagination-provider.ts @@ -7,49 +7,56 @@ import { PaginatedInterface } from '../paginatedInterfaces'; @Injectable() export class PaginationProvider { - constructor( - @Inject(REQUEST) - private readonly request: Request, - ) {} + constructor( + @Inject(REQUEST) + private readonly request: Request, + ) {} public async qpaginatedQuer( paginatedQueryDto: paginationQueryDto, - repository: Repository + repository: Repository, ): Promise> { const result = await repository.find({ - skip: (paginatedQueryDto.page -1) * paginatedQueryDto.limit, - take: paginatedQueryDto.limit - }) + skip: (paginatedQueryDto.page - 1) * paginatedQueryDto.limit, + take: paginatedQueryDto.limit, + }); // create a request url - const baseUrl = this.request.protocol + '://' + this.request.headers.host + '/' - const newUrl = new URL(this.request.url, baseUrl) - console.log(newUrl) + const baseUrl = + this.request.protocol + '://' + this.request.headers.host + '/'; + const newUrl = new URL(this.request.url, baseUrl); + console.log(newUrl); /** - * calculating page number + * calculating page number */ - const totalItems = await repository.count() - const totalPages = Math.ceil(totalItems/paginatedQueryDto.limit) - const nextPage = paginatedQueryDto.page === totalPages ? paginatedQueryDto.page : paginatedQueryDto.page + 1 - const prevPage = paginatedQueryDto.page === 1 ? paginatedQueryDto.page : paginatedQueryDto.page - 1 + const totalItems = await repository.count(); + const totalPages = Math.ceil(totalItems / paginatedQueryDto.limit); + const nextPage = + paginatedQueryDto.page === totalPages + ? paginatedQueryDto.page + : paginatedQueryDto.page + 1; + const prevPage = + paginatedQueryDto.page === 1 + ? paginatedQueryDto.page + : paginatedQueryDto.page - 1; const finalResponse: PaginatedInterface = { - data: result, - meta: { - itemsPerPage: paginatedQueryDto.limit, - totalItems: totalItems, - currentPage: paginatedQueryDto.page, - totalPages: totalPages - }, - links: { - first: `${newUrl.origin}&${newUrl.pathname}?limit=${paginatedQueryDto.limit}&page=1`, - last: `${newUrl.origin}&${newUrl.pathname}?limit=${paginatedQueryDto.limit}&page=${totalPages}`, - current: `${newUrl.origin}&${newUrl.pathname}?limit=${paginatedQueryDto.limit}&page=${paginatedQueryDto.page}`, - previous: `${newUrl.origin}&${newUrl.pathname}?limit=${paginatedQueryDto.limit}&page=${prevPage}`, - next: `${newUrl.origin}&${newUrl.pathname}?limit=${paginatedQueryDto.limit}&page=${nextPage}` - } - } + data: result, + meta: { + itemsPerPage: paginatedQueryDto.limit, + totalItems: totalItems, + currentPage: paginatedQueryDto.page, + totalPages: totalPages, + }, + links: { + first: `${newUrl.origin}&${newUrl.pathname}?limit=${paginatedQueryDto.limit}&page=1`, + last: `${newUrl.origin}&${newUrl.pathname}?limit=${paginatedQueryDto.limit}&page=${totalPages}`, + current: `${newUrl.origin}&${newUrl.pathname}?limit=${paginatedQueryDto.limit}&page=${paginatedQueryDto.page}`, + previous: `${newUrl.origin}&${newUrl.pathname}?limit=${paginatedQueryDto.limit}&page=${prevPage}`, + next: `${newUrl.origin}&${newUrl.pathname}?limit=${paginatedQueryDto.limit}&page=${nextPage}`, + }, + }; - return finalResponse + return finalResponse; } -} \ No newline at end of file +} diff --git a/backend/src/config/enviroment.validation.ts b/backend/src/config/enviroment.validation.ts index 5720bb4..1a7f142 100644 --- a/backend/src/config/enviroment.validation.ts +++ b/backend/src/config/enviroment.validation.ts @@ -9,4 +9,4 @@ // DATABASE_USER: Joi.string().required().default('postgres'), // DATABASE_HOST: Joi.string().required().default(''), // DATABASE_NAME: Joi.string().required() -// }) \ No newline at end of file +// }) diff --git a/backend/src/database/migrations/20250601204322-AddDifficultyAndCategoryToIQQuestions.ts b/backend/src/database/migrations/20250601204322-AddDifficultyAndCategoryToIQQuestions.ts index 84b16ac..de998b1 100644 --- a/backend/src/database/migrations/20250601204322-AddDifficultyAndCategoryToIQQuestions.ts +++ b/backend/src/database/migrations/20250601204322-AddDifficultyAndCategoryToIQQuestions.ts @@ -1,58 +1,66 @@ -import { MigrationInterface, QueryRunner } from "typeorm" +import { MigrationInterface, QueryRunner } from 'typeorm'; -export class AddDifficultyAndCategoryToIQQuestions20250601204322 implements MigrationInterface { - name = "AddDifficultyAndCategoryToIQQuestions20250601204322" +export class AddDifficultyAndCategoryToIQQuestions20250601204322 + implements MigrationInterface +{ + name = 'AddDifficultyAndCategoryToIQQuestions20250601204322'; public async up(queryRunner: QueryRunner): Promise { // Create enum types await queryRunner.query(` CREATE TYPE "public"."question_difficulty_enum" AS ENUM('easy', 'medium', 'hard') - `) + `); await queryRunner.query(` CREATE TYPE "public"."question_category_enum" AS ENUM( 'Science', 'Mathematics', 'Logic', 'Language', 'History', 'Geography', 'Literature', 'Art', 'Sports', 'Entertainment', 'General Knowledge' ) - `) + `); // Add columns to iq_questions table await queryRunner.query(` ALTER TABLE "iq_questions" ADD COLUMN "difficulty" "public"."question_difficulty_enum" NOT NULL DEFAULT 'medium' - `) + `); await queryRunner.query(` ALTER TABLE "iq_questions" ADD COLUMN "category" "public"."question_category_enum" - `) + `); // Create indexes for better performance await queryRunner.query(` CREATE INDEX "IDX_iq_questions_difficulty" ON "iq_questions" ("difficulty") - `) + `); await queryRunner.query(` CREATE INDEX "IDX_iq_questions_category" ON "iq_questions" ("category") - `) + `); await queryRunner.query(` CREATE INDEX "IDX_iq_questions_difficulty_category" ON "iq_questions" ("difficulty", "category") - `) + `); } public async down(queryRunner: QueryRunner): Promise { // Drop indexes - await queryRunner.query(`DROP INDEX "IDX_iq_questions_difficulty_category"`) - await queryRunner.query(`DROP INDEX "IDX_iq_questions_category"`) - await queryRunner.query(`DROP INDEX "IDX_iq_questions_difficulty"`) + await queryRunner.query( + `DROP INDEX "IDX_iq_questions_difficulty_category"`, + ); + await queryRunner.query(`DROP INDEX "IDX_iq_questions_category"`); + await queryRunner.query(`DROP INDEX "IDX_iq_questions_difficulty"`); // Drop columns - await queryRunner.query(`ALTER TABLE "iq_questions" DROP COLUMN "category"`) - await queryRunner.query(`ALTER TABLE "iq_questions" DROP COLUMN "difficulty"`) + await queryRunner.query( + `ALTER TABLE "iq_questions" DROP COLUMN "category"`, + ); + await queryRunner.query( + `ALTER TABLE "iq_questions" DROP COLUMN "difficulty"`, + ); // Drop enum types - await queryRunner.query(`DROP TYPE "public"."question_category_enum"`) - await queryRunner.query(`DROP TYPE "public"."question_difficulty_enum"`) + await queryRunner.query(`DROP TYPE "public"."question_category_enum"`); + await queryRunner.query(`DROP TYPE "public"."question_difficulty_enum"`); } -} \ No newline at end of file +} diff --git a/backend/src/database/migrations/20250601204323-CreateDailyStreaksTable.ts b/backend/src/database/migrations/20250601204323-CreateDailyStreaksTable.ts index b55512e..c66b9cc 100644 --- a/backend/src/database/migrations/20250601204323-CreateDailyStreaksTable.ts +++ b/backend/src/database/migrations/20250601204323-CreateDailyStreaksTable.ts @@ -1,6 +1,8 @@ import { MigrationInterface, QueryRunner } from 'typeorm'; -export class CreateDailyStreaksTable20250601204323 implements MigrationInterface { +export class CreateDailyStreaksTable20250601204323 + implements MigrationInterface +{ name = 'CreateDailyStreaksTable20250601204323'; public async up(queryRunner: QueryRunner): Promise { @@ -40,4 +42,4 @@ export class CreateDailyStreaksTable20250601204323 implements MigrationInterface DROP TABLE "daily_streaks"; `); } -} \ No newline at end of file +} diff --git a/backend/src/progress/dtos/submit-answer.dto.ts b/backend/src/progress/dtos/submit-answer.dto.ts index 009c2e6..fca416b 100644 --- a/backend/src/progress/dtos/submit-answer.dto.ts +++ b/backend/src/progress/dtos/submit-answer.dto.ts @@ -9,7 +9,7 @@ export class SubmitAnswerDto { @IsUUID() @IsNotEmpty() userId: string; - + @ApiProperty({ description: 'Unique identifier of the puzzle being answered', example: '456e7890-e12b-34d5-a678-526614174111', @@ -27,7 +27,7 @@ export class SubmitAnswerDto { categoryId: string; @ApiProperty({ - description: 'The user\'s answer to the puzzle', + description: "The user's answer to the puzzle", example: 'A', }) @IsString() diff --git a/backend/src/progress/progress.service.ts b/backend/src/progress/progress.service.ts index 8c3d27e..07b2f3d 100644 --- a/backend/src/progress/progress.service.ts +++ b/backend/src/progress/progress.service.ts @@ -13,17 +13,25 @@ export class ProgressService { * This would typically be used in a controller or other service */ async submitAnswer(submitAnswerDto: SubmitAnswerDto) { - return this.progressCalculationProvider.processAnswerSubmission(submitAnswerDto); + return this.progressCalculationProvider.processAnswerSubmission( + submitAnswerDto, + ); } async getUserStats(userId: string, categoryId: string) { - return this.progressCalculationProvider.getUserProgressStats(userId, categoryId); + return this.progressCalculationProvider.getUserProgressStats( + userId, + categoryId, + ); } /** * Direct access to validation methods for testing */ validateAnswer(userAnswer: string, correctAnswer: string) { - return this.progressCalculationProvider.validateAnswer(userAnswer, correctAnswer); + return this.progressCalculationProvider.validateAnswer( + userAnswer, + correctAnswer, + ); } } diff --git a/backend/src/progress/providers/progress-calculation.provider.ts b/backend/src/progress/providers/progress-calculation.provider.ts index 7c26d56..4f564e7 100644 --- a/backend/src/progress/providers/progress-calculation.provider.ts +++ b/backend/src/progress/providers/progress-calculation.provider.ts @@ -16,6 +16,13 @@ export interface ProgressCalculationResult { validation: AnswerValidationResult; } +interface ProgressStatsRaw { + totalAttempts: string; + correctAttempts: string; + totalPoints: string; + averageTimeSpent: string; +} + @Injectable() export class ProgressCalculationProvider { constructor( @@ -100,7 +107,9 @@ export class ProgressCalculationProvider { }); if (!puzzle) { - throw new NotFoundException(`Puzzle with ID ${submitAnswerDto.puzzleId} not found`); + throw new NotFoundException( + `Puzzle with ID ${submitAnswerDto.puzzleId} not found`, + ); } if (recentAttempt) { @@ -157,16 +166,17 @@ export class ProgressCalculationProvider { .addSelect('AVG(progress.timeSpent)', 'averageTimeSpent') .where('progress.userId = :userId', { userId }) .andWhere('progress.categoryId = :categoryId', { categoryId }) - .getRawOne(); + .getRawOne(); return { - totalAttempts: Number(stats.totalAttempts) || 0, - correctAttempts: parseInt(stats.correctAttempts) || 0, - totalPoints: parseInt(stats.totalPoints) || 0, - averageTimeSpent: parseFloat(stats.averageTimeSpent) || 0, + totalAttempts: Number(stats?.totalAttempts) || 0, + correctAttempts: parseInt(stats?.correctAttempts || '0', 10), + totalPoints: parseInt(stats?.totalPoints || '0', 10), + averageTimeSpent: parseFloat(stats?.averageTimeSpent || '0'), accuracy: - stats.totalAttempts > 0 - ? (parseInt(stats.correctAttempts) / parseInt(stats.totalAttempts)) * + stats && Number(stats.totalAttempts) > 0 + ? (parseInt(stats.correctAttempts, 10) / + Number(stats.totalAttempts)) * 100 : 0, }; diff --git a/backend/src/puzzles/dtos/create-puzzle.dto.ts b/backend/src/puzzles/dtos/create-puzzle.dto.ts index 96029b5..5c02dc7 100644 --- a/backend/src/puzzles/dtos/create-puzzle.dto.ts +++ b/backend/src/puzzles/dtos/create-puzzle.dto.ts @@ -6,8 +6,6 @@ import { IsNumber, IsOptional, Min, - ArrayMinSize, - ArrayContains, MinLength, Validate, ValidatorConstraint, @@ -16,27 +14,34 @@ import { } from 'class-validator'; import { Type } from 'class-transformer'; import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { PuzzleDifficulty, getPointsByDifficulty } from '../enums/puzzle-difficulty.enum'; +import { + PuzzleDifficulty, + getPointsByDifficulty, +} from '../enums/puzzle-difficulty.enum'; @ValidatorConstraint({ name: 'correctAnswerInOptions', async: false }) -export class CorrectAnswerInOptionsConstraint implements ValidatorConstraintInterface { +export class CorrectAnswerInOptionsConstraint + implements ValidatorConstraintInterface +{ validate(correctAnswer: string, args: ValidationArguments) { const object = args.object as CreatePuzzleDto; return object.options?.includes(correctAnswer) || false; } - defaultMessage(args: ValidationArguments) { + defaultMessage() { return 'correctAnswer must be one of the provided options'; } } @ValidatorConstraint({ name: 'optionsMinimumLength', async: false }) -export class OptionsMinimumLengthConstraint implements ValidatorConstraintInterface { - validate(options: string[], args: ValidationArguments) { +export class OptionsMinimumLengthConstraint + implements ValidatorConstraintInterface +{ + validate(options: string[]) { return options?.length >= 2; } - defaultMessage(args: ValidationArguments) { + defaultMessage() { return 'options must contain at least 2 items'; } } @@ -44,7 +49,7 @@ export class OptionsMinimumLengthConstraint implements ValidatorConstraintInterf export class CreatePuzzleDto { @ApiProperty({ description: 'The puzzle question text', - example: 'What has keys but can\'t open locks?', + example: "What has keys but can't open locks?", minLength: 10, }) @IsString() @@ -88,7 +93,8 @@ export class CreatePuzzleDto { categoryId: string; @ApiPropertyOptional({ - description: 'Points awarded for solving this puzzle. If not provided, will be calculated based on difficulty', + description: + 'Points awarded for solving this puzzle. If not provided, will be calculated based on difficulty', example: 250, minimum: 0, default: null, @@ -124,4 +130,4 @@ export class CreatePuzzleDto { this.points = getPointsByDifficulty(this.difficulty); } } -} \ No newline at end of file +} diff --git a/backend/src/puzzles/dtos/update-puzzle.dto.ts b/backend/src/puzzles/dtos/update-puzzle.dto.ts index b6a751b..eef1fde 100644 --- a/backend/src/puzzles/dtos/update-puzzle.dto.ts +++ b/backend/src/puzzles/dtos/update-puzzle.dto.ts @@ -6,7 +6,6 @@ import { IsNumber, IsOptional, Min, - ArrayMinSize, MinLength, Validate, } from 'class-validator'; @@ -23,7 +22,7 @@ import { export class UpdatePuzzleDto extends PartialType(CreatePuzzleDto) { @ApiPropertyOptional({ description: 'The puzzle question text', - example: 'What has keys but can\'t open locks?', + example: "What has keys but can't open locks?", minLength: 10, nullable: true, }) @@ -120,4 +119,4 @@ export class UpdatePuzzleDto extends PartialType(CreatePuzzleDto) { } return undefined; } -} \ No newline at end of file +} diff --git a/backend/src/puzzles/entities/puzzle.entity.ts b/backend/src/puzzles/entities/puzzle.entity.ts index 5fdd8ba..b491710 100644 --- a/backend/src/puzzles/entities/puzzle.entity.ts +++ b/backend/src/puzzles/entities/puzzle.entity.ts @@ -34,7 +34,7 @@ export class Puzzle { }) @Index() difficulty: PuzzleDifficulty; - + @ManyToOne(() => Category, { eager: false }) @JoinColumn({ name: 'categoryId' }) category: Category; diff --git a/backend/src/puzzles/providers/create-puzzle.provider.ts b/backend/src/puzzles/providers/create-puzzle.provider.ts index d2af809..73ac32a 100644 --- a/backend/src/puzzles/providers/create-puzzle.provider.ts +++ b/backend/src/puzzles/providers/create-puzzle.provider.ts @@ -48,7 +48,9 @@ export class CreatePuzzleProvider { try { return await this.puzzleRepository.save(puzzle); } catch (error) { - throw new InternalServerErrorException('Failed to create puzzle'); + throw new InternalServerErrorException( + `Failed to create puzzle ${error}`, + ); } } } diff --git a/backend/src/puzzles/providers/puzzles.service.ts b/backend/src/puzzles/providers/puzzles.service.ts index f03a57c..1e2c9ba 100644 --- a/backend/src/puzzles/providers/puzzles.service.ts +++ b/backend/src/puzzles/providers/puzzles.service.ts @@ -52,7 +52,7 @@ export class PuzzlesService { return puzzles; } - + public async findAll(query: PuzzleQueryDto) { return this.AllPuzzlesProvider.findAll(query); } diff --git a/backend/src/quests/controllers/daily-quest.controller.ts b/backend/src/quests/controllers/daily-quest.controller.ts index d5e52fd..b070058 100644 --- a/backend/src/quests/controllers/daily-quest.controller.ts +++ b/backend/src/quests/controllers/daily-quest.controller.ts @@ -11,7 +11,7 @@ import { DailyQuestResponseDto } from '../dtos/daily-quest-response.dto'; import { ActiveUser } from '../../auth/decorators/activeUser.decorator'; import { Auth } from '../../auth/decorators/auth.decorator'; import { authType } from '../../auth/enum/auth-type.enum'; -import { User } from 'src/users/user.entity'; +import { User } from '../../users/user.entity'; import { request } from 'express'; @Controller('daily-quest') diff --git a/backend/src/roles/roleguard.spec.ts b/backend/src/roles/roleguard.spec.ts deleted file mode 100644 index 8c382f2..0000000 --- a/backend/src/roles/roleguard.spec.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { ExecutionContext } from '@nestjs/common'; -import { Reflector } from '@nestjs/core'; -import { RolesGuard } from './roles.guard'; -import { ForbiddenException } from '@nestjs/common'; - -describe('RolesGuard', () => { - let guard: RolesGuard; - let reflector: Reflector; - - beforeEach(() => { - reflector = new Reflector(); - guard = new RolesGuard(reflector); - }); - - const createMockExecutionContext = (roles: string[] = [], user?: any): ExecutionContext => { - return { - switchToHttp: () => ({ - getRequest: () => ({ - user, - }), - }), - getHandler: () => jest.fn(), - getClass: () => jest.fn(), - } as unknown as ExecutionContext; - }; - - it('should allow access if no roles are required', () => { - jest.spyOn(reflector, 'getAllAndOverride').mockReturnValue(undefined); - const context = createMockExecutionContext(); - expect(guard.canActivate(context)).toBe(true); - }); - - it('should allow access if user has required role', () => { - jest.spyOn(reflector, 'getAllAndOverride').mockReturnValue(['admin']); - const context = createMockExecutionContext(['admin'], { role: 'admin' }); - expect(guard.canActivate(context)).toBe(true); - }); - - it('should throw ForbiddenException if user lacks role', () => { - jest.spyOn(reflector, 'getAllAndOverride').mockReturnValue(['admin']); - const context = createMockExecutionContext(['admin'], { role: 'user' }); - expect(() => guard.canActivate(context)).toThrow(ForbiddenException); - }); - - it('should throw ForbiddenException if user is not authenticated', () => { - jest.spyOn(reflector, 'getAllAndOverride').mockReturnValue(['admin']); - const context = createMockExecutionContext(['admin']); - expect(() => guard.canActivate(context)).toThrow(ForbiddenException); - }); -}); \ No newline at end of file diff --git a/backend/src/seed.ts b/backend/src/seed.ts index 1319341..3d34099 100644 --- a/backend/src/seed.ts +++ b/backend/src/seed.ts @@ -1,10 +1,9 @@ import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; -import { DataSource } from 'typeorm'; async function bootstrap() { const app = await NestFactory.createApplicationContext(AppModule); - const dataSource = app.get(DataSource); + // const dataSource = app.get(DataSource); await app.close(); } diff --git a/backend/src/users/controllers/users.controller.ts b/backend/src/users/controllers/users.controller.ts index 02bb2f7..04a0fc6 100644 --- a/backend/src/users/controllers/users.controller.ts +++ b/backend/src/users/controllers/users.controller.ts @@ -18,9 +18,7 @@ import { User } from '../user.entity'; @Controller('users') @ApiTags('users') export class UsersController { - constructor( - private readonly usersService: UsersService, - ) {} + constructor(private readonly usersService: UsersService) {} @Delete(':id') @ApiOperation({ summary: 'Delete user by ID' }) @@ -38,7 +36,7 @@ export class UsersController { @Get(':id') findOne(@Param('id') id: string) { - return null; + return id; } @Post() diff --git a/backend/src/users/dtos/createUserDto.ts b/backend/src/users/dtos/createUserDto.ts index 7c03c8b..183d1fc 100644 --- a/backend/src/users/dtos/createUserDto.ts +++ b/backend/src/users/dtos/createUserDto.ts @@ -8,7 +8,7 @@ import { IsArray, ArrayNotEmpty, } from 'class-validator'; -import { ApiProperty } from '@nestjs/swagger' +import { ApiProperty } from '@nestjs/swagger'; import { Transform } from 'class-transformer'; import { userRole } from '../enums/userRole.enum'; import { ChallengeLevel } from '../enums/challengeLevel.enum'; @@ -17,7 +17,6 @@ import { ReferralSource } from '../enums/referralSource.enum'; import { AgeGroup } from '../enums/ageGroup.enum'; import { AuthProvider } from '../../auth/enum/authProvider.enum'; - export class CreateUserDto { /** * Email field @@ -80,7 +79,9 @@ export class CreateUserDto { }) @IsEnum(userRole) @IsOptional() - @Transform(({ value }) => value ?? userRole.USER) + @Transform( + ({ value }: { value: userRole | undefined }) => value ?? userRole.USER, + ) userRole?: userRole; /** @@ -92,7 +93,7 @@ export class CreateUserDto { }) @IsString() @IsOptional() - walletAddress?: any; + walletAddress?: string; /** * Public key (required for wallet signup) @@ -103,7 +104,7 @@ export class CreateUserDto { }) @IsString() @IsOptional() - publicKey?: any; + publicKey?: string; /** * Auth provider (local, google, wallet) @@ -115,21 +116,19 @@ export class CreateUserDto { }) @IsEnum(AuthProvider) @IsOptional() - provider?: any; + provider?: string; /** * Autogenerated from Google when signing up with Googlee */ @ApiProperty({ type: 'string', - example: 'poiuytrdspoiuytrewa\zxcvbnmml;poiuytrdsdcvbnm]', + example: 'poiuytrdspoiuytrewaxcvbnmml;poiuytrdsdcvbnm]', }) @IsString() @IsOptional() @MaxLength(225) googleId?: string; - - @ApiProperty({ enum: ChallengeLevel, example: ChallengeLevel.INTERMEDIATE, diff --git a/backend/src/users/dtos/editUserDto.dto.ts b/backend/src/users/dtos/editUserDto.dto.ts index 5eb000e..0707856 100644 --- a/backend/src/users/dtos/editUserDto.dto.ts +++ b/backend/src/users/dtos/editUserDto.dto.ts @@ -3,15 +3,15 @@ import { IsOptional, IsString, IsEmail, MinLength } from 'class-validator'; export class EditUserDto { @ApiProperty({ - description: "username of the user", + description: 'username of the user', required: false, }) @IsOptional() @IsString() - username?: string + username?: string; @ApiProperty({ - description: "Email address of the user", + description: 'Email address of the user', required: false, }) @IsOptional() @@ -19,7 +19,7 @@ export class EditUserDto { email?: string; @ApiProperty({ - description: "Password of the user", + description: 'Password of the user', required: false, minLength: 6, }) diff --git a/backend/src/users/dtos/updateUserProfile.dto.ts b/backend/src/users/dtos/updateUserProfile.dto.ts index 9e5a928..eb75f4a 100644 --- a/backend/src/users/dtos/updateUserProfile.dto.ts +++ b/backend/src/users/dtos/updateUserProfile.dto.ts @@ -1,19 +1,19 @@ -import { ApiProperty } from "@nestjs/swagger"; -import { IsOptional, IsString } from "class-validator"; +import { ApiProperty } from '@nestjs/swagger'; +import { IsOptional, IsString } from 'class-validator'; export class UpdateUserProfileDto { - @ApiProperty({ example: 'John Doe' }) - @IsString() - @IsOptional() - name?: string; - - @ApiProperty({ example: 'johndoe' }) - @IsString() - @IsOptional() - username?: string; - - @ApiProperty({ example: 'avatar_url.jpg' }) - @IsString() - @IsOptional() - avatar?: string; - } \ No newline at end of file + @ApiProperty({ example: 'John Doe' }) + @IsString() + @IsOptional() + name?: string; + + @ApiProperty({ example: 'johndoe' }) + @IsString() + @IsOptional() + username?: string; + + @ApiProperty({ example: 'avatar_url.jpg' }) + @IsString() + @IsOptional() + avatar?: string; +} diff --git a/backend/src/users/enums/ageGroup.enum.ts b/backend/src/users/enums/ageGroup.enum.ts index d9586ab..808e4e4 100644 --- a/backend/src/users/enums/ageGroup.enum.ts +++ b/backend/src/users/enums/ageGroup.enum.ts @@ -6,4 +6,4 @@ export enum AgeGroup { FORTIES = '45-54 years old', FIFTIES = '55-64 years old', SENIOR = '65+ years old', -} \ No newline at end of file +} diff --git a/backend/src/users/enums/challengeLevel.enum.ts b/backend/src/users/enums/challengeLevel.enum.ts index aeb0287..4cc9b35 100644 --- a/backend/src/users/enums/challengeLevel.enum.ts +++ b/backend/src/users/enums/challengeLevel.enum.ts @@ -3,4 +3,4 @@ export enum ChallengeLevel { INTERMEDIATE = 'intermediate', ADVANCED = 'advanced', EXPERT = 'expert', -} \ No newline at end of file +} diff --git a/backend/src/users/enums/challengeType.enum.ts b/backend/src/users/enums/challengeType.enum.ts index 700d909..8e6e1e0 100644 --- a/backend/src/users/enums/challengeType.enum.ts +++ b/backend/src/users/enums/challengeType.enum.ts @@ -2,4 +2,4 @@ export enum ChallengeType { CODING = 'Coding Challenges', LOGIC = 'Logic Puzzle', BLOCKCHAIN = 'Blockchain', -} \ No newline at end of file +} diff --git a/backend/src/users/enums/referralSource.enum.ts b/backend/src/users/enums/referralSource.enum.ts index 1ef6817..f0e45bb 100644 --- a/backend/src/users/enums/referralSource.enum.ts +++ b/backend/src/users/enums/referralSource.enum.ts @@ -3,4 +3,4 @@ export enum ReferralSource { TWITTER = 'X/Twitter', FRIENDS = 'Friends', OTHER = 'Other', -} \ No newline at end of file +} diff --git a/backend/src/users/enums/userRole.enum.ts b/backend/src/users/enums/userRole.enum.ts index 600fdae..98ce5b1 100644 --- a/backend/src/users/enums/userRole.enum.ts +++ b/backend/src/users/enums/userRole.enum.ts @@ -1,5 +1,5 @@ export enum userRole { - ADMIN = 'admin', - USER = 'user', - GUEST = 'guest', -} \ No newline at end of file + ADMIN = 'admin', + USER = 'user', + GUEST = 'guest', +} diff --git a/backend/src/users/providers/create-user.service.ts b/backend/src/users/providers/create-user.service.ts index 61cbc42..5027067 100644 --- a/backend/src/users/providers/create-user.service.ts +++ b/backend/src/users/providers/create-user.service.ts @@ -38,7 +38,9 @@ export class CreateUserService { try { // hash the password before saving if (!userData.password || typeof userData.password !== 'string') { - throw new BadRequestException('Password is required and must be a string'); + throw new BadRequestException( + 'Password is required and must be a string', + ); } const hashedPassword = await this.hashingProvider.hashPassword( userData.password, @@ -54,7 +56,7 @@ export class CreateUserService { } return savedUser; } catch (error) { - throw new InternalServerErrorException('Failed to create user'); + throw new InternalServerErrorException(`Failed to create user ${error}`); } } } diff --git a/backend/src/users/providers/delete-user.service.ts b/backend/src/users/providers/delete-user.service.ts index 1f71ee1..820fd4f 100644 --- a/backend/src/users/providers/delete-user.service.ts +++ b/backend/src/users/providers/delete-user.service.ts @@ -1,4 +1,8 @@ -import { Injectable, NotFoundException, InternalServerErrorException } from '@nestjs/common'; +import { + Injectable, + NotFoundException, + InternalServerErrorException, +} from '@nestjs/common'; import { User } from '../user.entity'; import { Repository } from 'typeorm'; import { InjectRepository } from '@nestjs/typeorm'; @@ -20,7 +24,7 @@ export class DeleteUserService { try { await this.userRepository.delete(id); } catch (error) { - throw new InternalServerErrorException('Failed to delete user.'); + throw new InternalServerErrorException(`Failed to delete user: ${error}`); } } } diff --git a/backend/src/users/providers/find-all.service.ts b/backend/src/users/providers/find-all.service.ts index 89e7781..4d6354b 100644 --- a/backend/src/users/providers/find-all.service.ts +++ b/backend/src/users/providers/find-all.service.ts @@ -46,7 +46,7 @@ export class FindAll { // users = await this.userRepository.find(); } catch (error) { throw new RequestTimeoutException('Could not fetch users', { - description: 'Error connecting to database', + description: `Error connecting to database ${error}`, }); } diff --git a/backend/src/users/providers/find-one-by-email.provider.ts b/backend/src/users/providers/find-one-by-email.provider.ts index 4f923e9..f9fea1a 100644 --- a/backend/src/users/providers/find-one-by-email.provider.ts +++ b/backend/src/users/providers/find-one-by-email.provider.ts @@ -1,4 +1,8 @@ -import { Injectable, RequestTimeoutException, UnauthorizedException } from '@nestjs/common'; +import { + Injectable, + RequestTimeoutException, + UnauthorizedException, +} from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger'; @@ -10,38 +14,46 @@ import { User } from '../user.entity'; @ApiTags('Users') @Injectable() export class FindOneByEmail { - /** - * Injects the User repository. - * @param userRepository - The repository for User entity. - */ - constructor(@InjectRepository(User) private userRepository: Repository) {} + /** + * Injects the User repository. + * @param userRepository - The repository for User entity. + */ + constructor( + @InjectRepository(User) private userRepository: Repository, + ) {} - /** - * Finds a user by email. - * @param email - The email of the user to find. - * @returns The user entity if found. - * @throws RequestTimeoutException if there is an error connecting to the database. - * @throws UnauthorizedException if the user does not exist. - */ - @ApiOperation({ summary: 'Find a user by email' }) - @ApiResponse({ status: 200, description: 'User found', type: User }) - @ApiResponse({ status: 408, description: 'Request Timeout - Could not fetch user' }) - @ApiResponse({ status: 401, description: 'Unauthorized - User does not exist' }) - public async findOneByEmail(email: string): Promise { - let user: User | null; + /** + * Finds a user by email. + * @param email - The email of the user to find. + * @returns The user entity if found. + * @throws RequestTimeoutException if there is an error connecting to the database. + * @throws UnauthorizedException if the user does not exist. + */ + @ApiOperation({ summary: 'Find a user by email' }) + @ApiResponse({ status: 200, description: 'User found', type: User }) + @ApiResponse({ + status: 408, + description: 'Request Timeout - Could not fetch user', + }) + @ApiResponse({ + status: 401, + description: 'Unauthorized - User does not exist', + }) + public async findOneByEmail(email: string): Promise { + let user: User | null; - try { - user = await this.userRepository.findOneBy({ email }); - } catch (error) { - throw new RequestTimeoutException('Could not fetch user', { - description: 'Error connecting to database', - }); - } - - if (!user) { - throw new UnauthorizedException('User does not exist'); - } + try { + user = await this.userRepository.findOneBy({ email }); + } catch (error) { + throw new RequestTimeoutException('Could not fetch user', { + description: `Error connecting to database ${error}`, + }); + } - return user; + if (!user) { + throw new UnauthorizedException('User does not exist'); } -} \ No newline at end of file + + return user; + } +} diff --git a/backend/src/users/providers/find-one-by-googleId.ts b/backend/src/users/providers/find-one-by-googleId.ts index ecbe452..d04efc4 100644 --- a/backend/src/users/providers/find-one-by-googleId.ts +++ b/backend/src/users/providers/find-one-by-googleId.ts @@ -29,4 +29,4 @@ export class FindOneByGoogleIdProvider { public async findOneByGoogleId(googleId: string): Promise { return await this.userRepository.findOneBy({ googleId }); } -} \ No newline at end of file +} diff --git a/backend/src/users/providers/find-one-by-wallet.provider.ts b/backend/src/users/providers/find-one-by-wallet.provider.ts index f2a04f7..cf515c7 100644 --- a/backend/src/users/providers/find-one-by-wallet.provider.ts +++ b/backend/src/users/providers/find-one-by-wallet.provider.ts @@ -46,7 +46,7 @@ export class FindOneByWallet { user = await this.userRepository.findOneBy({ stellarWallet: wallet }); } catch (error) { throw new RequestTimeoutException('Could not fetch user', { - description: 'Error connecting to database', + description: `Error connecting to database ${error}`, }); } diff --git a/backend/src/users/providers/googleUserProvider.ts b/backend/src/users/providers/googleUserProvider.ts index 1986747..8b15023 100644 --- a/backend/src/users/providers/googleUserProvider.ts +++ b/backend/src/users/providers/googleUserProvider.ts @@ -27,7 +27,10 @@ export class CreateGoogleUserProvider { */ @ApiOperation({ summary: 'Create a new Google user' }) @ApiResponse({ status: 201, description: 'User successfully created.' }) - @ApiResponse({ status: 409, description: 'Conflict: Could not create a new user.' }) + @ApiResponse({ + status: 409, + description: 'Conflict: Could not create a new user.', + }) public async createGoogleUser(googleUser: GoogleInterface): Promise { try { const user = this.userRepository.create(googleUser); @@ -38,4 +41,4 @@ export class CreateGoogleUserProvider { }); } } -} \ No newline at end of file +} diff --git a/backend/src/users/providers/update-user.service.ts b/backend/src/users/providers/update-user.service.ts index df61ad6..c964334 100644 --- a/backend/src/users/providers/update-user.service.ts +++ b/backend/src/users/providers/update-user.service.ts @@ -1,4 +1,8 @@ -import { Injectable, NotFoundException, InternalServerErrorException } from '@nestjs/common'; +import { + Injectable, + NotFoundException, + InternalServerErrorException, +} from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { User } from '../user.entity'; @@ -26,7 +30,7 @@ export class UpdateUserService { try { return await this.userRepository.save(user); } catch (error) { - throw new InternalServerErrorException('Failed to update user'); + throw new InternalServerErrorException(`Failed to update user ${error}`); } } } diff --git a/backend/src/users/providers/users.service.ts b/backend/src/users/providers/users.service.ts index ceeeb83..3a1e535 100644 --- a/backend/src/users/providers/users.service.ts +++ b/backend/src/users/providers/users.service.ts @@ -26,7 +26,7 @@ export class UsersService { private readonly findOneByGoogleIdProvider: FindOneByGoogleIdProvider, private readonly createGoogleUserProvider: CreateGoogleUserProvider, - private readonly updateUserService: UpdateUserService + private readonly updateUserService: UpdateUserService, ) {} public async findAllUsers( @@ -35,9 +35,9 @@ export class UsersService { return this.findAll.findAll(dto); } - public async findOne(): Promise { - return null; - } + // public async findOne(): Promise { + // return null; + // } public async GetOneByEmail(email: string) { return this.findOneByEmail.findOneByEmail(email); @@ -68,7 +68,7 @@ export class UsersService { } public async update(id: string, data: EditUserDto): Promise { - return this.updateUserService.editUser(id,data) + return this.updateUserService.editUser(id, data); } public async delete(id: string): Promise { diff --git a/backend/test/user-activity.e2e-spec.ts b/backend/test/user-activity.e2e-spec.ts deleted file mode 100644 index 7f7c79c..0000000 --- a/backend/test/user-activity.e2e-spec.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { INestApplication, ValidationPipe } from '@nestjs/common'; -import * as request from 'supertest'; -import { AppModule } from './../src/app.module'; -import { getRepositoryToken } from '@nestjs/typeorm'; -import { User } from '../src/users/user.entity'; -import { PuzzleSubmission } from '../src/puzzle/entities/puzzle-submission.entity'; -import { UserAchievement } from '../src/achievement/entities/user-achievement.entity'; - -describe('User Activity (e2e)', () => { - let app: INestApplication; - let userRepo; - let puzzleSubmissionRepo; - let userAchievementRepo; - let userId: string; - - beforeAll(async () => { - const moduleFixture: TestingModule = await Test.createTestingModule({ - imports: [AppModule], - }).compile(); - - app = moduleFixture.createNestApplication(); - app.useGlobalPipes(new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true })); - await app.init(); - - userRepo = moduleFixture.get(getRepositoryToken(User)); - puzzleSubmissionRepo = moduleFixture.get(getRepositoryToken(PuzzleSubmission)); - userAchievementRepo = moduleFixture.get(getRepositoryToken(UserAchievement)); - - // Create a test user - const user = userRepo.create({ username: 'testuser', email: 'test@example.com', password: 'testpass' }); - await userRepo.save(user); - = user.id; - - // Create a correct puzzle submission - await puzzleSubmissionRepo.save({ - user, - isCorrect: true, - puzzle: { title: 'Binary Tree Maximum Depth' }, - createdAt: new Date('2025-07-05T08:00:00Z'), - }); - - // Create an achievement - await userAchievementRepo.save({ - user, - achievement: { title: 'Code Ninja' }, - unlockedAt: new Date('2025-07-04T16:30:00Z'), - }); - }); - - afterAll(async () => { - await app.close(); - }); - - it('should return recent activity for a user', async () => { - const res = await request(app.getHttpServer()) - .get(`/users/${userId}/activity?page=1&limit=5`) - .expect(200); - expect(res.body.activities).toBeDefined(); - expect(res.body.activities.length).toBeGreaterThan(0); - expect(res.body.activities[0]).toHaveProperty('description'); - expect(res.body.activities[0]).toHaveProperty('timestamp'); - }); - - it('should return 404 for non-existent user', async () => { - await request(app.getHttpServer()) - .get('/users/nonexistentid/activity') - .expect(404); - }); - - it('should validate pagination params', async () => { - await request(app.getHttpServer()) - .get(`/users/${userId}/activity?page=0&limit=0`) - .expect(400); - }); -}); \ No newline at end of file diff --git a/frontend/app/auth/check-email/page.tsx b/frontend/app/auth/check-email/page.tsx index 370262d..159aa72 100644 --- a/frontend/app/auth/check-email/page.tsx +++ b/frontend/app/auth/check-email/page.tsx @@ -62,7 +62,7 @@ const CheckEmail = () => { {/* Message */}

- We've sent you a link to your email address to reset your password. Click the link in your inbox to continue. + We've sent you a link to your email address to reset your password. Click the link in your inbox to continue.

diff --git a/frontend/app/auth/signin/page.tsx b/frontend/app/auth/signin/page.tsx index 7278263..9624efe 100644 --- a/frontend/app/auth/signin/page.tsx +++ b/frontend/app/auth/signin/page.tsx @@ -12,7 +12,7 @@ import { useToast } from '@/components/ui/ToastProvider'; const SignInPage = () => { const router = useRouter(); - const { showSuccess, showError, showWarning, showInfo } = useToast(); + const { showSuccess, showError, showInfo } = useToast(); const [formData, setFormData] = useState({ username: '', password: '' @@ -213,7 +213,7 @@ const SignInPage = () => { {/* Sign Up Link */}
- Don't have an account? + Don't have an account? Promise; - isMetaMask?: boolean; - }; +"use client"; + +import React, { useState } from "react"; +import { useRouter } from "next/navigation"; +import Link from "next/link"; +import Input from "@/components/ui/Input"; +import Button from "@/components/ui/Button"; +import { Wallet } from "lucide-react"; +import Image from "next/image"; +import ErrorBoundary from "@/components/ErrorBoundary"; +import { useToast } from "@/components/ui/ToastProvider"; + +// Proper TypeScript types for Ethereum provider +interface EthereumProvider { + request: (args: { + method: string; + params?: unknown[]; + }) => Promise; + isMetaMask?: boolean; } declare global { interface Window { - ethereum?: { - request: (args: { method: string; params?: any[] }) => Promise; - isMetaMask?: boolean; - }; + ethereum?: EthereumProvider; } } @@ -31,10 +29,10 @@ const SignUpPage = () => { const router = useRouter(); const { showSuccess, showError, showWarning, showInfo } = useToast(); const [formData, setFormData] = useState({ - username: '', - fullName: '', - email: '', - password: '' + username: "", + fullName: "", + email: "", + password: "", }); const [isLoading, setIsLoading] = useState(false); @@ -47,7 +45,8 @@ const SignUpPage = () => { // Password validation function const validatePassword = (password: string) => { // At least 8 characters, 1 uppercase, 1 lowercase, 1 number - const passwordRegex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[a-zA-Z\d@$!%*?&]{8,}$/; + const passwordRegex = + /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[a-zA-Z\d@$!%*?&]{8,}$/; return passwordRegex.test(password); }; @@ -58,15 +57,14 @@ const SignUpPage = () => { return usernameRegex.test(username); }; - const handleInputChange = (field: string) => ( - e: React.ChangeEvent - ) => { - const value = e.target.value; - setFormData(prev => ({ - ...prev, - [field]: value - })); - }; + const handleInputChange = + (field: string) => (e: React.ChangeEvent) => { + const value = e.target.value; + setFormData((prev) => ({ + ...prev, + [field]: value, + })); + }; const handleSignUp = async (e: React.FormEvent) => { e.preventDefault(); @@ -74,43 +72,49 @@ const SignUpPage = () => { // Validate all fields if (!formData.username.trim()) { - showError('Username Required', 'Please enter a username'); + showError("Username Required", "Please enter a username"); setIsLoading(false); return; } if (!validateUsername(formData.username)) { - showError('Invalid Username', 'Username must be at least 3 characters and contain only letters, numbers, and underscores'); + showError( + "Invalid Username", + "Username must be at least 3 characters and contain only letters, numbers, and underscores", + ); setIsLoading(false); return; } if (!formData.fullName.trim()) { - showError('Full Name Required', 'Please enter your full name'); + showError("Full Name Required", "Please enter your full name"); setIsLoading(false); return; } if (!formData.email.trim()) { - showError('Email Required', 'Please enter your email address'); + showError("Email Required", "Please enter your email address"); setIsLoading(false); return; } if (!validateEmail(formData.email)) { - showError('Invalid Email', 'Please enter a valid email address'); + showError("Invalid Email", "Please enter a valid email address"); setIsLoading(false); return; } if (!formData.password.trim()) { - showError('Password Required', 'Please enter a password'); + showError("Password Required", "Please enter a password"); setIsLoading(false); return; } if (!validatePassword(formData.password)) { - showError('Weak Password', 'Password must be at least 8 characters with uppercase, lowercase, and number'); + showError( + "Weak Password", + "Password must be at least 8 characters with uppercase, lowercase, and number", + ); setIsLoading(false); return; } @@ -123,61 +127,84 @@ const SignUpPage = () => { fullname: formData.fullName, // Server expects lowercase 'n' password: formData.password, userRole: "user", // Default role - provider: "local" // Local registration + provider: "local", // Local registration }; - console.log('Sending request with data:', requestBody); // Debug log + console.log("Sending request with data:", requestBody); // Debug log - const response = await fetch('https://mindblock-webaapp.onrender.com/users', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', + const response = await fetch( + "https://mindblock-webaapp.onrender.com/users", + { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(requestBody), }, - body: JSON.stringify(requestBody), - }); + ); // Log response details for debugging - console.log('Response status:', response.status); - console.log('Response headers:', response.headers); + console.log("Response status:", response.status); + console.log("Response headers:", response.headers); // Checking if response is ok before trying to parse JSON if (!response.ok) { - let errorMessage = 'Registration failed. Please try again.'; - + let errorMessage = "Registration failed. Please try again."; + try { const errorData = await response.json(); - console.log('Error response data:', errorData); // Debug log - + console.log("Error response data:", errorData); // Debug log + // Safely extract error message - if (typeof errorData === 'object' && errorData !== null) { - if (typeof errorData.message === 'string') { + if (typeof errorData === "object" && errorData !== null) { + if (typeof errorData.message === "string") { errorMessage = errorData.message; - } else if (typeof errorData.error === 'string') { + } else if (typeof errorData.error === "string") { errorMessage = errorData.error; - } else if (Array.isArray(errorData.errors) && errorData.errors.length > 0) { + } else if ( + Array.isArray(errorData.errors) && + errorData.errors.length > 0 + ) { errorMessage = errorData.errors[0]; } } - + // Handle specific error cases if (response.status === 409) { - showError('Account Already Exists', errorMessage || 'User already exists with this email or username.'); + showError( + "Account Already Exists", + errorMessage || + "User already exists with this email or username.", + ); } else if (response.status === 400) { - showError('Invalid Input', errorMessage || 'Invalid input data. Please check your information.'); + showError( + "Invalid Input", + errorMessage || + "Invalid input data. Please check your information.", + ); } else if (response.status >= 500) { - showError('Server Error', 'Server error. Please try again later.'); + showError("Server Error", "Server error. Please try again later."); } else { - showError('Registration Failed', errorMessage); + showError("Registration Failed", errorMessage); } } catch (parseError) { - console.error('Error parsing response:', parseError); + console.error("Error parsing response:", parseError); // If response isn't JSON, use status-based messages if (response.status === 409) { - showError('Account Already Exists', 'An account with this email or username already exists.'); + showError( + "Account Already Exists", + "An account with this email or username already exists.", + ); } else if (response.status === 400) { - showError('Invalid Input', 'Please check your input and try again.'); + showError( + "Invalid Input", + "Please check your input and try again.", + ); } else { - showError('Registration Failed', `Error ${response.status}: ${response.statusText || 'Please try again.'}`); + showError( + "Registration Failed", + `Error ${response.status}: ${response.statusText || "Please try again."}`, + ); } } setIsLoading(false); @@ -186,38 +213,51 @@ const SignUpPage = () => { // Parse JSON only if response is ok const data = await response.json(); - console.log('Success response data:', data); // Debug log + console.log("Success response data:", data); // Debug log - if (data.accessToken || data.message === 'User created successfully' || data.success) { + if ( + data.accessToken || + data.message === "User created successfully" || + data.success + ) { // If we get a token, store it if (data.accessToken) { try { - localStorage.setItem('accessToken', data.accessToken); + localStorage.setItem("accessToken", data.accessToken); } catch (storageError) { - console.warn('Could not save token to localStorage:', storageError); + console.warn("Could not save token to localStorage:", storageError); } } - + // Show success toast - showSuccess('Registration Successful', 'Welcome to Mind Block!'); - + showSuccess("Registration Successful", "Welcome to Mind Block!"); + // Redirect to signin page or dashboard based on whether we got a token setTimeout(() => { if (data.accessToken) { - router.push('/dashboard'); + router.push("/dashboard"); } else { - router.push('/auth/signin'); + router.push("/auth/signin"); } }, 1000); // Small delay to show success message } else { - showError('Invalid Response', 'Invalid response from server. Please try again.'); + showError( + "Invalid Response", + "Invalid response from server. Please try again.", + ); } } catch (error) { - console.error('Sign up error:', error); - if (error instanceof TypeError && error.message.includes('fetch')) { - showError('Network Error', 'Could not connect to the server. Please check your internet connection and try again.'); + console.error("Sign up error:", error); + if (error instanceof TypeError && error.message.includes("fetch")) { + showError( + "Network Error", + "Could not connect to the server. Please check your internet connection and try again.", + ); } else { - showError('Network Error', 'An unexpected error occurred. Please try again.'); + showError( + "Network Error", + "An unexpected error occurred. Please try again.", + ); } } finally { setIsLoading(false); @@ -225,47 +265,52 @@ const SignUpPage = () => { }; const handleGoogleSignUp = () => { - showInfo('Google Sign-Up', 'Redirecting to Google authentication...'); - window.location.href = "https://mindblock-webaapp.onrender.com/auth/google-authentication"; + showInfo("Google Sign-Up", "Redirecting to Google authentication..."); + window.location.href = + "https://mindblock-webaapp.onrender.com/auth/google-authentication"; }; const handleWalletConnect = async () => { try { - showInfo('Wallet Connection', 'Connecting to your wallet...'); - + showInfo("Wallet Connection", "Connecting to your wallet..."); + // Check if Web3 is available (MetaMask or similar) - if (typeof window.ethereum !== 'undefined') { - // Request account access - const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' }); - const walletAddress = accounts[0]; + if (typeof window.ethereum !== "undefined") { + // Request account access with type assertion + const accounts = (await window.ethereum.request({ + method: "eth_requestAccounts", + })) as string[]; + const walletAddress = accounts[0]; + if (walletAddress) { // Generate a nonce for authentication const timestamp = Date.now(); const randomString = Math.random().toString(36).substring(2, 15); const nonce = `nonce_${timestamp}_${randomString}_${Math.random().toString(36).substring(2, 8)}`; - + // Create a message for signing that includes the nonce const message = `Sign this message to authenticate with Mind Block. Nonce: ${nonce}`; - + try { - // Request signature using personal_sign - const signature = await window.ethereum.request({ - method: 'personal_sign', + // Request signature using personal_sign with type assertion + const signature = (await window.ethereum.request({ + method: "personal_sign", params: [message, walletAddress], - }); - + })) as string; + // Get the public key (this might not be directly available from MetaMask) // For now, we'll use a placeholder or try to derive it - let publicKey = ''; + let publicKey = ""; try { // Try to get public key - this might not work with all wallets - const encryptionKey = await window.ethereum.request({ - method: 'eth_getEncryptionPublicKey', + const encryptionKey = (await window.ethereum.request({ + method: "eth_getEncryptionPublicKey", params: [walletAddress], - }); + })) as string; + // Convert base64 to hex format if needed - if (encryptionKey && !encryptionKey.startsWith('0x')) { + if (encryptionKey && !encryptionKey.startsWith("0x")) { try { // Convert base64 to hex using browser's atob const binaryString = atob(encryptionKey); @@ -273,78 +318,101 @@ const SignUpPage = () => { for (let i = 0; i < binaryString.length; i++) { bytes[i] = binaryString.charCodeAt(i); } - publicKey = '0x' + Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join(''); + publicKey = + "0x" + + Array.from(bytes) + .map((b) => b.toString(16).padStart(2, "0")) + .join(""); } catch (convError) { - console.warn('Could not convert public key format:', convError); + console.warn( + "Could not convert public key format:", + convError, + ); publicKey = encryptionKey; // Use as-is if conversion fails } } else { - publicKey = encryptionKey || ''; + publicKey = encryptionKey || ""; } } catch (pkError) { - console.warn('Could not get public key:', pkError); + console.warn("Could not get public key:", pkError); // Generate a placeholder public key in correct hex format - publicKey = '0x' + Array(128).fill(0).map(() => Math.floor(Math.random() * 16).toString(16)).join(''); + publicKey = + "0x" + + Array(128) + .fill(0) + .map(() => Math.floor(Math.random() * 16).toString(16)) + .join(""); } - + // Format the request body to match the expected wallet login format const requestBody = { walletAddress: walletAddress, signature: [signature], // Single signature in array format nonce: nonce, - publicKey: publicKey + publicKey: publicKey, }; - - console.log('Wallet login request:', requestBody); // Debug log - + + console.log("Wallet login request:", requestBody); // Debug log + // Use the wallet login endpoint - const response = await fetch('https://mindblock-webaapp.onrender.com/auth/wallet-login', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', + const response = await fetch( + "https://mindblock-webaapp.onrender.com/auth/wallet-login", + { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(requestBody), }, - body: JSON.stringify(requestBody), - }); - - console.log('Wallet login response status:', response.status); - + ); + + console.log("Wallet login response status:", response.status); + if (response.ok) { const data = await response.json(); - console.log('Wallet login response data:', data); - + console.log("Wallet login response data:", data); + // Store token if provided if (data.accessToken || data.token) { try { - localStorage.setItem('accessToken', data.accessToken || data.token); + localStorage.setItem( + "accessToken", + data.accessToken || data.token, + ); } catch (storageError) { - console.warn('Could not save token to localStorage:', storageError); + console.warn( + "Could not save token to localStorage:", + storageError, + ); } } - - showSuccess('Wallet Connected', 'Successfully authenticated with wallet!'); - + + showSuccess( + "Wallet Connected", + "Successfully authenticated with wallet!", + ); + // Redirect based on response setTimeout(() => { if (data.accessToken || data.token) { - router.push('/dashboard'); + router.push("/dashboard"); } else { - router.push('/auth/signin'); + router.push("/auth/signin"); } }, 1000); - } else { // Handle wallet login errors try { const errorData = await response.json(); - console.log('Wallet login error data:', errorData); - - let errorMessage = 'Wallet authentication failed'; - + console.log("Wallet login error data:", errorData); + + let errorMessage = "Wallet authentication failed"; + // Handle nested message object if (errorData.message) { - if (typeof errorData.message === 'string') { + if (typeof errorData.message === "string") { errorMessage = errorData.message; - } else if (typeof errorData.message === 'object') { + } else if (typeof errorData.message === "object") { // If message is an object, try to extract meaningful info if (errorData.message.error) { errorMessage = errorData.message.error; @@ -355,45 +423,95 @@ const SignUpPage = () => { errorMessage = JSON.stringify(errorData.message); } } - } else if (typeof errorData.error === 'string') { + } else if (typeof errorData.error === "string") { errorMessage = errorData.error; } - + if (response.status === 404) { // Wallet not registered, suggest registration - showError('Wallet Not Registered', 'This wallet is not registered. Please sign up first.'); + showError( + "Wallet Not Registered", + "This wallet is not registered. Please sign up first.", + ); } else if (response.status === 401) { - showError('Authentication Failed', 'Invalid signature. Please try again.'); + showError( + "Authentication Failed", + "Invalid signature. Please try again.", + ); } else if (response.status === 400) { - showError('Invalid Request', `Bad request: ${errorMessage}`); + showError("Invalid Request", `Bad request: ${errorMessage}`); } else { - showError('Wallet Login Failed', errorMessage); + showError("Wallet Login Failed", errorMessage); } } catch (parseError) { - console.error('Error parsing wallet login response:', parseError); - showError('Wallet Login Failed', `Error ${response.status}: Please try again.`); + console.error( + "Error parsing wallet login response:", + parseError, + ); + showError( + "Wallet Login Failed", + `Error ${response.status}: Please try again.`, + ); } } - - } catch (signError: any) { - console.error('Signature error:', signError); - if (signError?.code === 4001) { - showWarning('Signature Cancelled', 'Message signing was cancelled by user'); + } catch (signError: unknown) { + console.error("Signature error:", signError); + + // Type guard to check if it's an error with a code property + const isWalletError = ( + error: unknown, + ): error is { code: number; message?: string } => { + return ( + typeof error === "object" && + error !== null && + "code" in error && + typeof (error as { code: unknown }).code === "number" + ); + }; + + if (isWalletError(signError) && signError.code === 4001) { + showWarning( + "Signature Cancelled", + "Message signing was cancelled by user", + ); } else { - showError('Signature Error', 'Failed to sign message. Please try again.'); + showError( + "Signature Error", + "Failed to sign message. Please try again.", + ); } } } } else { // No Web3 wallet detected - showWarning('Wallet Not Found', 'Please install MetaMask or another Web3 wallet to continue'); + showWarning( + "Wallet Not Found", + "Please install MetaMask or another Web3 wallet to continue", + ); } - } catch (error: any) { - console.error('Wallet connection error:', error); - if (error?.code === 4001) { - showWarning('Connection Cancelled', 'Wallet connection was cancelled by user'); + } catch (error: unknown) { + console.error("Wallet connection error:", error); + + // Type guard to check if it's an error with a code property + const isWalletError = (err: unknown): err is { code: number } => { + return ( + typeof err === "object" && + err !== null && + "code" in err && + typeof (err as { code: unknown }).code === "number" + ); + }; + + if (isWalletError(error) && error.code === 4001) { + showWarning( + "Connection Cancelled", + "Wallet connection was cancelled by user", + ); } else { - showError('Wallet Error', 'Failed to connect wallet. Please try again.'); + showError( + "Wallet Error", + "Failed to connect wallet. Please try again.", + ); } } }; @@ -402,21 +520,15 @@ const SignUpPage = () => {
{/* Header */} -
-
- +
+ {/* Main Content */}
-
-
+
+
- Home + Home

@@ -430,44 +542,50 @@ const SignUpPage = () => { type="text" placeholder="Username" value={formData.username} - onChange={handleInputChange('username')} + onChange={handleInputChange("username")} /> {/* Sign Up Button */} {/* Sign In Link */}
Have an account? - @@ -487,10 +605,22 @@ const SignUpPage = () => { className="w-full h-12 border-2 border-blue-500 text-white rounded-lg flex items-center justify-center gap-3 hover:bg-blue-500/10 transition-colors disabled:opacity-50 disabled:cursor-not-allowed" > - - - - + + + + Sign up with Google @@ -508,12 +638,18 @@ const SignUpPage = () => { {/* Terms and Privacy */}
- By signing up for Mind Block, you agree to our{' '} - + By signing up for Mind Block, you agree to our{" "} + Terms - - {' '}and{' '} - + {" "} + and{" "} + Privacy Policy
diff --git a/frontend/app/streak/page.tsx b/frontend/app/streak/page.tsx index 90a26be..00523e4 100644 --- a/frontend/app/streak/page.tsx +++ b/frontend/app/streak/page.tsx @@ -2,7 +2,6 @@ import { StreakScreen } from "@/components/StreakScreen"; import { DayData } from "@/components/WeeklyCalendar"; import { useRouter } from "next/navigation"; -import StreakNavbar from "@/components/StreakNavbar"; export default function StreakPage() { const router = useRouter(); diff --git a/frontend/components/ErrorBoundary.tsx b/frontend/components/ErrorBoundary.tsx index 32445bf..ae74e26 100644 --- a/frontend/components/ErrorBoundary.tsx +++ b/frontend/components/ErrorBoundary.tsx @@ -33,7 +33,7 @@ class ErrorBoundary extends Component {

Something went wrong

- We're sorry, but something unexpected happened. Please try refreshing the page. + We're sorry, but something unexpected happened. Please try refreshing the page.

- Great job! You've cleared Level {level}. + Great job! You've cleared Level {level}.

diff --git a/frontend/lib/features/quiz/quizSlice.ts b/frontend/lib/features/quiz/quizSlice.ts index 5a28b52..43e75e0 100644 --- a/frontend/lib/features/quiz/quizSlice.ts +++ b/frontend/lib/features/quiz/quizSlice.ts @@ -53,6 +53,7 @@ export const fetchQuestions = createAsyncThunk( points: 20, }, ] as Question[]; + console.log(category) }, ); diff --git a/frontend/providers/storeProvider.tsx b/frontend/providers/storeProvider.tsx index 3782581..179b340 100644 --- a/frontend/providers/storeProvider.tsx +++ b/frontend/providers/storeProvider.tsx @@ -8,7 +8,7 @@ export default function StoreProvider({ }: { children: React.ReactNode; }) { - const storeRef = useRef(); + const storeRef = useRef(null); if (!storeRef.current) { storeRef.current = makeStore(); } diff --git a/package-lock.json b/package-lock.json index a7a46f1..8d99953 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,9 @@ ], "dependencies": { "tsconfig-paths": "^4.2.0" + }, + "devDependencies": { + "@types/nodemailer": "^7.0.9" } }, "backend": { @@ -42,6 +45,7 @@ "fast-csv": "^5.0.2", "google-auth-library": "^9.15.1", "ioredis": "^5.6.1", + "nodemailer": "^7.0.12", "oauth2client": "^1.0.0", "passport": "^0.7.0", "passport-google-oauth20": "^2.0.0", @@ -193,7 +197,7 @@ "version": "1.13.5", "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.13.5.tgz", "integrity": "sha512-WezcBo8a0Dg2rnR82zhwoR6aRNxeTGfK5QCD6TQ+kg3xx/zNT02s/0o+81h/3zhvFSB24NtqEr8FTw88O5W/JQ==", - "devOptional": true, + "dev": true, "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { @@ -4222,6 +4226,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -4238,6 +4243,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -4254,6 +4260,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -4270,6 +4277,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -4286,6 +4294,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -4302,6 +4311,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -4318,6 +4328,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -4334,6 +4345,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -4350,6 +4362,7 @@ "cpu": [ "ia32" ], + "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -4366,6 +4379,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -4379,7 +4393,7 @@ "version": "0.1.3", "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", - "devOptional": true, + "dev": true, "license": "Apache-2.0" }, "node_modules/@swc/helpers": { @@ -4395,7 +4409,7 @@ "version": "0.1.24", "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.24.tgz", "integrity": "sha512-tjTMh3V4vAORHtdTprLlfoMptu1WfTZG9Rsca6yOKyNYsRr+MUXutKmliB17orgSZk5DpnDxs8GUdd/qwYxOng==", - "devOptional": true, + "dev": true, "license": "Apache-2.0", "dependencies": { "@swc/counter": "^0.1.3" @@ -5005,6 +5019,16 @@ "undici-types": "~6.21.0" } }, + "node_modules/@types/nodemailer": { + "version": "7.0.9", + "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-7.0.9.tgz", + "integrity": "sha512-vI8oF1M+8JvQhsId0Pc38BdUP2evenIIys7c7p+9OZXSPOH5c1dyINP1jT8xQ2xPuBUXmIC87s+91IZMDjH8Ow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/oauth": { "version": "0.9.6", "resolved": "https://registry.npmjs.org/@types/oauth/-/oauth-0.9.6.tgz", @@ -14013,6 +14037,15 @@ "dev": true, "license": "MIT" }, + "node_modules/nodemailer": { + "version": "7.0.13", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.13.tgz", + "integrity": "sha512-PNDFSJdP+KFgdsG3ZzMXCgquO7I6McjY2vlqILjtJd0hy8wEvtugS9xKRF2NWlPNGxvLCXlTNIae4serI7dinw==", + "license": "MIT-0", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/nopt": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", diff --git a/package.json b/package.json index 962ebb4..3c22fdb 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,9 @@ "test": "echo \"Error: no test specified\" && exit 1", "dev:frontend": "npm --workspace frontend run dev", "dev:backend": "npm --workspace backend run start:dev", - "dev": "concurrently \"npm run dev:frontend\" \"npm run dev:backend\"" + "dev": "concurrently \"npm run dev:frontend\" \"npm run dev:backend\"", + "lint:frontend": "npm --workspace frontend run lint", + "lint:backend": "npm --workspace backend run lint" }, "repository": { "type": "git", @@ -28,5 +30,8 @@ "homepage": "https://github.com/MindBlockLabs/mindBlock_Backend#readme", "dependencies": { "tsconfig-paths": "^4.2.0" + }, + "devDependencies": { + "@types/nodemailer": "^7.0.9" } } diff --git a/shared/abis/MyContract.json b/shared/abis/MyContract.json deleted file mode 100644 index e69de29..0000000 diff --git a/shared/utils/starknet.ts b/shared/utils/starknet.ts deleted file mode 100644 index 77b003b..0000000 --- a/shared/utils/starknet.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { Provider, Contract } from "starknet"; - -export const getContract = (abi: any, address: string, provider: Provider) => { - return new Contract(abi, address, provider); -}; \ No newline at end of file