Files
tabungin/PROFILE_IMPROVEMENTS_PLAN.md
dwindown 249f3a9d7d feat: remove OTP gate from transactions, fix categories auth, add implementation plan
- Remove OtpGateGuard from transactions controller (OTP verified at login)
- Fix categories controller to use authenticated user instead of TEMP_USER_ID
- Add comprehensive implementation plan document
- Update .env.example with WEB_APP_URL
- Prepare for admin dashboard development
2025-10-11 14:00:11 +07:00

9.3 KiB

📋 Profile Page Improvements - Implementation Plan

Completed:

  1. Avatar download and local storage (fixes Google rate limit)
  2. WhatsApp OTP full implementation

TODO:


1. Avatar Download & Storage DONE

Implementation:

  • Downloads Google avatar and stores in /public/avatars/
  • Serves from http://localhost:3001/avatars/{userId}.jpg
  • No more rate limits!

Files Modified:

  • apps/api/src/auth/auth.service.ts - Added downloadAndStoreAvatar() method
  • apps/api/src/main.ts - Configured static file serving

Testing:

  1. Login with Google
  2. Avatar downloads to apps/api/public/avatars/{userId}.jpg
  3. Avatar URL in database: /avatars/{userId}.jpg
  4. Accessible at: http://localhost:3001/avatars/{userId}.jpg

2. Profile Page Redesign with Tabs

Current Structure:

Profile Page
├── Account Information Card
├── Password Change Card
└── OTP Security Card (all methods together)

New Structure:

Profile Page
├── Profile Card (with tabs)
│   ├── Edit Profile Tab
│   │   ├── Avatar Upload
│   │   ├── Name
│   │   ├── Email (readonly)
│   │   └── Phone
│   └── Security Tab
│       ├── Change Password Card
│       │   ├── Current Password
│       │   ├── New Password
│       │   └── Confirm Password
│       └── Two-Factor Authentication Card
│           ├── Phone Number
│           ├── WhatsApp OTP
│           ├── Email OTP
│           └── TOTP

Implementation Steps:

A. Add Avatar Upload

Backend:

  1. Create upload endpoint: POST /api/users/avatar
  2. Use multer for file upload
  3. Save to /public/avatars/{userId}.jpg
  4. Update user's avatarUrl

Frontend:

  1. Add file input with preview
  2. Show current avatar
  3. Upload button
  4. Loading state

B. Reorganize Profile Page

Create Tabs:

<Tabs defaultValue="profile">
  <TabsList>
    <TabsTrigger value="profile">Edit Profile</TabsTrigger>
    <TabsTrigger value="security">Security</TabsTrigger>
  </TabsList>
  
  <TabsContent value="profile">
    {/* Avatar, Name, Email, Phone */}
  </TabsContent>
  
  <TabsContent value="security">
    {/* Password Change + 2FA */}
  </TabsContent>
</Tabs>

3. Account Deletion

Backend Implementation:

Endpoint: DELETE /api/users/account

Steps:

  1. Verify user password
  2. Delete related data (transactions, categories, etc.)
  3. Delete user account
  4. Return success

Code:

// users.controller.ts
@Delete('account')
async deleteAccount(
  @Req() req: RequestWithUser,
  @Body() body: { password: string }
) {
  return this.users.deleteAccount(req.user.userId, body.password)
}

// users.service.ts
async deleteAccount(userId: string, password: string) {
  // 1. Get user and verify password
  const user = await this.prisma.user.findUnique({
    where: { id: userId },
    select: { passwordHash: true }
  })
  
  if (user?.passwordHash) {
    const isValid = await bcrypt.compare(password, user.passwordHash)
    if (!isValid) {
      throw new UnauthorizedException('Invalid password')
    }
  }
  
  // 2. Delete related data
  await this.prisma.$transaction([
    this.prisma.transaction.deleteMany({ where: { userId } }),
    this.prisma.category.deleteMany({ where: { userId } }),
    this.prisma.authAccount.deleteMany({ where: { userId } }),
    this.prisma.user.delete({ where: { id: userId } })
  ])
  
  return { success: true, message: 'Account deleted successfully' }
}

Frontend Implementation:

Add to Security Tab:

<Card className="border-destructive">
  <CardHeader>
    <CardTitle className="text-destructive">Danger Zone</CardTitle>
    <CardDescription>
      Permanently delete your account and all data
    </CardDescription>
  </CardHeader>
  <CardContent>
    <Button 
      variant="destructive"
      onClick={() => setShowDeleteDialog(true)}
    >
      Delete Account
    </Button>
  </CardContent>
</Card>

{/* Confirmation Dialog */}
<AlertDialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
  <AlertDialogContent>
    <AlertDialogHeader>
      <AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
      <AlertDialogDescription>
        This action cannot be undone. This will permanently delete your
        account and remove all your data from our servers.
      </AlertDialogDescription>
    </AlertDialogHeader>
    <div className="space-y-2">
      <Label>Enter your password to confirm</Label>
      <Input
        type="password"
        value={deletePassword}
        onChange={(e) => setDeletePassword(e.target.value)}
      />
    </div>
    <AlertDialogFooter>
      <AlertDialogCancel>Cancel</AlertDialogCancel>
      <AlertDialogAction
        onClick={handleDeleteAccount}
        className="bg-destructive"
      >
        Delete Account
      </AlertDialogAction>
    </AlertDialogFooter>
  </AlertDialogContent>
</AlertDialog>

4. WhatsApp OTP Resend

Backend Implementation:

Endpoint: POST /api/otp/whatsapp/resend

Code:

// otp.controller.ts
@Public()
@Post('whatsapp/resend')
async resendWhatsappOtp(@Body() body: { tempToken: string }) {
  try {
    // Verify temp token
    const payload = this.jwtService.verify(body.tempToken) as {
      temp?: boolean;
      userId?: string;
      sub?: string;
    };

    if (!payload.temp) {
      throw new UnauthorizedException('Invalid token type');
    }

    const userId = payload.userId || payload.sub;

    if (!userId) {
      throw new UnauthorizedException('Invalid token payload');
    }

    // Send WhatsApp OTP (live mode for login)
    return this.otpService.sendWhatsappOtp(userId, 'live');
  } catch {
    throw new UnauthorizedException('Invalid or expired token');
  }
}

Frontend Implementation:

Update OTP Verification Page:

// Add resend handler for WhatsApp
const handleResendWhatsApp = async () => {
  setResendLoading(true)
  setError('')
  
  try {
    await axios.post(`${API_URL}/api/otp/whatsapp/resend`, {
      tempToken
    })
    
    setResendTimer(30)
    setCanResend(false)
    setError('')
  } catch (err) {
    setError('Failed to resend code. Please try again.')
  } finally {
    setResendLoading(false)
  }
}

// Add resend button in WhatsApp tab
<TabsContent value="whatsapp" className="space-y-4">
  {/* ... existing code ... */}
  
  <Button
    type="button"
    variant="outline"
    className="w-full"
    onClick={handleResendWhatsApp}
    disabled={!canResend || resendLoading || loading}
  >
    {resendLoading ? (
      <>
        <RefreshCw className="mr-2 h-4 w-4 animate-spin" />
        Sending...
      </>
    ) : canResend ? (
      <>
        <Smartphone className="mr-2 h-4 w-4" />
        Resend Code
      </>
    ) : (
      <>
        <Smartphone className="mr-2 h-4 w-4" />
        Resend in {resendTimer}s
      </>
    )}
  </Button>
</TabsContent>

5. Auth Pages Design Restoration

Check Git History:

# Find commits that modified Login/Register pages
git log --all --full-history -- "apps/web/src/components/pages/Login.tsx"
git log --all --full-history -- "apps/web/src/components/pages/Register.tsx"

# View specific commit
git show <commit-hash>:apps/web/src/components/pages/Login.tsx

Steps:

  1. Find the preferred design commit
  2. Compare with current version
  3. Restore styling and layout
  4. Keep current functionality (OTP, Google OAuth)
  5. Test responsiveness

📊 Implementation Priority:

Phase 1: Critical (Do First)

  1. Avatar download & storage - DONE
  2. WhatsApp OTP resend
  3. Profile page tabs reorganization

Phase 2: Important (Do Next)

  1. Avatar upload functionality
  2. Account deletion

Phase 3: Nice to Have (Do Last)

  1. Auth pages design restoration

📝 Files to Modify:

Backend:

  1. apps/api/src/auth/auth.service.ts - Avatar download (DONE)
  2. apps/api/src/main.ts - Static files (DONE)
  3. apps/api/src/otp/otp.controller.ts - WhatsApp resend
  4. apps/api/src/users/users.controller.ts - Avatar upload, delete account
  5. apps/api/src/users/users.service.ts - Delete account logic

Frontend:

  1. apps/web/src/components/pages/Profile.tsx - Tabs, avatar upload
  2. apps/web/src/components/pages/OtpVerification.tsx - WhatsApp resend
  3. apps/web/src/components/pages/Login.tsx - Design restoration
  4. apps/web/src/components/pages/Register.tsx - Design restoration

🧪 Testing Checklist:

Avatar Download:

  • Login with Google
  • Check apps/api/public/avatars/ folder
  • Avatar file exists
  • Avatar loads in Profile page
  • No rate limit errors

WhatsApp Resend:

  • Login triggers WhatsApp OTP
  • Wait 30 seconds
  • Click "Resend Code"
  • New code received
  • Timer resets

Profile Tabs:

  • See "Edit Profile" and "Security" tabs
  • Edit Profile shows avatar, name, email, phone
  • Security shows password change and 2FA
  • Tab switching works

Account Deletion:

  • Click "Delete Account"
  • Confirmation dialog appears
  • Enter password
  • Account deleted
  • Redirected to login
  • Cannot login with deleted account

Ready to implement! Start with WhatsApp resend, then profile tabs! 🚀