Spaces:
Sleeping
Sleeping
deployment v0.1
Browse files- .env +27 -0
- Dockerfile +17 -0
- README.md +393 -10
- database/connection.go +56 -0
- go.mod +70 -0
- go.sum +232 -0
- handlers/achievements.go +54 -0
- handlers/ai_reflection.go +127 -0
- handlers/auth.go +42 -0
- handlers/chat.go +244 -0
- handlers/comments.go +143 -0
- handlers/dashboard.go +41 -0
- handlers/device_token.go +81 -0
- handlers/feed.go +108 -0
- handlers/google_auth.go +87 -0
- handlers/moderation.go +61 -0
- handlers/mood.go +87 -0
- handlers/reports.go +83 -0
- handlers/stats.go +54 -0
- handlers/support.go +73 -0
- handlers/user_reset.go +77 -0
- handlers/user_stats.go +115 -0
- handlers/websocket.go +184 -0
- main.go +82 -0
- middleware/rate_limiter.go +81 -0
- middleware/user_validation.go +32 -0
- models/achievement.go +59 -0
- models/ai_reflection.go +33 -0
- models/chat.go +57 -0
- models/device_token.go +30 -0
- models/google_auth.go +13 -0
- models/journal_comment.go +37 -0
- models/journal_report.go +26 -0
- models/journal_support.go +19 -0
- models/mood_entry.go +44 -0
- models/user.go +68 -0
- moderation/words.txt +18 -0
- routes/routes.go +70 -0
- services/achievements.go +144 -0
- services/chat_matching.go +78 -0
- services/gemini.go +99 -0
- services/google_auth.go +35 -0
- services/moderation.go +95 -0
- services/push_notification.go +154 -0
- utils/response.go +32 -0
.env
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
DB_HOST=pongo.kencang.com
|
| 2 |
+
DB_PORT=3306
|
| 3 |
+
DB_USER=academyc_root_pp
|
| 4 |
+
DB_PASS=Langkahpemula123
|
| 5 |
+
DB_NAME=academyc_moodlink
|
| 6 |
+
PORT=3000
|
| 7 |
+
|
| 8 |
+
# JWT
|
| 9 |
+
JWT_SECRET=a7f278d3443f86d9625fd1192b90502e95c285929f94e213e1472c4a369d6e5c
|
| 10 |
+
|
| 11 |
+
# OAuth Google
|
| 12 |
+
GOOGLE_CLIENT_ID=158043402219-a7olere9afjoqi7treqjh1ilt4td8rr1.apps.googleusercontent.com
|
| 13 |
+
GOOGLE_CLIENT_SECRET=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
|
| 14 |
+
|
| 15 |
+
# OAuth GitHub
|
| 16 |
+
GITHUB_CLIENT_ID=Ov23liNsFFydwvDS0nrc
|
| 17 |
+
GITHUB_CLIENT_SECRET=a8c680d652a62f57e773116ddbe0aeffdcd9576f
|
| 18 |
+
|
| 19 |
+
# OAuth Facebook
|
| 20 |
+
FACEBOOK_APP_ID=1742762306350480
|
| 21 |
+
FACEBOOK_APP_SECRET=468cfb99799744bada9330efd9f8b5b2
|
| 22 |
+
|
| 23 |
+
# Firebase Configuration (optional)
|
| 24 |
+
FIREBASE_SERVICE_ACCOUNT_PATH=path/to/firebase-service-account.json
|
| 25 |
+
|
| 26 |
+
# Google Gemini API Configuration
|
| 27 |
+
GEMINI_API_KEY=AIzaSyA8SKk2FxIf3lQdsJIHWUhR1ZK8s2Yh2ZI
|
Dockerfile
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM golang:1.23
|
| 2 |
+
|
| 3 |
+
# Set working directory
|
| 4 |
+
WORKDIR /app
|
| 5 |
+
|
| 6 |
+
# Copy go.mod and go.sum first to leverage Docker cache
|
| 7 |
+
COPY go.mod go.sum ./
|
| 8 |
+
RUN go mod download
|
| 9 |
+
|
| 10 |
+
# Copy the rest of the application
|
| 11 |
+
COPY . .
|
| 12 |
+
|
| 13 |
+
# Build the Go app
|
| 14 |
+
RUN go build -o moodlink-backend
|
| 15 |
+
|
| 16 |
+
# Run the binary
|
| 17 |
+
CMD ["./moodlink-backend"]
|
README.md
CHANGED
|
@@ -1,10 +1,393 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
-
|
| 9 |
-
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# MoodLink Backend
|
| 2 |
+
|
| 3 |
+
A realtime emotional journaling system backend built with Go and Fiber framework with advanced features including Google Auth, achievements, push notifications, and automated moderation.
|
| 4 |
+
|
| 5 |
+
## Features
|
| 6 |
+
|
| 7 |
+
- **Anonymous User Registration**: UUID-based anonymous user system
|
| 8 |
+
- **Google Auth Integration**: Optional Google account binding for data sync
|
| 9 |
+
- **Mood Check-In**: Daily mood tracking with location and notes
|
| 10 |
+
- **Gamification**: Points and level system with streak bonuses
|
| 11 |
+
- **Achievements System**: Badge system with various challenges
|
| 12 |
+
- **Anonymous Feed**: View recent mood entries from other users
|
| 13 |
+
- **Realtime Updates**: WebSocket support for live feed updates
|
| 14 |
+
- **Support System**: Send emotional support reactions to journal entries
|
| 15 |
+
- **Push Notifications**: FCM-based notifications for support and reminders
|
| 16 |
+
- **Dashboard**: User statistics and progress tracking
|
| 17 |
+
- **Rate Limiting**: Protect sensitive endpoints from abuse
|
| 18 |
+
- **WebSocket Notifications**: Realtime support notifications to users
|
| 19 |
+
- **Global Statistics**: System-wide analytics and insights
|
| 20 |
+
- **Content Moderation**: Report and manage inappropriate content
|
| 21 |
+
- **Automated Moderation**: Content filtering with configurable terms
|
| 22 |
+
- **Location-Based Feed**: Filter feed entries by geographic proximity
|
| 23 |
+
- **Account Reset**: Complete data deletion and new UUID generation
|
| 24 |
+
|
| 25 |
+
## API Endpoints
|
| 26 |
+
|
| 27 |
+
### Authentication
|
| 28 |
+
- `GET /api/v1/user` - Register or get user (requires User-ID header)
|
| 29 |
+
- `POST /api/v1/auth/google` - Link Google account to UUID
|
| 30 |
+
- `POST /api/v1/auth/restore` - Restore UUID from Google account
|
| 31 |
+
|
| 32 |
+
### Mood Tracking
|
| 33 |
+
- `POST /api/v1/checkin` - Submit daily mood check-in
|
| 34 |
+
|
| 35 |
+
### Feed
|
| 36 |
+
- `GET /api/v1/feed` - Get recent mood entries (supports pagination)
|
| 37 |
+
- Optional query parameters: `lat`, `lng`, `radius_km` for location filtering
|
| 38 |
+
- `WS /ws/feed` - WebSocket connection for realtime feed updates
|
| 39 |
+
- Requires `user_id` query parameter for targeted notifications
|
| 40 |
+
|
| 41 |
+
### Support
|
| 42 |
+
- `POST /api/v1/support` - Send support reaction to journal entry
|
| 43 |
+
|
| 44 |
+
### Comments
|
| 45 |
+
- `POST /api/v1/comment` - Add comment to journal entry
|
| 46 |
+
- `GET /api/v1/comment/:journal_id` - Get comments for journal entry
|
| 47 |
+
|
| 48 |
+
### Anonymous Chat
|
| 49 |
+
- `POST /api/v1/chat/start` - Start or join anonymous chat
|
| 50 |
+
- `POST /api/v1/chat/:room_id/message` - Send message in chat room
|
| 51 |
+
- `GET /api/v1/chat/:room_id/messages` - Get chat messages
|
| 52 |
+
- `DELETE /api/v1/chat/:room_id/end` - End chat room
|
| 53 |
+
- `GET /api/v1/chat/active` - Get active chat rooms for user
|
| 54 |
+
|
| 55 |
+
### AI Reflection
|
| 56 |
+
- `POST /api/v1/ai/reflect` - Get AI-powered emotional reflection
|
| 57 |
+
- `GET /api/v1/ai/history` - Get reflection history
|
| 58 |
+
|
| 59 |
+
### Dashboard
|
| 60 |
+
- `GET /api/v1/dashboard/:user_id` - Get user dashboard data
|
| 61 |
+
- `GET /api/v1/user/stats` - Get detailed user statistics
|
| 62 |
+
|
| 63 |
+
### User Management
|
| 64 |
+
- `POST /api/v1/user/reset` - Reset account and delete all data
|
| 65 |
+
|
| 66 |
+
### Achievements
|
| 67 |
+
- `GET /api/v1/achievements/:user_id` - Get user achievements
|
| 68 |
+
|
| 69 |
+
### Push Notifications
|
| 70 |
+
- `POST /api/v1/device-token` - Register FCM device token
|
| 71 |
+
- `DELETE /api/v1/device-token` - Unregister FCM device token
|
| 72 |
+
### Statistics
|
| 73 |
+
- `GET /api/v1/stats/global` - Get global system statistics
|
| 74 |
+
|
| 75 |
+
### Moderation
|
| 76 |
+
- `POST /api/v1/report` - Report inappropriate journal entry
|
| 77 |
+
- `GET /api/v1/reports` - List reports (admin only, requires `Is-Admin: true` header)
|
| 78 |
+
- `GET /api/v1/moderation/flagged` - List flagged entries (admin only)
|
| 79 |
+
- `PUT /api/v1/moderation/unflag/:entry_id` - Unflag entry (admin only)
|
| 80 |
+
|
| 81 |
+
## Database Schema
|
| 82 |
+
|
| 83 |
+
### anonymous_users
|
| 84 |
+
- `id` (UUID, PRIMARY KEY)
|
| 85 |
+
- `google_id` (VARCHAR, NULLABLE, UNIQUE)
|
| 86 |
+
- `is_synced` (BOOLEAN, DEFAULT false)
|
| 87 |
+
- `created_at` (DATETIME)
|
| 88 |
+
- `points` (INT, DEFAULT 0)
|
| 89 |
+
- `level` (INT, DEFAULT 1)
|
| 90 |
+
- `last_checkin` (DATE)
|
| 91 |
+
|
| 92 |
+
### mood_entries
|
| 93 |
+
- `id` (UUID, PRIMARY KEY)
|
| 94 |
+
- `user_id` (UUID, FOREIGN KEY)
|
| 95 |
+
- `mood` (ENUM: 'happy','sad','tired','angry','anxious','neutral')
|
| 96 |
+
- `note` (TEXT)
|
| 97 |
+
- `is_flagged` (BOOLEAN, DEFAULT false)
|
| 98 |
+
- `location_lat` (FLOAT)
|
| 99 |
+
- `location_lng` (FLOAT)
|
| 100 |
+
- `created_at` (DATETIME)
|
| 101 |
+
|
| 102 |
+
### journal_supports
|
| 103 |
+
- `id` (INT, PRIMARY KEY)
|
| 104 |
+
- `journal_id` (UUID, FOREIGN KEY)
|
| 105 |
+
- `type` (ENUM: 'heart','hug','pray')
|
| 106 |
+
- `created_at` (DATETIME)
|
| 107 |
+
|
| 108 |
+
### journal_reports
|
| 109 |
+
- `id` (UUID, PRIMARY KEY)
|
| 110 |
+
- `journal_id` (UUID, FOREIGN KEY)
|
| 111 |
+
- `reason` (VARCHAR)
|
| 112 |
+
- `created_at` (DATETIME)
|
| 113 |
+
|
| 114 |
+
### user_achievements
|
| 115 |
+
- `id` (INT, PRIMARY KEY)
|
| 116 |
+
- `user_id` (UUID, FOREIGN KEY)
|
| 117 |
+
- `badge_name` (VARCHAR)
|
| 118 |
+
- `earned_at` (DATETIME)
|
| 119 |
+
|
| 120 |
+
### device_tokens
|
| 121 |
+
- `id` (INT, PRIMARY KEY)
|
| 122 |
+
- `user_id` (UUID, FOREIGN KEY)
|
| 123 |
+
- `token` (TEXT)
|
| 124 |
+
- `created_at` (DATETIME)
|
| 125 |
+
- `updated_at` (DATETIME)
|
| 126 |
+
|
| 127 |
+
### journal_comments
|
| 128 |
+
- `id` (UUID, PRIMARY KEY)
|
| 129 |
+
- `journal_id` (UUID, FOREIGN KEY)
|
| 130 |
+
- `user_id` (UUID, FOREIGN KEY)
|
| 131 |
+
- `comment` (TEXT)
|
| 132 |
+
- `is_flagged` (BOOLEAN, DEFAULT false)
|
| 133 |
+
- `created_at` (DATETIME)
|
| 134 |
+
|
| 135 |
+
### chat_rooms
|
| 136 |
+
- `id` (UUID, PRIMARY KEY)
|
| 137 |
+
- `user_a` (UUID, FOREIGN KEY)
|
| 138 |
+
- `user_b` (UUID, FOREIGN KEY)
|
| 139 |
+
- `is_active` (BOOLEAN, DEFAULT true)
|
| 140 |
+
- `created_at` (DATETIME)
|
| 141 |
+
- `updated_at` (DATETIME)
|
| 142 |
+
|
| 143 |
+
### chat_messages
|
| 144 |
+
- `id` (UUID, PRIMARY KEY)
|
| 145 |
+
- `room_id` (UUID, FOREIGN KEY)
|
| 146 |
+
- `sender_id` (UUID, FOREIGN KEY)
|
| 147 |
+
- `message` (TEXT)
|
| 148 |
+
- `created_at` (DATETIME)
|
| 149 |
+
|
| 150 |
+
### ai_reflections
|
| 151 |
+
- `id` (UUID, PRIMARY KEY)
|
| 152 |
+
- `user_id` (UUID, FOREIGN KEY)
|
| 153 |
+
- `question` (TEXT)
|
| 154 |
+
- `answer` (TEXT)
|
| 155 |
+
- `created_at` (DATETIME)
|
| 156 |
+
|
| 157 |
+
## Environment Variables
|
| 158 |
+
|
| 159 |
+
```env
|
| 160 |
+
DB_HOST=pongo.kencang.com
|
| 161 |
+
DB_PORT=3306
|
| 162 |
+
DB_USER=academyc_root_pp
|
| 163 |
+
DB_PASS=Langkahpemula123
|
| 164 |
+
DB_NAME=academyc_moodlink
|
| 165 |
+
PORT=3000
|
| 166 |
+
|
| 167 |
+
# Firebase Configuration (optional)
|
| 168 |
+
FIREBASE_SERVICE_ACCOUNT_PATH=path/to/firebase-service-account.json
|
| 169 |
+
|
| 170 |
+
# Google Gemini API Configuration
|
| 171 |
+
GEMINI_API_KEY=AIzaSyA8SKk2FxIf3lQdsJIHWUhR1ZK8s2Yh2ZI
|
| 172 |
+
```
|
| 173 |
+
|
| 174 |
+
## Achievements System
|
| 175 |
+
|
| 176 |
+
Available achievements:
|
| 177 |
+
- **First Step** 🌟: Complete first check-in
|
| 178 |
+
- **Streak 7 Hari** 🔥: Check-in for 7 consecutive days
|
| 179 |
+
- **Supporter** ❤️: Give 10 support reactions
|
| 180 |
+
- **Emotional Explorer** 🌈: Experience 5 different moods in 7 days
|
| 181 |
+
- **Level Master** 👑: Reach level 5
|
| 182 |
+
|
| 183 |
+
## Push Notifications
|
| 184 |
+
|
| 185 |
+
The system supports Firebase Cloud Messaging (FCM) for push notifications:
|
| 186 |
+
- Support received notifications
|
| 187 |
+
- Inactivity reminders (24h after last check-in)
|
| 188 |
+
- Configurable via Firebase service account
|
| 189 |
+
|
| 190 |
+
## Content Moderation
|
| 191 |
+
|
| 192 |
+
Automated content filtering using configurable terms in `moderation/words.txt`:
|
| 193 |
+
- Real-time content scanning during check-in
|
| 194 |
+
- Flagged content marked in database
|
| 195 |
+
- Admin interface for reviewing flagged content
|
| 196 |
+
- Regex-based pattern matching
|
| 197 |
+
|
| 198 |
+
## Rate Limiting
|
| 199 |
+
|
| 200 |
+
The following endpoints are rate-limited to 5 requests per user per minute:
|
| 201 |
+
- `POST /api/v1/checkin`
|
| 202 |
+
- `POST /api/v1/support`
|
| 203 |
+
|
| 204 |
+
Rate limiting is based on the `User-ID` header.
|
| 205 |
+
|
| 206 |
+
## Points System
|
| 207 |
+
|
| 208 |
+
- **Check-in**: +10 points
|
| 209 |
+
- **Streak bonus**: +20 points (for consecutive days)
|
| 210 |
+
- **Sending support**: +2 points
|
| 211 |
+
- **Receiving support**: +5 points
|
| 212 |
+
- **Adding comment**: +3 points
|
| 213 |
+
- **Receiving comment**: +2 points
|
| 214 |
+
- **Chat message**: +1 point
|
| 215 |
+
- **AI reflection**: +5 points
|
| 216 |
+
|
| 217 |
+
## Level System
|
| 218 |
+
|
| 219 |
+
- Level 1: < 100 points
|
| 220 |
+
- Level 2: 100-299 points
|
| 221 |
+
- Level 3: 300-699 points
|
| 222 |
+
- Level 4: 700-1499 points
|
| 223 |
+
- Level 5+: 1500+ points
|
| 224 |
+
|
| 225 |
+
## Google Auth Integration
|
| 226 |
+
|
| 227 |
+
Optional Google account binding allows users to:
|
| 228 |
+
- Link their anonymous UUID to Google account
|
| 229 |
+
- Restore their UUID after app reinstall
|
| 230 |
+
- Maintain data continuity across devices
|
| 231 |
+
|
| 232 |
+
### Link Google Account
|
| 233 |
+
```bash
|
| 234 |
+
curl -X POST http://localhost:3000/api/v1/auth/google \
|
| 235 |
+
-H "Content-Type: application/json" \
|
| 236 |
+
-H "User-ID: 550e8400-e29b-41d4-a716-446655440000" \
|
| 237 |
+
-d '{
|
| 238 |
+
"id_token": "google_id_token_here"
|
| 239 |
+
}'
|
| 240 |
+
```
|
| 241 |
+
|
| 242 |
+
### Restore Account
|
| 243 |
+
```bash
|
| 244 |
+
curl -X POST http://localhost:3000/api/v1/auth/restore \
|
| 245 |
+
-H "Content-Type: application/json" \
|
| 246 |
+
-d '{
|
| 247 |
+
"id_token": "google_id_token_here"
|
| 248 |
+
}'
|
| 249 |
+
```
|
| 250 |
+
|
| 251 |
+
## Usage
|
| 252 |
+
|
| 253 |
+
1. Set up MySQL database
|
| 254 |
+
2. Copy `.env.example` to `.env` and configure
|
| 255 |
+
3. (Optional) Set up Firebase project and download service account JSON
|
| 256 |
+
3. Run `go mod download` to install dependencies
|
| 257 |
+
4. Run `go run main.go` to start the server
|
| 258 |
+
|
| 259 |
+
## WebSocket Usage
|
| 260 |
+
|
| 261 |
+
Connect to `/ws/feed?user_id=<uuid>` to receive realtime updates.
|
| 262 |
+
|
| 263 |
+
### Message Types
|
| 264 |
+
|
| 265 |
+
**New Entry Broadcast:**
|
| 266 |
+
```json
|
| 267 |
+
{
|
| 268 |
+
"type": "new_entry",
|
| 269 |
+
"data": {
|
| 270 |
+
"id": "uuid",
|
| 271 |
+
"mood": "happy",
|
| 272 |
+
"note": "Feeling great today!",
|
| 273 |
+
"created_at": "2024-01-01T12:00:00Z",
|
| 274 |
+
"support_count": 0
|
| 275 |
+
}
|
| 276 |
+
}
|
| 277 |
+
```
|
| 278 |
+
|
| 279 |
+
**Support Notification (targeted to specific user):**
|
| 280 |
+
```json
|
| 281 |
+
{
|
| 282 |
+
"type": "support_received",
|
| 283 |
+
"data": {
|
| 284 |
+
"journal_id": "uuid",
|
| 285 |
+
"support": "heart"
|
| 286 |
+
}
|
| 287 |
+
}
|
| 288 |
+
```
|
| 289 |
+
|
| 290 |
+
## API Examples
|
| 291 |
+
|
| 292 |
+
### Check-in
|
| 293 |
+
```bash
|
| 294 |
+
curl -X POST http://localhost:3000/api/v1/checkin \
|
| 295 |
+
-H "Content-Type: application/json" \
|
| 296 |
+
-d '{
|
| 297 |
+
"user_id": "550e8400-e29b-41d4-a716-446655440000",
|
| 298 |
+
"mood": "happy",
|
| 299 |
+
"note": "Had a great day at work!",
|
| 300 |
+
"location_lat": 40.7128,
|
| 301 |
+
"location_lng": -74.0060
|
| 302 |
+
}'
|
| 303 |
+
```
|
| 304 |
+
|
| 305 |
+
### Send Support
|
| 306 |
+
```bash
|
| 307 |
+
curl -X POST http://localhost:3000/api/v1/support \
|
| 308 |
+
-H "Content-Type: application/json" \
|
| 309 |
+
-H "User-ID: 550e8400-e29b-41d4-a716-446655440000" \
|
| 310 |
+
-d '{
|
| 311 |
+
"journal_id": "550e8400-e29b-41d4-a716-446655440001",
|
| 312 |
+
"type": "heart"
|
| 313 |
+
}'
|
| 314 |
+
```
|
| 315 |
+
|
| 316 |
+
### Location-Based Feed
|
| 317 |
+
```bash
|
| 318 |
+
curl "http://localhost:3000/api/v1/feed?lat=40.7128&lng=-74.0060&radius_km=10"
|
| 319 |
+
```
|
| 320 |
+
|
| 321 |
+
### Report Journal Entry
|
| 322 |
+
```bash
|
| 323 |
+
curl -X POST http://localhost:3000/api/v1/report \
|
| 324 |
+
-H "Content-Type: application/json" \
|
| 325 |
+
-d '{
|
| 326 |
+
"journal_id": "550e8400-e29b-41d4-a716-446655440001",
|
| 327 |
+
"reason": "inappropriate content"
|
| 328 |
+
}'
|
| 329 |
+
```
|
| 330 |
+
|
| 331 |
+
### Global Statistics
|
| 332 |
+
```bash
|
| 333 |
+
curl http://localhost:3000/api/v1/stats/global
|
| 334 |
+
```
|
| 335 |
+
|
| 336 |
+
### Register Device Token
|
| 337 |
+
```bash
|
| 338 |
+
curl -X POST http://localhost:3000/api/v1/device-token \
|
| 339 |
+
-H "Content-Type: application/json" \
|
| 340 |
+
-H "User-ID: 550e8400-e29b-41d4-a716-446655440000" \
|
| 341 |
+
-d '{
|
| 342 |
+
"token": "fcm_device_token_here"
|
| 343 |
+
}'
|
| 344 |
+
```
|
| 345 |
+
|
| 346 |
+
### Get User Statistics
|
| 347 |
+
```bash
|
| 348 |
+
curl -H "User-ID: 550e8400-e29b-41d4-a716-446655440000" \
|
| 349 |
+
http://localhost:3000/api/v1/user/stats
|
| 350 |
+
```
|
| 351 |
+
|
| 352 |
+
### Reset Account
|
| 353 |
+
```bash
|
| 354 |
+
curl -X POST http://localhost:3000/api/v1/user/reset \
|
| 355 |
+
-H "User-ID: 550e8400-e29b-41d4-a716-446655440000"
|
| 356 |
+
```
|
| 357 |
+
|
| 358 |
+
### Add Comment
|
| 359 |
+
```bash
|
| 360 |
+
curl -X POST http://localhost:3000/api/v1/comment \
|
| 361 |
+
-H "Content-Type: application/json" \
|
| 362 |
+
-H "User-ID: 550e8400-e29b-41d4-a716-446655440000" \
|
| 363 |
+
-d '{
|
| 364 |
+
"journal_id": "550e8400-e29b-41d4-a716-446655440001",
|
| 365 |
+
"comment": "Thanks for sharing this!"
|
| 366 |
+
}'
|
| 367 |
+
```
|
| 368 |
+
|
| 369 |
+
### Start Anonymous Chat
|
| 370 |
+
```bash
|
| 371 |
+
curl -X POST http://localhost:3000/api/v1/chat/start \
|
| 372 |
+
-H "User-ID: 550e8400-e29b-41d4-a716-446655440000"
|
| 373 |
+
```
|
| 374 |
+
|
| 375 |
+
### Send Chat Message
|
| 376 |
+
```bash
|
| 377 |
+
curl -X POST http://localhost:3000/api/v1/chat/room-uuid/message \
|
| 378 |
+
-H "Content-Type: application/json" \
|
| 379 |
+
-H "User-ID: 550e8400-e29b-41d4-a716-446655440000" \
|
| 380 |
+
-d '{
|
| 381 |
+
"message": "Hello there!"
|
| 382 |
+
}'
|
| 383 |
+
```
|
| 384 |
+
|
| 385 |
+
### AI Reflection
|
| 386 |
+
```bash
|
| 387 |
+
curl -X POST http://localhost:3000/api/v1/ai/reflect \
|
| 388 |
+
-H "Content-Type: application/json" \
|
| 389 |
+
-H "User-ID: 550e8400-e29b-41d4-a716-446655440000" \
|
| 390 |
+
-d '{
|
| 391 |
+
"prompt": "I am feeling anxious about work today. How can I manage this feeling?"
|
| 392 |
+
}'
|
| 393 |
+
```
|
database/connection.go
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package database
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"fmt"
|
| 5 |
+
"log"
|
| 6 |
+
"os"
|
| 7 |
+
"moodlink-backend/models"
|
| 8 |
+
|
| 9 |
+
"gorm.io/driver/mysql"
|
| 10 |
+
"gorm.io/gorm"
|
| 11 |
+
"gorm.io/gorm/logger"
|
| 12 |
+
)
|
| 13 |
+
|
| 14 |
+
var DB *gorm.DB
|
| 15 |
+
|
| 16 |
+
func Connect() {
|
| 17 |
+
var err error
|
| 18 |
+
|
| 19 |
+
dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local",
|
| 20 |
+
os.Getenv("DB_USER"),
|
| 21 |
+
os.Getenv("DB_PASS"),
|
| 22 |
+
os.Getenv("DB_HOST"),
|
| 23 |
+
os.Getenv("DB_PORT"),
|
| 24 |
+
os.Getenv("DB_NAME"),
|
| 25 |
+
)
|
| 26 |
+
|
| 27 |
+
DB, err = gorm.Open(mysql.Open(dsn), &gorm.Config{
|
| 28 |
+
Logger: logger.Default.LogMode(logger.Info),
|
| 29 |
+
})
|
| 30 |
+
|
| 31 |
+
if err != nil {
|
| 32 |
+
log.Fatal("Failed to connect to database:", err)
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
log.Println("Database connected successfully")
|
| 36 |
+
|
| 37 |
+
// Auto migrate tables
|
| 38 |
+
err = DB.AutoMigrate(
|
| 39 |
+
&models.AnonymousUser{},
|
| 40 |
+
&models.MoodEntry{},
|
| 41 |
+
&models.JournalSupport{},
|
| 42 |
+
&models.JournalReport{},
|
| 43 |
+
&models.UserAchievement{},
|
| 44 |
+
&models.DeviceToken{},
|
| 45 |
+
&models.JournalComment{},
|
| 46 |
+
&models.ChatRoom{},
|
| 47 |
+
&models.ChatMessage{},
|
| 48 |
+
&models.AIReflection{},
|
| 49 |
+
)
|
| 50 |
+
|
| 51 |
+
if err != nil {
|
| 52 |
+
log.Fatal("Failed to migrate database:", err)
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
log.Println("Database migration completed")
|
| 56 |
+
}
|
go.mod
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
module moodlink-backend
|
| 2 |
+
|
| 3 |
+
go 1.23
|
| 4 |
+
|
| 5 |
+
toolchain go1.23.11
|
| 6 |
+
|
| 7 |
+
require (
|
| 8 |
+
firebase.google.com/go/v4 v4.12.0
|
| 9 |
+
github.com/go-sql-driver/mysql v1.7.1 // indirect
|
| 10 |
+
github.com/gofiber/fiber/v2 v2.52.0
|
| 11 |
+
github.com/gofiber/websocket/v2 v2.2.1
|
| 12 |
+
github.com/google/uuid v1.6.0
|
| 13 |
+
github.com/joho/godotenv v1.4.0
|
| 14 |
+
google.golang.org/api v0.197.0
|
| 15 |
+
gorm.io/driver/mysql v1.5.2
|
| 16 |
+
gorm.io/gorm v1.25.5
|
| 17 |
+
)
|
| 18 |
+
|
| 19 |
+
require (
|
| 20 |
+
cloud.google.com/go v0.116.0 // indirect
|
| 21 |
+
cloud.google.com/go/auth v0.9.3 // indirect
|
| 22 |
+
cloud.google.com/go/auth/oauth2adapt v0.2.4 // indirect
|
| 23 |
+
cloud.google.com/go/compute/metadata v0.5.0 // indirect
|
| 24 |
+
cloud.google.com/go/firestore v1.16.0 // indirect
|
| 25 |
+
cloud.google.com/go/iam v1.2.0 // indirect
|
| 26 |
+
cloud.google.com/go/longrunning v0.6.0 // indirect
|
| 27 |
+
cloud.google.com/go/storage v1.43.0 // indirect
|
| 28 |
+
github.com/MicahParks/keyfunc v1.9.0 // indirect
|
| 29 |
+
github.com/andybalholm/brotli v1.0.5 // indirect
|
| 30 |
+
github.com/fasthttp/websocket v1.5.4 // indirect
|
| 31 |
+
github.com/felixge/httpsnoop v1.0.4 // indirect
|
| 32 |
+
github.com/go-logr/logr v1.4.2 // indirect
|
| 33 |
+
github.com/go-logr/stdr v1.2.2 // indirect
|
| 34 |
+
github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
|
| 35 |
+
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
| 36 |
+
github.com/golang/protobuf v1.5.4 // indirect
|
| 37 |
+
github.com/google/s2a-go v0.1.8 // indirect
|
| 38 |
+
github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect
|
| 39 |
+
github.com/googleapis/gax-go/v2 v2.13.0 // indirect
|
| 40 |
+
github.com/jinzhu/inflection v1.0.0 // indirect
|
| 41 |
+
github.com/jinzhu/now v1.1.5 // indirect
|
| 42 |
+
github.com/klauspost/compress v1.17.0 // indirect
|
| 43 |
+
github.com/mattn/go-colorable v0.1.13 // indirect
|
| 44 |
+
github.com/mattn/go-isatty v0.0.20 // indirect
|
| 45 |
+
github.com/mattn/go-runewidth v0.0.15 // indirect
|
| 46 |
+
github.com/rivo/uniseg v0.2.0 // indirect
|
| 47 |
+
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee // indirect
|
| 48 |
+
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
| 49 |
+
github.com/valyala/fasthttp v1.51.0 // indirect
|
| 50 |
+
github.com/valyala/tcplisten v1.0.0 // indirect
|
| 51 |
+
go.opencensus.io v0.24.0 // indirect
|
| 52 |
+
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0 // indirect
|
| 53 |
+
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 // indirect
|
| 54 |
+
go.opentelemetry.io/otel v1.29.0 // indirect
|
| 55 |
+
go.opentelemetry.io/otel/metric v1.29.0 // indirect
|
| 56 |
+
go.opentelemetry.io/otel/trace v1.29.0 // indirect
|
| 57 |
+
golang.org/x/crypto v0.27.0 // indirect
|
| 58 |
+
golang.org/x/net v0.29.0 // indirect
|
| 59 |
+
golang.org/x/oauth2 v0.23.0 // indirect
|
| 60 |
+
golang.org/x/sync v0.8.0 // indirect
|
| 61 |
+
golang.org/x/sys v0.25.0 // indirect
|
| 62 |
+
golang.org/x/text v0.18.0 // indirect
|
| 63 |
+
golang.org/x/time v0.6.0 // indirect
|
| 64 |
+
google.golang.org/appengine/v2 v2.0.2 // indirect
|
| 65 |
+
google.golang.org/genproto v0.0.0-20240903143218-8af14fe29dc1 // indirect
|
| 66 |
+
google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1 // indirect
|
| 67 |
+
google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 // indirect
|
| 68 |
+
google.golang.org/grpc v1.66.2 // indirect
|
| 69 |
+
google.golang.org/protobuf v1.34.2 // indirect
|
| 70 |
+
)
|
go.sum
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
| 2 |
+
cloud.google.com/go v0.116.0 h1:B3fRrSDkLRt5qSHWe40ERJvhvnQwdZiHu0bJOpldweE=
|
| 3 |
+
cloud.google.com/go v0.116.0/go.mod h1:cEPSRWPzZEswwdr9BxE6ChEn01dWlTaF05LiC2Xs70U=
|
| 4 |
+
cloud.google.com/go/auth v0.9.3 h1:VOEUIAADkkLtyfr3BLa3R8Ed/j6w1jTBmARx+wb5w5U=
|
| 5 |
+
cloud.google.com/go/auth v0.9.3/go.mod h1:7z6VY+7h3KUdRov5F1i8NDP5ZzWKYmEPO842BgCsmTk=
|
| 6 |
+
cloud.google.com/go/auth/oauth2adapt v0.2.4 h1:0GWE/FUsXhf6C+jAkWgYm7X9tK8cuEIfy19DBn6B6bY=
|
| 7 |
+
cloud.google.com/go/auth/oauth2adapt v0.2.4/go.mod h1:jC/jOpwFP6JBxhB3P5Rr0a9HLMC/Pe3eaL4NmdvqPtc=
|
| 8 |
+
cloud.google.com/go/compute/metadata v0.5.0 h1:Zr0eK8JbFv6+Wi4ilXAR8FJ3wyNdpxHKJNPos6LTZOY=
|
| 9 |
+
cloud.google.com/go/compute/metadata v0.5.0/go.mod h1:aHnloV2TPI38yx4s9+wAZhHykWvVCfu7hQbF+9CWoiY=
|
| 10 |
+
cloud.google.com/go/firestore v1.16.0 h1:YwmDHcyrxVRErWcgxunzEaZxtNbc8QoFYA/JOEwDPgc=
|
| 11 |
+
cloud.google.com/go/firestore v1.16.0/go.mod h1:+22v/7p+WNBSQwdSwP57vz47aZiY+HrDkrOsJNhk7rg=
|
| 12 |
+
cloud.google.com/go/iam v1.2.0 h1:kZKMKVNk/IsSSc/udOb83K0hL/Yh/Gcqpz+oAkoIFN8=
|
| 13 |
+
cloud.google.com/go/iam v1.2.0/go.mod h1:zITGuWgsLZxd8OwAlX+eMFgZDXzBm7icj1PVTYG766Q=
|
| 14 |
+
cloud.google.com/go/longrunning v0.6.0 h1:mM1ZmaNsQsnb+5n1DNPeL0KwQd9jQRqSqSDEkBZr+aI=
|
| 15 |
+
cloud.google.com/go/longrunning v0.6.0/go.mod h1:uHzSZqW89h7/pasCWNYdUpwGz3PcVWhrWupreVPYLts=
|
| 16 |
+
cloud.google.com/go/storage v1.43.0 h1:CcxnSohZwizt4LCzQHWvBf1/kvtHUn7gk9QERXPyXFs=
|
| 17 |
+
cloud.google.com/go/storage v1.43.0/go.mod h1:ajvxEa7WmZS1PxvKRq4bq0tFT3vMd502JwstCcYv0Q0=
|
| 18 |
+
firebase.google.com/go/v4 v4.12.0 h1:I6dCkcWUMFNkFdWgzlf8SLWecQnKdFgJhMv5fT9l1qI=
|
| 19 |
+
firebase.google.com/go/v4 v4.12.0/go.mod h1:60c36dWLK4+j05Vw5XMllek3b3PCynU3BfI46OSwsUE=
|
| 20 |
+
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
| 21 |
+
github.com/MicahParks/keyfunc v1.9.0 h1:lhKd5xrFHLNOWrDc4Tyb/Q1AJ4LCzQ48GVJyVIID3+o=
|
| 22 |
+
github.com/MicahParks/keyfunc v1.9.0/go.mod h1:IdnCilugA0O/99dW+/MkvlyrsX8+L8+x95xuVNtM5jw=
|
| 23 |
+
github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs=
|
| 24 |
+
github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
| 25 |
+
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
| 26 |
+
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
| 27 |
+
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
| 28 |
+
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
| 29 |
+
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
| 30 |
+
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
| 31 |
+
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
| 32 |
+
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
| 33 |
+
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
| 34 |
+
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
| 35 |
+
github.com/fasthttp/websocket v1.5.4 h1:Bq8HIcoiffh3pmwSKB8FqaNooluStLQQxnzQspMatgI=
|
| 36 |
+
github.com/fasthttp/websocket v1.5.4/go.mod h1:R2VXd4A6KBspb5mTrsWnZwn6ULkX56/Ktk8/0UNSJao=
|
| 37 |
+
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
| 38 |
+
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
| 39 |
+
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
| 40 |
+
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
| 41 |
+
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
| 42 |
+
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
| 43 |
+
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
| 44 |
+
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
|
| 45 |
+
github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI=
|
| 46 |
+
github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
|
| 47 |
+
github.com/gofiber/fiber/v2 v2.52.0 h1:S+qXi7y+/Pgvqq4DrSmREGiFwtB7Bu6+QFLuIHYw/UE=
|
| 48 |
+
github.com/gofiber/fiber/v2 v2.52.0/go.mod h1:KEOE+cXMhXG0zHc9d8+E38hoX+ZN7bhOtgeF2oT6jrQ=
|
| 49 |
+
github.com/gofiber/websocket/v2 v2.2.1 h1:C9cjxvloojayOp9AovmpQrk8VqvVnT8Oao3+IUygH7w=
|
| 50 |
+
github.com/gofiber/websocket/v2 v2.2.1/go.mod h1:Ao/+nyNnX5u/hIFPuHl28a+NIkrqK7PRimyKaj4JxVU=
|
| 51 |
+
github.com/golang-jwt/jwt/v4 v4.4.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
| 52 |
+
github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
|
| 53 |
+
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
| 54 |
+
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
| 55 |
+
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
| 56 |
+
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
|
| 57 |
+
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
| 58 |
+
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
| 59 |
+
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
| 60 |
+
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
| 61 |
+
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
| 62 |
+
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
| 63 |
+
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
| 64 |
+
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
| 65 |
+
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
| 66 |
+
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
| 67 |
+
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
| 68 |
+
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
| 69 |
+
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
| 70 |
+
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
| 71 |
+
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
| 72 |
+
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
| 73 |
+
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
| 74 |
+
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
| 75 |
+
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
| 76 |
+
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
| 77 |
+
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
| 78 |
+
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
| 79 |
+
github.com/google/martian/v3 v3.3.3 h1:DIhPTQrbPkgs2yJYdXU/eNACCG5DVQjySNRNlflZ9Fc=
|
| 80 |
+
github.com/google/martian/v3 v3.3.3/go.mod h1:iEPrYcgCF7jA9OtScMFQyAlZZ4YXTKEtJ1E6RWzmBA0=
|
| 81 |
+
github.com/google/s2a-go v0.1.8 h1:zZDs9gcbt9ZPLV0ndSyQk6Kacx2g/X+SKYovpnz3SMM=
|
| 82 |
+
github.com/google/s2a-go v0.1.8/go.mod h1:6iNWHTpQ+nfNRN5E00MSdfDwVesa8hhS32PhPO8deJA=
|
| 83 |
+
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
| 84 |
+
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
| 85 |
+
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
| 86 |
+
github.com/googleapis/enterprise-certificate-proxy v0.3.4 h1:XYIDZApgAnrN1c855gTgghdIA6Stxb52D5RnLI1SLyw=
|
| 87 |
+
github.com/googleapis/enterprise-certificate-proxy v0.3.4/go.mod h1:YKe7cfqYXjKGpGvmSg28/fFvhNzinZQm8DGnaburhGA=
|
| 88 |
+
github.com/googleapis/gax-go/v2 v2.13.0 h1:yitjD5f7jQHhyDsnhKEBU52NdvvdSeGzlAnDPT0hH1s=
|
| 89 |
+
github.com/googleapis/gax-go/v2 v2.13.0/go.mod h1:Z/fvTZXF8/uw7Xu5GuslPw+bplx6SS338j1Is2S+B7A=
|
| 90 |
+
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
| 91 |
+
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
| 92 |
+
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
| 93 |
+
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
| 94 |
+
github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg=
|
| 95 |
+
github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
| 96 |
+
github.com/klauspost/compress v1.17.0 h1:Rnbp4K9EjcDuVuHtd0dgA4qNuv9yKDYKK1ulpJwgrqM=
|
| 97 |
+
github.com/klauspost/compress v1.17.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
|
| 98 |
+
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
| 99 |
+
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
| 100 |
+
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
| 101 |
+
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
| 102 |
+
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
| 103 |
+
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
|
| 104 |
+
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
| 105 |
+
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
| 106 |
+
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
| 107 |
+
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
| 108 |
+
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
| 109 |
+
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
| 110 |
+
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee h1:8Iv5m6xEo1NR1AvpV+7XmhI4r39LGNzwUL4YpMuL5vk=
|
| 111 |
+
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee/go.mod h1:qwtSXrKuJh/zsFQ12yEE89xfCrGKK63Rr7ctU/uCo4g=
|
| 112 |
+
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
| 113 |
+
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
| 114 |
+
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
| 115 |
+
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
| 116 |
+
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
| 117 |
+
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
| 118 |
+
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
| 119 |
+
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
| 120 |
+
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
| 121 |
+
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
| 122 |
+
github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA=
|
| 123 |
+
github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g=
|
| 124 |
+
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
|
| 125 |
+
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
|
| 126 |
+
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
|
| 127 |
+
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
|
| 128 |
+
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0 h1:r6I7RJCN86bpD/FQwedZ0vSixDpwuWREjW9oRMsmqDc=
|
| 129 |
+
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0/go.mod h1:B9yO6b04uB80CzjedvewuqDhxJxi11s7/GtiGa8bAjI=
|
| 130 |
+
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 h1:TT4fX+nBOA/+LUkobKGW1ydGcn+G3vRw9+g5HwCphpk=
|
| 131 |
+
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0/go.mod h1:L7UH0GbB0p47T4Rri3uHjbpCFYrVrwc1I25QhNPiGK8=
|
| 132 |
+
go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw=
|
| 133 |
+
go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8=
|
| 134 |
+
go.opentelemetry.io/otel/metric v1.29.0 h1:vPf/HFWTNkPu1aYeIsc98l4ktOQaL6LeSoeV2g+8YLc=
|
| 135 |
+
go.opentelemetry.io/otel/metric v1.29.0/go.mod h1:auu/QWieFVWx+DmQOUMgj0F8LHWdgalxXqvp7BII/W8=
|
| 136 |
+
go.opentelemetry.io/otel/sdk v1.29.0 h1:vkqKjk7gwhS8VaWb0POZKmIEDimRCMsopNYnriHyryo=
|
| 137 |
+
go.opentelemetry.io/otel/sdk v1.29.0/go.mod h1:pM8Dx5WKnvxLCb+8lG1PRNIDxu9g9b9g59Qr7hfAAok=
|
| 138 |
+
go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4=
|
| 139 |
+
go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ=
|
| 140 |
+
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
| 141 |
+
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
| 142 |
+
golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A=
|
| 143 |
+
golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70=
|
| 144 |
+
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
| 145 |
+
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
| 146 |
+
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
| 147 |
+
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
| 148 |
+
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
| 149 |
+
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
| 150 |
+
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
| 151 |
+
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
| 152 |
+
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
| 153 |
+
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
| 154 |
+
golang.org/x/net v0.0.0-20220708220712-1185a9018129/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
| 155 |
+
golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo=
|
| 156 |
+
golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0=
|
| 157 |
+
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
| 158 |
+
golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs=
|
| 159 |
+
golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
| 160 |
+
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
| 161 |
+
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
| 162 |
+
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
| 163 |
+
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
|
| 164 |
+
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
| 165 |
+
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
| 166 |
+
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
| 167 |
+
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
| 168 |
+
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
| 169 |
+
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
| 170 |
+
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
| 171 |
+
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
| 172 |
+
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
| 173 |
+
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
|
| 174 |
+
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
| 175 |
+
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
| 176 |
+
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
| 177 |
+
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
| 178 |
+
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
| 179 |
+
golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
|
| 180 |
+
golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
| 181 |
+
golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U=
|
| 182 |
+
golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
| 183 |
+
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
| 184 |
+
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
| 185 |
+
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
| 186 |
+
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
| 187 |
+
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
| 188 |
+
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
| 189 |
+
google.golang.org/api v0.197.0 h1:x6CwqQLsFiA5JKAiGyGBjc2bNtHtLddhJCE2IKuhhcQ=
|
| 190 |
+
google.golang.org/api v0.197.0/go.mod h1:AuOuo20GoQ331nq7DquGHlU6d+2wN2fZ8O0ta60nRNw=
|
| 191 |
+
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
| 192 |
+
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
| 193 |
+
google.golang.org/appengine/v2 v2.0.2 h1:MSqyWy2shDLwG7chbwBJ5uMyw6SNqJzhJHNDwYB0Akk=
|
| 194 |
+
google.golang.org/appengine/v2 v2.0.2/go.mod h1:PkgRUWz4o1XOvbqtWTkBtCitEJ5Tp4HoVEdMMYQR/8E=
|
| 195 |
+
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
| 196 |
+
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
| 197 |
+
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
| 198 |
+
google.golang.org/genproto v0.0.0-20240903143218-8af14fe29dc1 h1:BulPr26Jqjnd4eYDVe+YvyR7Yc2vJGkO5/0UxD0/jZU=
|
| 199 |
+
google.golang.org/genproto v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:hL97c3SYopEHblzpxRL4lSs523++l8DYxGM1FQiYmb4=
|
| 200 |
+
google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1 h1:hjSy6tcFQZ171igDaN5QHOw2n6vx40juYbC/x67CEhc=
|
| 201 |
+
google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:qpvKtACPCQhAdu3PyQgV4l3LMXZEtft7y8QcarRsp9I=
|
| 202 |
+
google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 h1:pPJltXNxVzT4pK9yD8vR9X75DaWYYmLGMsEvBfFQZzQ=
|
| 203 |
+
google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU=
|
| 204 |
+
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
| 205 |
+
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
| 206 |
+
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
| 207 |
+
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
| 208 |
+
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
|
| 209 |
+
google.golang.org/grpc v1.66.2 h1:3QdXkuq3Bkh7w+ywLdLvM56cmGvQHUMZpiCzt6Rqaoo=
|
| 210 |
+
google.golang.org/grpc v1.66.2/go.mod h1:s3/l6xSSCURdVfAnL+TqCNMyTDAGN6+lZeVxnZR128Y=
|
| 211 |
+
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
| 212 |
+
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
| 213 |
+
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
| 214 |
+
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
| 215 |
+
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
| 216 |
+
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
| 217 |
+
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
| 218 |
+
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
| 219 |
+
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
| 220 |
+
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
|
| 221 |
+
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
|
| 222 |
+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
| 223 |
+
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
| 224 |
+
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
| 225 |
+
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
| 226 |
+
gorm.io/driver/mysql v1.5.2 h1:QC2HRskSE75wBuOxe0+iCkyJZ+RqpudsQtqkp+IMuXs=
|
| 227 |
+
gorm.io/driver/mysql v1.5.2/go.mod h1:pQLhh1Ut/WUAySdTHwBpBv6+JKcj+ua4ZFx1QQTBzb8=
|
| 228 |
+
gorm.io/gorm v1.25.2-0.20230530020048-26663ab9bf55/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
|
| 229 |
+
gorm.io/gorm v1.25.5 h1:zR9lOiiYf09VNh5Q1gphfyia1JpiClIWG9hQaxB/mls=
|
| 230 |
+
gorm.io/gorm v1.25.5/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
|
| 231 |
+
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
| 232 |
+
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
handlers/achievements.go
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package handlers
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"moodlink-backend/database"
|
| 5 |
+
"moodlink-backend/models"
|
| 6 |
+
"moodlink-backend/utils"
|
| 7 |
+
|
| 8 |
+
"github.com/gofiber/fiber/v2"
|
| 9 |
+
"github.com/google/uuid"
|
| 10 |
+
)
|
| 11 |
+
|
| 12 |
+
func GetUserAchievements(c *fiber.Ctx) error {
|
| 13 |
+
userIdStr := c.Params("user_id")
|
| 14 |
+
userId, err := uuid.Parse(userIdStr)
|
| 15 |
+
if err != nil {
|
| 16 |
+
return utils.ErrorResponse(c, fiber.StatusBadRequest, "Invalid user ID")
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
// Get user's earned achievements
|
| 20 |
+
var userAchievements []models.UserAchievement
|
| 21 |
+
database.DB.Where("user_id = ?", userId).Order("earned_at desc").Find(&userAchievements)
|
| 22 |
+
|
| 23 |
+
// Create response with achievement details
|
| 24 |
+
var response []map[string]interface{}
|
| 25 |
+
earnedMap := make(map[string]models.UserAchievement)
|
| 26 |
+
|
| 27 |
+
for _, ua := range userAchievements {
|
| 28 |
+
earnedMap[ua.BadgeName] = ua
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
for _, achievement := range models.AvailableAchievements {
|
| 32 |
+
achievementData := map[string]interface{}{
|
| 33 |
+
"name": achievement.Name,
|
| 34 |
+
"display_name": achievement.DisplayName,
|
| 35 |
+
"description": achievement.Description,
|
| 36 |
+
"icon": achievement.Icon,
|
| 37 |
+
"earned": false,
|
| 38 |
+
"earned_at": nil,
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
if earned, exists := earnedMap[achievement.Name]; exists {
|
| 42 |
+
achievementData["earned"] = true
|
| 43 |
+
achievementData["earned_at"] = earned.EarnedAt
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
response = append(response, achievementData)
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
return utils.SuccessResponse(c, map[string]interface{}{
|
| 50 |
+
"achievements": response,
|
| 51 |
+
"total_earned": len(userAchievements),
|
| 52 |
+
"total_available": len(models.AvailableAchievements),
|
| 53 |
+
})
|
| 54 |
+
}
|
handlers/ai_reflection.go
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package handlers
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"moodlink-backend/database"
|
| 5 |
+
"moodlink-backend/models"
|
| 6 |
+
"moodlink-backend/services"
|
| 7 |
+
"moodlink-backend/utils"
|
| 8 |
+
"time"
|
| 9 |
+
|
| 10 |
+
"github.com/gofiber/fiber/v2"
|
| 11 |
+
"github.com/google/uuid"
|
| 12 |
+
)
|
| 13 |
+
|
| 14 |
+
func AIReflection(c *fiber.Ctx) error {
|
| 15 |
+
userIdStr := c.Get("User-ID")
|
| 16 |
+
if userIdStr == "" {
|
| 17 |
+
return utils.ErrorResponse(c, fiber.StatusBadRequest, "User-ID header is required")
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
userId, err := uuid.Parse(userIdStr)
|
| 21 |
+
if err != nil {
|
| 22 |
+
return utils.ErrorResponse(c, fiber.StatusBadRequest, "Invalid UUID format")
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
var req models.AIReflectionRequest
|
| 26 |
+
if err := c.BodyParser(&req); err != nil {
|
| 27 |
+
return utils.ErrorResponse(c, fiber.StatusBadRequest, "Invalid request body")
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
// Check if user exists
|
| 31 |
+
var user models.AnonymousUser
|
| 32 |
+
if err := database.DB.Where("id = ?", userId).First(&user).Error; err != nil {
|
| 33 |
+
return utils.ErrorResponse(c, fiber.StatusNotFound, "User not found")
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
// Call Gemini API
|
| 37 |
+
answer, err := services.CallGeminiAPI(req.Prompt)
|
| 38 |
+
if err != nil {
|
| 39 |
+
return utils.ErrorResponse(c, fiber.StatusInternalServerError, "Failed to get AI reflection: "+err.Error())
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
// Save reflection to database
|
| 43 |
+
reflection := models.AIReflection{
|
| 44 |
+
UserID: userId,
|
| 45 |
+
Question: req.Prompt,
|
| 46 |
+
Answer: answer,
|
| 47 |
+
CreatedAt: time.Now(),
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
if err := database.DB.Create(&reflection).Error; err != nil {
|
| 51 |
+
// Log error but don't fail the request
|
| 52 |
+
// The user still gets their AI response even if saving fails
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
// Award points for using AI reflection (+5 points)
|
| 56 |
+
user.Points += 5
|
| 57 |
+
user.UpdateLevel()
|
| 58 |
+
database.DB.Save(&user)
|
| 59 |
+
|
| 60 |
+
// Assign achievements
|
| 61 |
+
go services.AssignAchievements(userId)
|
| 62 |
+
|
| 63 |
+
response := models.AIReflectionResponse{
|
| 64 |
+
Question: req.Prompt,
|
| 65 |
+
Answer: answer,
|
| 66 |
+
CreatedAt: reflection.CreatedAt,
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
return utils.SuccessResponse(c, response)
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
func GetReflectionHistory(c *fiber.Ctx) error {
|
| 73 |
+
userIdStr := c.Get("User-ID")
|
| 74 |
+
if userIdStr == "" {
|
| 75 |
+
return utils.ErrorResponse(c, fiber.StatusBadRequest, "User-ID header is required")
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
userId, err := uuid.Parse(userIdStr)
|
| 79 |
+
if err != nil {
|
| 80 |
+
return utils.ErrorResponse(c, fiber.StatusBadRequest, "Invalid UUID format")
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
// Get reflection history with pagination
|
| 84 |
+
page := c.QueryInt("page", 1)
|
| 85 |
+
limit := c.QueryInt("limit", 10)
|
| 86 |
+
|
| 87 |
+
if page < 1 {
|
| 88 |
+
page = 1
|
| 89 |
+
}
|
| 90 |
+
if limit < 1 || limit > 50 {
|
| 91 |
+
limit = 10
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
offset := (page - 1) * limit
|
| 95 |
+
|
| 96 |
+
var reflections []models.AIReflection
|
| 97 |
+
var total int64
|
| 98 |
+
|
| 99 |
+
// Get total count
|
| 100 |
+
database.DB.Model(&models.AIReflection{}).Where("user_id = ?", userId).Count(&total)
|
| 101 |
+
|
| 102 |
+
// Get paginated reflections
|
| 103 |
+
if err := database.DB.Where("user_id = ?", userId).
|
| 104 |
+
Order("created_at desc").
|
| 105 |
+
Limit(limit).
|
| 106 |
+
Offset(offset).
|
| 107 |
+
Find(&reflections).Error; err != nil {
|
| 108 |
+
return utils.ErrorResponse(c, fiber.StatusInternalServerError, "Failed to fetch reflection history")
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
// Convert to response format
|
| 112 |
+
var responses []models.AIReflectionResponse
|
| 113 |
+
for _, reflection := range reflections {
|
| 114 |
+
responses = append(responses, models.AIReflectionResponse{
|
| 115 |
+
Question: reflection.Question,
|
| 116 |
+
Answer: reflection.Answer,
|
| 117 |
+
CreatedAt: reflection.CreatedAt,
|
| 118 |
+
})
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
return utils.SuccessResponse(c, map[string]interface{}{
|
| 122 |
+
"reflections": responses,
|
| 123 |
+
"page": page,
|
| 124 |
+
"limit": limit,
|
| 125 |
+
"total": total,
|
| 126 |
+
})
|
| 127 |
+
}
|
handlers/auth.go
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package handlers
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"moodlink-backend/database"
|
| 5 |
+
"moodlink-backend/models"
|
| 6 |
+
"moodlink-backend/utils"
|
| 7 |
+
"time"
|
| 8 |
+
|
| 9 |
+
"github.com/gofiber/fiber/v2"
|
| 10 |
+
"github.com/google/uuid"
|
| 11 |
+
)
|
| 12 |
+
|
| 13 |
+
func RegisterOrGetUser(c *fiber.Ctx) error {
|
| 14 |
+
userIdStr := c.Get("User-ID")
|
| 15 |
+
if userIdStr == "" {
|
| 16 |
+
return utils.ErrorResponse(c, fiber.StatusBadRequest, "User-ID header is required")
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
userId, err := uuid.Parse(userIdStr)
|
| 20 |
+
if err != nil {
|
| 21 |
+
return utils.ErrorResponse(c, fiber.StatusBadRequest, "Invalid UUID format")
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
var user models.AnonymousUser
|
| 25 |
+
result := database.DB.Where("id = ?", userId).First(&user)
|
| 26 |
+
|
| 27 |
+
if result.Error != nil {
|
| 28 |
+
// User doesn't exist, create new one
|
| 29 |
+
user = models.AnonymousUser{
|
| 30 |
+
ID: userId,
|
| 31 |
+
CreatedAt: time.Now(),
|
| 32 |
+
Points: 0,
|
| 33 |
+
Level: 1,
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
if err := database.DB.Create(&user).Error; err != nil {
|
| 37 |
+
return utils.ErrorResponse(c, fiber.StatusInternalServerError, "Failed to create user")
|
| 38 |
+
}
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
return utils.SuccessResponse(c, user)
|
| 42 |
+
}
|
handlers/chat.go
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package handlers
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"moodlink-backend/database"
|
| 5 |
+
"moodlink-backend/models"
|
| 6 |
+
"moodlink-backend/services"
|
| 7 |
+
"moodlink-backend/utils"
|
| 8 |
+
"time"
|
| 9 |
+
|
| 10 |
+
"github.com/gofiber/fiber/v2"
|
| 11 |
+
"github.com/google/uuid"
|
| 12 |
+
)
|
| 13 |
+
|
| 14 |
+
func StartChat(c *fiber.Ctx) error {
|
| 15 |
+
userIdStr := c.Get("User-ID")
|
| 16 |
+
if userIdStr == "" {
|
| 17 |
+
return utils.ErrorResponse(c, fiber.StatusBadRequest, "User-ID header is required")
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
userId, err := uuid.Parse(userIdStr)
|
| 21 |
+
if err != nil {
|
| 22 |
+
return utils.ErrorResponse(c, fiber.StatusBadRequest, "Invalid UUID format")
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
// Check if user exists
|
| 26 |
+
var user models.AnonymousUser
|
| 27 |
+
if err := database.DB.Where("id = ?", userId).First(&user).Error; err != nil {
|
| 28 |
+
return utils.ErrorResponse(c, fiber.StatusNotFound, "User not found")
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
// Try to find or create chat room
|
| 32 |
+
room, isNewRoom, err := services.FindOrCreateChatRoom(userId)
|
| 33 |
+
if err != nil {
|
| 34 |
+
return utils.ErrorResponse(c, fiber.StatusInternalServerError, "Failed to create chat room")
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
if room == nil {
|
| 38 |
+
// User added to waiting queue
|
| 39 |
+
return utils.SuccessResponse(c, map[string]interface{}{
|
| 40 |
+
"status": "waiting",
|
| 41 |
+
"message": "Looking for someone to chat with... Please wait.",
|
| 42 |
+
})
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
var message string
|
| 46 |
+
if isNewRoom {
|
| 47 |
+
message = "Chat room created! You're now connected with someone."
|
| 48 |
+
} else {
|
| 49 |
+
message = "You're already in an active chat room."
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
response := models.ChatStartResponse{
|
| 53 |
+
RoomID: room.ID,
|
| 54 |
+
Message: message,
|
| 55 |
+
CreatedAt: room.CreatedAt,
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
return utils.SuccessResponse(c, response)
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
func SendMessage(c *fiber.Ctx) error {
|
| 62 |
+
userIdStr := c.Get("User-ID")
|
| 63 |
+
if userIdStr == "" {
|
| 64 |
+
return utils.ErrorResponse(c, fiber.StatusBadRequest, "User-ID header is required")
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
userId, err := uuid.Parse(userIdStr)
|
| 68 |
+
if err != nil {
|
| 69 |
+
return utils.ErrorResponse(c, fiber.StatusBadRequest, "Invalid UUID format")
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
roomIdStr := c.Params("room_id")
|
| 73 |
+
roomId, err := uuid.Parse(roomIdStr)
|
| 74 |
+
if err != nil {
|
| 75 |
+
return utils.ErrorResponse(c, fiber.StatusBadRequest, "Invalid room ID")
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
// Check if user is in this room
|
| 79 |
+
if !services.IsUserInRoom(userId, roomId) {
|
| 80 |
+
return utils.ErrorResponse(c, fiber.StatusForbidden, "You are not a member of this chat room")
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
var req models.SendMessageRequest
|
| 84 |
+
if err := c.BodyParser(&req); err != nil {
|
| 85 |
+
return utils.ErrorResponse(c, fiber.StatusBadRequest, "Invalid request body")
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
// Create message
|
| 89 |
+
message := models.ChatMessage{
|
| 90 |
+
RoomID: roomId,
|
| 91 |
+
SenderID: userId,
|
| 92 |
+
Message: req.Message,
|
| 93 |
+
CreatedAt: time.Now(),
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
if err := database.DB.Create(&message).Error; err != nil {
|
| 97 |
+
return utils.ErrorResponse(c, fiber.StatusInternalServerError, "Failed to send message")
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
// Update room's last activity
|
| 101 |
+
database.DB.Model(&models.ChatRoom{}).Where("id = ?", roomId).Update("updated_at", time.Now())
|
| 102 |
+
|
| 103 |
+
// Award points for active chatting (+1 point)
|
| 104 |
+
var user models.AnonymousUser
|
| 105 |
+
if err := database.DB.Where("id = ?", userId).First(&user).Error; err == nil {
|
| 106 |
+
user.Points += 1
|
| 107 |
+
user.UpdateLevel()
|
| 108 |
+
database.DB.Save(&user)
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
response := models.MessageResponse{
|
| 112 |
+
ID: message.ID,
|
| 113 |
+
RoomID: message.RoomID,
|
| 114 |
+
SenderID: message.SenderID,
|
| 115 |
+
Message: message.Message,
|
| 116 |
+
CreatedAt: message.CreatedAt,
|
| 117 |
+
IsOwn: true,
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
return utils.SuccessResponse(c, response)
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
func GetMessages(c *fiber.Ctx) error {
|
| 124 |
+
userIdStr := c.Get("User-ID")
|
| 125 |
+
if userIdStr == "" {
|
| 126 |
+
return utils.ErrorResponse(c, fiber.StatusBadRequest, "User-ID header is required")
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
userId, err := uuid.Parse(userIdStr)
|
| 130 |
+
if err != nil {
|
| 131 |
+
return utils.ErrorResponse(c, fiber.StatusBadRequest, "Invalid UUID format")
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
roomIdStr := c.Params("room_id")
|
| 135 |
+
roomId, err := uuid.Parse(roomIdStr)
|
| 136 |
+
if err != nil {
|
| 137 |
+
return utils.ErrorResponse(c, fiber.StatusBadRequest, "Invalid room ID")
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
// Check if user is in this room
|
| 141 |
+
if !services.IsUserInRoom(userId, roomId) {
|
| 142 |
+
return utils.ErrorResponse(c, fiber.StatusForbidden, "You are not a member of this chat room")
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
// Get messages with pagination
|
| 146 |
+
page := c.QueryInt("page", 1)
|
| 147 |
+
limit := c.QueryInt("limit", 50)
|
| 148 |
+
|
| 149 |
+
if page < 1 {
|
| 150 |
+
page = 1
|
| 151 |
+
}
|
| 152 |
+
if limit < 1 || limit > 100 {
|
| 153 |
+
limit = 50
|
| 154 |
+
}
|
| 155 |
+
|
| 156 |
+
offset := (page - 1) * limit
|
| 157 |
+
|
| 158 |
+
var messages []models.ChatMessage
|
| 159 |
+
var total int64
|
| 160 |
+
|
| 161 |
+
// Get total count
|
| 162 |
+
database.DB.Model(&models.ChatMessage{}).Where("room_id = ?", roomId).Count(&total)
|
| 163 |
+
|
| 164 |
+
// Get paginated messages
|
| 165 |
+
if err := database.DB.Where("room_id = ?", roomId).
|
| 166 |
+
Order("created_at asc").
|
| 167 |
+
Limit(limit).
|
| 168 |
+
Offset(offset).
|
| 169 |
+
Find(&messages).Error; err != nil {
|
| 170 |
+
return utils.ErrorResponse(c, fiber.StatusInternalServerError, "Failed to fetch messages")
|
| 171 |
+
}
|
| 172 |
+
|
| 173 |
+
// Convert to response format
|
| 174 |
+
var responses []models.MessageResponse
|
| 175 |
+
for _, message := range messages {
|
| 176 |
+
responses = append(responses, models.MessageResponse{
|
| 177 |
+
ID: message.ID,
|
| 178 |
+
RoomID: message.RoomID,
|
| 179 |
+
SenderID: message.SenderID,
|
| 180 |
+
Message: message.Message,
|
| 181 |
+
CreatedAt: message.CreatedAt,
|
| 182 |
+
IsOwn: message.SenderID == userId,
|
| 183 |
+
})
|
| 184 |
+
}
|
| 185 |
+
|
| 186 |
+
return utils.SuccessResponse(c, map[string]interface{}{
|
| 187 |
+
"messages": responses,
|
| 188 |
+
"page": page,
|
| 189 |
+
"limit": limit,
|
| 190 |
+
"total": total,
|
| 191 |
+
})
|
| 192 |
+
}
|
| 193 |
+
|
| 194 |
+
func EndChat(c *fiber.Ctx) error {
|
| 195 |
+
userIdStr := c.Get("User-ID")
|
| 196 |
+
if userIdStr == "" {
|
| 197 |
+
return utils.ErrorResponse(c, fiber.StatusBadRequest, "User-ID header is required")
|
| 198 |
+
}
|
| 199 |
+
|
| 200 |
+
userId, err := uuid.Parse(userIdStr)
|
| 201 |
+
if err != nil {
|
| 202 |
+
return utils.ErrorResponse(c, fiber.StatusBadRequest, "Invalid UUID format")
|
| 203 |
+
}
|
| 204 |
+
|
| 205 |
+
roomIdStr := c.Params("room_id")
|
| 206 |
+
roomId, err := uuid.Parse(roomIdStr)
|
| 207 |
+
if err != nil {
|
| 208 |
+
return utils.ErrorResponse(c, fiber.StatusBadRequest, "Invalid room ID")
|
| 209 |
+
}
|
| 210 |
+
|
| 211 |
+
// Check if user is in this room
|
| 212 |
+
if !services.IsUserInRoom(userId, roomId) {
|
| 213 |
+
return utils.ErrorResponse(c, fiber.StatusForbidden, "You are not a member of this chat room")
|
| 214 |
+
}
|
| 215 |
+
|
| 216 |
+
// End the chat room
|
| 217 |
+
if err := services.EndChatRoom(roomId); err != nil {
|
| 218 |
+
return utils.ErrorResponse(c, fiber.StatusInternalServerError, "Failed to end chat room")
|
| 219 |
+
}
|
| 220 |
+
|
| 221 |
+
return utils.MessageResponse(c, "Chat room ended successfully")
|
| 222 |
+
}
|
| 223 |
+
|
| 224 |
+
func GetActiveChats(c *fiber.Ctx) error {
|
| 225 |
+
userIdStr := c.Get("User-ID")
|
| 226 |
+
if userIdStr == "" {
|
| 227 |
+
return utils.ErrorResponse(c, fiber.StatusBadRequest, "User-ID header is required")
|
| 228 |
+
}
|
| 229 |
+
|
| 230 |
+
userId, err := uuid.Parse(userIdStr)
|
| 231 |
+
if err != nil {
|
| 232 |
+
return utils.ErrorResponse(c, fiber.StatusBadRequest, "Invalid UUID format")
|
| 233 |
+
}
|
| 234 |
+
|
| 235 |
+
rooms, err := services.GetActiveRoomsForUser(userId)
|
| 236 |
+
if err != nil {
|
| 237 |
+
return utils.ErrorResponse(c, fiber.StatusInternalServerError, "Failed to fetch active chats")
|
| 238 |
+
}
|
| 239 |
+
|
| 240 |
+
return utils.SuccessResponse(c, map[string]interface{}{
|
| 241 |
+
"active_chats": rooms,
|
| 242 |
+
"count": len(rooms),
|
| 243 |
+
})
|
| 244 |
+
}
|
handlers/comments.go
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package handlers
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"moodlink-backend/database"
|
| 5 |
+
"moodlink-backend/models"
|
| 6 |
+
"moodlink-backend/services"
|
| 7 |
+
"moodlink-backend/utils"
|
| 8 |
+
"time"
|
| 9 |
+
|
| 10 |
+
"github.com/gofiber/fiber/v2"
|
| 11 |
+
"github.com/google/uuid"
|
| 12 |
+
)
|
| 13 |
+
|
| 14 |
+
func AddComment(c *fiber.Ctx) error {
|
| 15 |
+
userIdStr := c.Get("User-ID")
|
| 16 |
+
if userIdStr == "" {
|
| 17 |
+
return utils.ErrorResponse(c, fiber.StatusBadRequest, "User-ID header is required")
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
userId, err := uuid.Parse(userIdStr)
|
| 21 |
+
if err != nil {
|
| 22 |
+
return utils.ErrorResponse(c, fiber.StatusBadRequest, "Invalid UUID format")
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
var req models.CommentRequest
|
| 26 |
+
if err := c.BodyParser(&req); err != nil {
|
| 27 |
+
return utils.ErrorResponse(c, fiber.StatusBadRequest, "Invalid request body")
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
journalId, err := uuid.Parse(req.JournalID)
|
| 31 |
+
if err != nil {
|
| 32 |
+
return utils.ErrorResponse(c, fiber.StatusBadRequest, "Invalid journal ID")
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
// Check if journal entry exists
|
| 36 |
+
var entry models.MoodEntry
|
| 37 |
+
if err := database.DB.Where("id = ?", journalId).First(&entry).Error; err != nil {
|
| 38 |
+
return utils.ErrorResponse(c, fiber.StatusNotFound, "Journal entry not found")
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
// Check for inappropriate content
|
| 42 |
+
isFlagged := services.CheckContent(req.Comment)
|
| 43 |
+
|
| 44 |
+
// Create comment
|
| 45 |
+
comment := models.JournalComment{
|
| 46 |
+
JournalID: journalId,
|
| 47 |
+
UserID: userId,
|
| 48 |
+
Comment: req.Comment,
|
| 49 |
+
IsFlagged: isFlagged,
|
| 50 |
+
CreatedAt: time.Now(),
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
if err := database.DB.Create(&comment).Error; err != nil {
|
| 54 |
+
return utils.ErrorResponse(c, fiber.StatusInternalServerError, "Failed to create comment")
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
// Award points to commenter (+3 points)
|
| 58 |
+
var user models.AnonymousUser
|
| 59 |
+
if err := database.DB.Where("id = ?", userId).First(&user).Error; err == nil {
|
| 60 |
+
user.Points += 3
|
| 61 |
+
user.UpdateLevel()
|
| 62 |
+
database.DB.Save(&user)
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
// Award points to journal author (+2 points)
|
| 66 |
+
var journalAuthor models.AnonymousUser
|
| 67 |
+
if err := database.DB.Where("id = ?", entry.UserID).First(&journalAuthor).Error; err == nil {
|
| 68 |
+
journalAuthor.Points += 2
|
| 69 |
+
journalAuthor.UpdateLevel()
|
| 70 |
+
database.DB.Save(&journalAuthor)
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
response := models.CommentResponse{
|
| 74 |
+
ID: comment.ID,
|
| 75 |
+
JournalID: comment.JournalID,
|
| 76 |
+
Comment: comment.Comment,
|
| 77 |
+
CreatedAt: comment.CreatedAt,
|
| 78 |
+
IsFlagged: comment.IsFlagged,
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
return utils.SuccessResponse(c, response)
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
func GetComments(c *fiber.Ctx) error {
|
| 85 |
+
journalIdStr := c.Params("journal_id")
|
| 86 |
+
journalId, err := uuid.Parse(journalIdStr)
|
| 87 |
+
if err != nil {
|
| 88 |
+
return utils.ErrorResponse(c, fiber.StatusBadRequest, "Invalid journal ID")
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
// Check if journal entry exists
|
| 92 |
+
var entry models.MoodEntry
|
| 93 |
+
if err := database.DB.Where("id = ?", journalId).First(&entry).Error; err != nil {
|
| 94 |
+
return utils.ErrorResponse(c, fiber.StatusNotFound, "Journal entry not found")
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
// Get comments with pagination
|
| 98 |
+
page := c.QueryInt("page", 1)
|
| 99 |
+
limit := c.QueryInt("limit", 20)
|
| 100 |
+
|
| 101 |
+
if page < 1 {
|
| 102 |
+
page = 1
|
| 103 |
+
}
|
| 104 |
+
if limit < 1 || limit > 50 {
|
| 105 |
+
limit = 20
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
offset := (page - 1) * limit
|
| 109 |
+
|
| 110 |
+
var comments []models.JournalComment
|
| 111 |
+
var total int64
|
| 112 |
+
|
| 113 |
+
// Get total count
|
| 114 |
+
database.DB.Model(&models.JournalComment{}).Where("journal_id = ?", journalId).Count(&total)
|
| 115 |
+
|
| 116 |
+
// Get paginated comments (exclude flagged ones from public view)
|
| 117 |
+
if err := database.DB.Where("journal_id = ? AND is_flagged = ?", journalId, false).
|
| 118 |
+
Order("created_at asc").
|
| 119 |
+
Limit(limit).
|
| 120 |
+
Offset(offset).
|
| 121 |
+
Find(&comments).Error; err != nil {
|
| 122 |
+
return utils.ErrorResponse(c, fiber.StatusInternalServerError, "Failed to fetch comments")
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
// Convert to response format
|
| 126 |
+
var responses []models.CommentResponse
|
| 127 |
+
for _, comment := range comments {
|
| 128 |
+
responses = append(responses, models.CommentResponse{
|
| 129 |
+
ID: comment.ID,
|
| 130 |
+
JournalID: comment.JournalID,
|
| 131 |
+
Comment: comment.Comment,
|
| 132 |
+
CreatedAt: comment.CreatedAt,
|
| 133 |
+
IsFlagged: comment.IsFlagged,
|
| 134 |
+
})
|
| 135 |
+
}
|
| 136 |
+
|
| 137 |
+
return utils.SuccessResponse(c, map[string]interface{}{
|
| 138 |
+
"comments": responses,
|
| 139 |
+
"page": page,
|
| 140 |
+
"limit": limit,
|
| 141 |
+
"total": total,
|
| 142 |
+
})
|
| 143 |
+
}
|
handlers/dashboard.go
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package handlers
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"moodlink-backend/database"
|
| 5 |
+
"moodlink-backend/models"
|
| 6 |
+
"moodlink-backend/utils"
|
| 7 |
+
|
| 8 |
+
"github.com/gofiber/fiber/v2"
|
| 9 |
+
"github.com/google/uuid"
|
| 10 |
+
)
|
| 11 |
+
|
| 12 |
+
func GetDashboard(c *fiber.Ctx) error {
|
| 13 |
+
userIdStr := c.Params("user_id")
|
| 14 |
+
userId, err := uuid.Parse(userIdStr)
|
| 15 |
+
if err != nil {
|
| 16 |
+
return utils.ErrorResponse(c, fiber.StatusBadRequest, "Invalid user ID")
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
var user models.AnonymousUser
|
| 20 |
+
if err := database.DB.Where("id = ?", userId).First(&user).Error; err != nil {
|
| 21 |
+
return utils.ErrorResponse(c, fiber.StatusNotFound, "User not found")
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
// Get latest mood entry
|
| 25 |
+
var latestMood models.MoodEntry
|
| 26 |
+
database.DB.Where("user_id = ?", userId).Order("created_at desc").First(&latestMood)
|
| 27 |
+
|
| 28 |
+
// Get streak count
|
| 29 |
+
streakCount := user.GetStreakCount(database.DB)
|
| 30 |
+
|
| 31 |
+
dashboardData := map[string]interface{}{
|
| 32 |
+
"user_id": user.ID,
|
| 33 |
+
"level": user.Level,
|
| 34 |
+
"total_points": user.Points,
|
| 35 |
+
"streak_count": streakCount,
|
| 36 |
+
"latest_mood": latestMood.Mood,
|
| 37 |
+
"last_checkin": user.LastCheckin,
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
return utils.SuccessResponse(c, dashboardData)
|
| 41 |
+
}
|
handlers/device_token.go
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package handlers
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"moodlink-backend/database"
|
| 5 |
+
"moodlink-backend/models"
|
| 6 |
+
"moodlink-backend/utils"
|
| 7 |
+
|
| 8 |
+
"github.com/gofiber/fiber/v2"
|
| 9 |
+
"github.com/google/uuid"
|
| 10 |
+
)
|
| 11 |
+
|
| 12 |
+
func RegisterDeviceToken(c *fiber.Ctx) error {
|
| 13 |
+
userIdStr := c.Get("User-ID")
|
| 14 |
+
if userIdStr == "" {
|
| 15 |
+
return utils.ErrorResponse(c, fiber.StatusBadRequest, "User-ID header is required")
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
userId, err := uuid.Parse(userIdStr)
|
| 19 |
+
if err != nil {
|
| 20 |
+
return utils.ErrorResponse(c, fiber.StatusBadRequest, "Invalid UUID format")
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
var req models.DeviceTokenRequest
|
| 24 |
+
if err := c.BodyParser(&req); err != nil {
|
| 25 |
+
return utils.ErrorResponse(c, fiber.StatusBadRequest, "Invalid request body")
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
// Check if user exists
|
| 29 |
+
var user models.AnonymousUser
|
| 30 |
+
if err := database.DB.Where("id = ?", userId).First(&user).Error; err != nil {
|
| 31 |
+
return utils.ErrorResponse(c, fiber.StatusNotFound, "User not found")
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
// Check if token already exists for this user
|
| 35 |
+
var existingToken models.DeviceToken
|
| 36 |
+
result := database.DB.Where("user_id = ? AND token = ?", userId, req.Token).First(&existingToken)
|
| 37 |
+
|
| 38 |
+
if result.Error == nil {
|
| 39 |
+
// Token already exists, update timestamp
|
| 40 |
+
database.DB.Save(&existingToken)
|
| 41 |
+
return utils.MessageResponse(c, "Device token updated")
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
// Create new device token
|
| 45 |
+
deviceToken := models.DeviceToken{
|
| 46 |
+
UserID: userId,
|
| 47 |
+
Token: req.Token,
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
if err := database.DB.Create(&deviceToken).Error; err != nil {
|
| 51 |
+
return utils.ErrorResponse(c, fiber.StatusInternalServerError, "Failed to register device token")
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
return utils.MessageResponse(c, "Device token registered successfully")
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
func UnregisterDeviceToken(c *fiber.Ctx) error {
|
| 58 |
+
userIdStr := c.Get("User-ID")
|
| 59 |
+
if userIdStr == "" {
|
| 60 |
+
return utils.ErrorResponse(c, fiber.StatusBadRequest, "User-ID header is required")
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
userId, err := uuid.Parse(userIdStr)
|
| 64 |
+
if err != nil {
|
| 65 |
+
return utils.ErrorResponse(c, fiber.StatusBadRequest, "Invalid UUID format")
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
var req models.DeviceTokenRequest
|
| 69 |
+
if err := c.BodyParser(&req); err != nil {
|
| 70 |
+
return utils.ErrorResponse(c, fiber.StatusBadRequest, "Invalid request body")
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
// Delete the token
|
| 74 |
+
result := database.DB.Where("user_id = ? AND token = ?", userId, req.Token).Delete(&models.DeviceToken{})
|
| 75 |
+
|
| 76 |
+
if result.RowsAffected == 0 {
|
| 77 |
+
return utils.ErrorResponse(c, fiber.StatusNotFound, "Device token not found")
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
return utils.MessageResponse(c, "Device token unregistered successfully")
|
| 81 |
+
}
|
handlers/feed.go
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package handlers
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"moodlink-backend/database"
|
| 5 |
+
"moodlink-backend/models"
|
| 6 |
+
"moodlink-backend/utils"
|
| 7 |
+
"fmt"
|
| 8 |
+
"strconv"
|
| 9 |
+
"time"
|
| 10 |
+
|
| 11 |
+
"github.com/gofiber/fiber/v2"
|
| 12 |
+
)
|
| 13 |
+
|
| 14 |
+
// Haversine formula to calculate distance between two points
|
| 15 |
+
func haversineDistance(lat1, lng1, lat2, lng2 float64) float64 {
|
| 16 |
+
const earthRadius = 6371 // Earth radius in kilometers
|
| 17 |
+
|
| 18 |
+
lat1Rad := lat1 * 3.14159265359 / 180
|
| 19 |
+
lng1Rad := lng1 * 3.14159265359 / 180
|
| 20 |
+
lat2Rad := lat2 * 3.14159265359 / 180
|
| 21 |
+
lng2Rad := lng2 * 3.14159265359 / 180
|
| 22 |
+
|
| 23 |
+
dlat := lat2Rad - lat1Rad
|
| 24 |
+
dlng := lng2Rad - lng1Rad
|
| 25 |
+
|
| 26 |
+
a := 0.5 - 0.5 * (dlat * dlat + lat1Rad * lat2Rad * dlng * dlng)
|
| 27 |
+
return earthRadius * 2 * (a * a)
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
func GetFeed(c *fiber.Ctx) error {
|
| 31 |
+
page := c.QueryInt("page", 1)
|
| 32 |
+
limit := c.QueryInt("limit", 20)
|
| 33 |
+
|
| 34 |
+
// Location filtering parameters
|
| 35 |
+
latStr := c.Query("lat")
|
| 36 |
+
lngStr := c.Query("lng")
|
| 37 |
+
radiusStr := c.Query("radius_km")
|
| 38 |
+
|
| 39 |
+
if page < 1 {
|
| 40 |
+
page = 1
|
| 41 |
+
}
|
| 42 |
+
if limit < 1 || limit > 100 {
|
| 43 |
+
limit = 20
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
offset := (page - 1) * limit
|
| 47 |
+
|
| 48 |
+
var entries []models.MoodEntry
|
| 49 |
+
|
| 50 |
+
// Get entries from last 24 hours
|
| 51 |
+
yesterday := time.Now().AddDate(0, 0, -1)
|
| 52 |
+
|
| 53 |
+
query := database.DB.Where("created_at >= ?", yesterday)
|
| 54 |
+
|
| 55 |
+
// Apply location filtering if parameters provided
|
| 56 |
+
if latStr != "" && lngStr != "" && radiusStr != "" {
|
| 57 |
+
lat, err1 := strconv.ParseFloat(latStr, 64)
|
| 58 |
+
lng, err2 := strconv.ParseFloat(lngStr, 64)
|
| 59 |
+
radius, err3 := strconv.ParseFloat(radiusStr, 64)
|
| 60 |
+
|
| 61 |
+
if err1 != nil || err2 != nil || err3 != nil {
|
| 62 |
+
return utils.ErrorResponse(c, fiber.StatusBadRequest, "Invalid location parameters")
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
// Use Haversine formula in raw SQL for distance filtering
|
| 66 |
+
haversineSQL := fmt.Sprintf(`
|
| 67 |
+
(6371 * acos(
|
| 68 |
+
cos(radians(%f)) *
|
| 69 |
+
cos(radians(location_lat)) *
|
| 70 |
+
cos(radians(location_lng) - radians(%f)) +
|
| 71 |
+
sin(radians(%f)) *
|
| 72 |
+
sin(radians(location_lat))
|
| 73 |
+
)) <= %f
|
| 74 |
+
`, lat, lng, lat, radius)
|
| 75 |
+
|
| 76 |
+
query = query.Where(haversineSQL)
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
query = query.Order("created_at desc").
|
| 80 |
+
Limit(limit).
|
| 81 |
+
Offset(offset)
|
| 82 |
+
|
| 83 |
+
if err := query.Find(&entries).Error; err != nil {
|
| 84 |
+
return utils.ErrorResponse(c, fiber.StatusInternalServerError, "Failed to fetch feed")
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
// Get support counts for each entry
|
| 88 |
+
var feedResponse []models.FeedResponse
|
| 89 |
+
for _, entry := range entries {
|
| 90 |
+
var supportCount int64
|
| 91 |
+
database.DB.Model(&models.JournalSupport{}).Where("journal_id = ?", entry.ID).Count(&supportCount)
|
| 92 |
+
|
| 93 |
+
feedResponse = append(feedResponse, models.FeedResponse{
|
| 94 |
+
ID: entry.ID,
|
| 95 |
+
Mood: entry.Mood,
|
| 96 |
+
Note: entry.Note,
|
| 97 |
+
CreatedAt: entry.CreatedAt,
|
| 98 |
+
SupportCount: int(supportCount),
|
| 99 |
+
})
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
return utils.SuccessResponse(c, map[string]interface{}{
|
| 103 |
+
"entries": feedResponse,
|
| 104 |
+
"page": page,
|
| 105 |
+
"limit": limit,
|
| 106 |
+
"total": len(feedResponse),
|
| 107 |
+
})
|
| 108 |
+
}
|
handlers/google_auth.go
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package handlers
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"moodlink-backend/database"
|
| 5 |
+
"moodlink-backend/models"
|
| 6 |
+
"moodlink-backend/services"
|
| 7 |
+
"moodlink-backend/utils"
|
| 8 |
+
|
| 9 |
+
"github.com/gofiber/fiber/v2"
|
| 10 |
+
"github.com/google/uuid"
|
| 11 |
+
)
|
| 12 |
+
|
| 13 |
+
func GoogleAuth(c *fiber.Ctx) error {
|
| 14 |
+
userIdStr := c.Get("User-ID")
|
| 15 |
+
if userIdStr == "" {
|
| 16 |
+
return utils.ErrorResponse(c, fiber.StatusBadRequest, "User-ID header is required")
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
userId, err := uuid.Parse(userIdStr)
|
| 20 |
+
if err != nil {
|
| 21 |
+
return utils.ErrorResponse(c, fiber.StatusBadRequest, "Invalid UUID format")
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
var req models.GoogleAuthRequest
|
| 25 |
+
if err := c.BodyParser(&req); err != nil {
|
| 26 |
+
return utils.ErrorResponse(c, fiber.StatusBadRequest, "Invalid request body")
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
// Verify Google token
|
| 30 |
+
tokenInfo, err := services.VerifyGoogleToken(req.IDToken)
|
| 31 |
+
if err != nil {
|
| 32 |
+
return utils.ErrorResponse(c, fiber.StatusUnauthorized, "Invalid Google token")
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
// Check if Google ID is already linked to another user
|
| 36 |
+
var existingUser models.AnonymousUser
|
| 37 |
+
result := database.DB.Where("google_id = ?", tokenInfo.Sub).First(&existingUser)
|
| 38 |
+
if result.Error == nil && existingUser.ID != userId {
|
| 39 |
+
return utils.ErrorResponse(c, fiber.StatusConflict, "Google account already linked to another user")
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
// Get current user
|
| 43 |
+
var user models.AnonymousUser
|
| 44 |
+
if err := database.DB.Where("id = ?", userId).First(&user).Error; err != nil {
|
| 45 |
+
return utils.ErrorResponse(c, fiber.StatusNotFound, "User not found")
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
// Link Google account
|
| 49 |
+
user.GoogleID = &tokenInfo.Sub
|
| 50 |
+
user.IsSynced = true
|
| 51 |
+
|
| 52 |
+
if err := database.DB.Save(&user).Error; err != nil {
|
| 53 |
+
return utils.ErrorResponse(c, fiber.StatusInternalServerError, "Failed to link Google account")
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
return utils.SuccessResponse(c, map[string]interface{}{
|
| 57 |
+
"message": "Google account linked successfully",
|
| 58 |
+
"user_id": user.ID,
|
| 59 |
+
"is_synced": user.IsSynced,
|
| 60 |
+
"google_id": user.GoogleID,
|
| 61 |
+
})
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
func RestoreFromGoogle(c *fiber.Ctx) error {
|
| 65 |
+
var req models.GoogleAuthRequest
|
| 66 |
+
if err := c.BodyParser(&req); err != nil {
|
| 67 |
+
return utils.ErrorResponse(c, fiber.StatusBadRequest, "Invalid request body")
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
// Verify Google token
|
| 71 |
+
tokenInfo, err := services.VerifyGoogleToken(req.IDToken)
|
| 72 |
+
if err != nil {
|
| 73 |
+
return utils.ErrorResponse(c, fiber.StatusUnauthorized, "Invalid Google token")
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
// Find user by Google ID
|
| 77 |
+
var user models.AnonymousUser
|
| 78 |
+
if err := database.DB.Where("google_id = ?", tokenInfo.Sub).First(&user).Error; err != nil {
|
| 79 |
+
return utils.ErrorResponse(c, fiber.StatusNotFound, "No account found for this Google account")
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
return utils.SuccessResponse(c, map[string]interface{}{
|
| 83 |
+
"user_id": user.ID,
|
| 84 |
+
"is_synced": user.IsSynced,
|
| 85 |
+
"message": "Account restored successfully",
|
| 86 |
+
})
|
| 87 |
+
}
|
handlers/moderation.go
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package handlers
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"moodlink-backend/database"
|
| 5 |
+
"moodlink-backend/models"
|
| 6 |
+
"moodlink-backend/utils"
|
| 7 |
+
|
| 8 |
+
"github.com/gofiber/fiber/v2"
|
| 9 |
+
)
|
| 10 |
+
|
| 11 |
+
func GetFlaggedEntries(c *fiber.Ctx) error {
|
| 12 |
+
page := c.QueryInt("page", 1)
|
| 13 |
+
limit := c.QueryInt("limit", 20)
|
| 14 |
+
|
| 15 |
+
if page < 1 {
|
| 16 |
+
page = 1
|
| 17 |
+
}
|
| 18 |
+
if limit < 1 || limit > 100 {
|
| 19 |
+
limit = 20
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
offset := (page - 1) * limit
|
| 23 |
+
|
| 24 |
+
var entries []models.MoodEntry
|
| 25 |
+
var total int64
|
| 26 |
+
|
| 27 |
+
// Get total count of flagged entries
|
| 28 |
+
database.DB.Model(&models.MoodEntry{}).Where("is_flagged = ?", true).Count(&total)
|
| 29 |
+
|
| 30 |
+
// Get paginated flagged entries
|
| 31 |
+
if err := database.DB.Where("is_flagged = ?", true).
|
| 32 |
+
Order("created_at desc").
|
| 33 |
+
Limit(limit).
|
| 34 |
+
Offset(offset).
|
| 35 |
+
Find(&entries).Error; err != nil {
|
| 36 |
+
return utils.ErrorResponse(c, fiber.StatusInternalServerError, "Failed to fetch flagged entries")
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
return utils.SuccessResponse(c, map[string]interface{}{
|
| 40 |
+
"entries": entries,
|
| 41 |
+
"page": page,
|
| 42 |
+
"limit": limit,
|
| 43 |
+
"total": total,
|
| 44 |
+
})
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
func UnflagEntry(c *fiber.Ctx) error {
|
| 48 |
+
entryId := c.Params("entry_id")
|
| 49 |
+
|
| 50 |
+
var entry models.MoodEntry
|
| 51 |
+
if err := database.DB.Where("id = ?", entryId).First(&entry).Error; err != nil {
|
| 52 |
+
return utils.ErrorResponse(c, fiber.StatusNotFound, "Entry not found")
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
entry.IsFlagged = false
|
| 56 |
+
if err := database.DB.Save(&entry).Error; err != nil {
|
| 57 |
+
return utils.ErrorResponse(c, fiber.StatusInternalServerError, "Failed to unflag entry")
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
return utils.MessageResponse(c, "Entry unflagged successfully")
|
| 61 |
+
}
|
handlers/mood.go
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package handlers
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"moodlink-backend/database"
|
| 5 |
+
"moodlink-backend/models"
|
| 6 |
+
"moodlink-backend/services"
|
| 7 |
+
"moodlink-backend/utils"
|
| 8 |
+
"time"
|
| 9 |
+
|
| 10 |
+
"github.com/gofiber/fiber/v2"
|
| 11 |
+
"github.com/google/uuid"
|
| 12 |
+
)
|
| 13 |
+
|
| 14 |
+
func CheckIn(c *fiber.Ctx) error {
|
| 15 |
+
var req models.MoodEntryRequest
|
| 16 |
+
if err := c.BodyParser(&req); err != nil {
|
| 17 |
+
return utils.ErrorResponse(c, fiber.StatusBadRequest, "Invalid request body")
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
userId, err := uuid.Parse(req.UserID)
|
| 21 |
+
if err != nil {
|
| 22 |
+
return utils.ErrorResponse(c, fiber.StatusBadRequest, "Invalid user ID")
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
// Check if user exists
|
| 26 |
+
var user models.AnonymousUser
|
| 27 |
+
if err := database.DB.Where("id = ?", userId).First(&user).Error; err != nil {
|
| 28 |
+
return utils.ErrorResponse(c, fiber.StatusNotFound, "User not found")
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
// Check if user has already checked in today
|
| 32 |
+
today := time.Now().Format("2006-01-02")
|
| 33 |
+
var existingEntry models.MoodEntry
|
| 34 |
+
result := database.DB.Where("user_id = ? AND DATE(created_at) = ?", userId, today).First(&existingEntry)
|
| 35 |
+
|
| 36 |
+
if result.Error == nil {
|
| 37 |
+
return utils.ErrorResponse(c, fiber.StatusBadRequest, "You have already checked in today")
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
// Create new mood entry
|
| 41 |
+
entry := models.MoodEntry{
|
| 42 |
+
UserID: userId,
|
| 43 |
+
Mood: req.Mood,
|
| 44 |
+
Note: req.Note,
|
| 45 |
+
IsFlagged: services.CheckContent(req.Note),
|
| 46 |
+
LocationLat: req.LocationLat,
|
| 47 |
+
LocationLng: req.LocationLng,
|
| 48 |
+
CreatedAt: time.Now(),
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
if err := database.DB.Create(&entry).Error; err != nil {
|
| 52 |
+
return utils.ErrorResponse(c, fiber.StatusInternalServerError, "Failed to create mood entry")
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
// Update user points and level
|
| 56 |
+
points := 10 // Base points for check-in
|
| 57 |
+
|
| 58 |
+
// Check for streak bonus
|
| 59 |
+
if user.LastCheckin != nil {
|
| 60 |
+
yesterday := time.Now().AddDate(0, 0, -1).Format("2006-01-02")
|
| 61 |
+
if user.LastCheckin.Format("2006-01-02") == yesterday {
|
| 62 |
+
points += 20 // Streak bonus
|
| 63 |
+
}
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
user.Points += points
|
| 67 |
+
user.UpdateLevel()
|
| 68 |
+
todayDate := time.Now()
|
| 69 |
+
user.LastCheckin = &todayDate
|
| 70 |
+
|
| 71 |
+
if err := database.DB.Save(&user).Error; err != nil {
|
| 72 |
+
return utils.ErrorResponse(c, fiber.StatusInternalServerError, "Failed to update user")
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
// Broadcast to WebSocket clients
|
| 76 |
+
BroadcastNewEntry(entry)
|
| 77 |
+
|
| 78 |
+
// Assign achievements
|
| 79 |
+
go services.AssignAchievements(userId)
|
| 80 |
+
|
| 81 |
+
return utils.SuccessResponse(c, map[string]interface{}{
|
| 82 |
+
"entry": entry,
|
| 83 |
+
"points_earned": points,
|
| 84 |
+
"total_points": user.Points,
|
| 85 |
+
"level": user.Level,
|
| 86 |
+
})
|
| 87 |
+
}
|
handlers/reports.go
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package handlers
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"moodlink-backend/database"
|
| 5 |
+
"moodlink-backend/models"
|
| 6 |
+
"moodlink-backend/utils"
|
| 7 |
+
"time"
|
| 8 |
+
|
| 9 |
+
"github.com/gofiber/fiber/v2"
|
| 10 |
+
"github.com/google/uuid"
|
| 11 |
+
)
|
| 12 |
+
|
| 13 |
+
func ReportJournal(c *fiber.Ctx) error {
|
| 14 |
+
var req models.ReportRequest
|
| 15 |
+
if err := c.BodyParser(&req); err != nil {
|
| 16 |
+
return utils.ErrorResponse(c, fiber.StatusBadRequest, "Invalid request body")
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
journalId, err := uuid.Parse(req.JournalID)
|
| 20 |
+
if err != nil {
|
| 21 |
+
return utils.ErrorResponse(c, fiber.StatusBadRequest, "Invalid journal ID")
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
// Check if journal entry exists
|
| 25 |
+
var entry models.MoodEntry
|
| 26 |
+
if err := database.DB.Where("id = ?", journalId).First(&entry).Error; err != nil {
|
| 27 |
+
return utils.ErrorResponse(c, fiber.StatusNotFound, "Journal entry not found")
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
// Create report entry
|
| 31 |
+
report := models.JournalReport{
|
| 32 |
+
JournalID: journalId,
|
| 33 |
+
Reason: req.Reason,
|
| 34 |
+
CreatedAt: time.Now(),
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
if err := database.DB.Create(&report).Error; err != nil {
|
| 38 |
+
return utils.ErrorResponse(c, fiber.StatusInternalServerError, "Failed to create report")
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
return utils.SuccessResponse(c, report)
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
func GetReports(c *fiber.Ctx) error {
|
| 45 |
+
// Check if user is admin (simple check via header)
|
| 46 |
+
isAdmin := c.Get("Is-Admin")
|
| 47 |
+
if isAdmin != "true" {
|
| 48 |
+
return utils.ErrorResponse(c, fiber.StatusForbidden, "Admin access required")
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
page := c.QueryInt("page", 1)
|
| 52 |
+
limit := c.QueryInt("limit", 20)
|
| 53 |
+
|
| 54 |
+
if page < 1 {
|
| 55 |
+
page = 1
|
| 56 |
+
}
|
| 57 |
+
if limit < 1 || limit > 100 {
|
| 58 |
+
limit = 20
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
offset := (page - 1) * limit
|
| 62 |
+
|
| 63 |
+
var reports []models.JournalReport
|
| 64 |
+
var total int64
|
| 65 |
+
|
| 66 |
+
// Get total count
|
| 67 |
+
database.DB.Model(&models.JournalReport{}).Count(&total)
|
| 68 |
+
|
| 69 |
+
// Get paginated reports
|
| 70 |
+
if err := database.DB.Order("created_at desc").
|
| 71 |
+
Limit(limit).
|
| 72 |
+
Offset(offset).
|
| 73 |
+
Find(&reports).Error; err != nil {
|
| 74 |
+
return utils.ErrorResponse(c, fiber.StatusInternalServerError, "Failed to fetch reports")
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
return utils.SuccessResponse(c, map[string]interface{}{
|
| 78 |
+
"reports": reports,
|
| 79 |
+
"page": page,
|
| 80 |
+
"limit": limit,
|
| 81 |
+
"total": total,
|
| 82 |
+
})
|
| 83 |
+
}
|
handlers/stats.go
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package handlers
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"moodlink-backend/database"
|
| 5 |
+
"moodlink-backend/models"
|
| 6 |
+
"moodlink-backend/utils"
|
| 7 |
+
"time"
|
| 8 |
+
|
| 9 |
+
"github.com/gofiber/fiber/v2"
|
| 10 |
+
)
|
| 11 |
+
|
| 12 |
+
func GetGlobalStats(c *fiber.Ctx) error {
|
| 13 |
+
var stats map[string]interface{} = make(map[string]interface{})
|
| 14 |
+
|
| 15 |
+
// Total users
|
| 16 |
+
var totalUsers int64
|
| 17 |
+
database.DB.Model(&models.AnonymousUser{}).Count(&totalUsers)
|
| 18 |
+
stats["total_users"] = totalUsers
|
| 19 |
+
|
| 20 |
+
// Total mood check-ins
|
| 21 |
+
var totalCheckins int64
|
| 22 |
+
database.DB.Model(&models.MoodEntry{}).Count(&totalCheckins)
|
| 23 |
+
stats["total_checkins"] = totalCheckins
|
| 24 |
+
|
| 25 |
+
// Mood distribution in last 24 hours
|
| 26 |
+
yesterday := time.Now().AddDate(0, 0, -1)
|
| 27 |
+
var moodDistribution []struct {
|
| 28 |
+
Mood string `json:"mood"`
|
| 29 |
+
Count int64 `json:"count"`
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
database.DB.Model(&models.MoodEntry{}).
|
| 33 |
+
Select("mood, COUNT(*) as count").
|
| 34 |
+
Where("created_at >= ?", yesterday).
|
| 35 |
+
Group("mood").
|
| 36 |
+
Find(&moodDistribution)
|
| 37 |
+
|
| 38 |
+
stats["mood_distribution_24h"] = moodDistribution
|
| 39 |
+
|
| 40 |
+
// Total support sent (grouped by type)
|
| 41 |
+
var supportDistribution []struct {
|
| 42 |
+
Type string `json:"type"`
|
| 43 |
+
Count int64 `json:"count"`
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
database.DB.Model(&models.JournalSupport{}).
|
| 47 |
+
Select("type, COUNT(*) as count").
|
| 48 |
+
Group("type").
|
| 49 |
+
Find(&supportDistribution)
|
| 50 |
+
|
| 51 |
+
stats["support_distribution"] = supportDistribution
|
| 52 |
+
|
| 53 |
+
return utils.SuccessResponse(c, stats)
|
| 54 |
+
}
|
handlers/support.go
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package handlers
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"moodlink-backend/database"
|
| 5 |
+
"moodlink-backend/models"
|
| 6 |
+
"moodlink-backend/services"
|
| 7 |
+
"moodlink-backend/utils"
|
| 8 |
+
"time"
|
| 9 |
+
|
| 10 |
+
"github.com/gofiber/fiber/v2"
|
| 11 |
+
"github.com/google/uuid"
|
| 12 |
+
)
|
| 13 |
+
|
| 14 |
+
func SendSupport(c *fiber.Ctx) error {
|
| 15 |
+
var req models.SupportRequest
|
| 16 |
+
if err := c.BodyParser(&req); err != nil {
|
| 17 |
+
return utils.ErrorResponse(c, fiber.StatusBadRequest, "Invalid request body")
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
journalId, err := uuid.Parse(req.JournalID)
|
| 21 |
+
if err != nil {
|
| 22 |
+
return utils.ErrorResponse(c, fiber.StatusBadRequest, "Invalid journal ID")
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
// Check if journal entry exists
|
| 26 |
+
var entry models.MoodEntry
|
| 27 |
+
if err := database.DB.Where("id = ?", journalId).First(&entry).Error; err != nil {
|
| 28 |
+
return utils.ErrorResponse(c, fiber.StatusNotFound, "Journal entry not found")
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
// Create support entry
|
| 32 |
+
support := models.JournalSupport{
|
| 33 |
+
JournalID: journalId,
|
| 34 |
+
Type: req.Type,
|
| 35 |
+
CreatedAt: time.Now(),
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
if err := database.DB.Create(&support).Error; err != nil {
|
| 39 |
+
return utils.ErrorResponse(c, fiber.StatusInternalServerError, "Failed to create support")
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
// Update points for journal author (+5 points)
|
| 43 |
+
var journalAuthor models.AnonymousUser
|
| 44 |
+
if err := database.DB.Where("id = ?", entry.UserID).First(&journalAuthor).Error; err == nil {
|
| 45 |
+
journalAuthor.Points += 5
|
| 46 |
+
journalAuthor.UpdateLevel()
|
| 47 |
+
database.DB.Save(&journalAuthor)
|
| 48 |
+
|
| 49 |
+
// Send WebSocket notification to journal author
|
| 50 |
+
BroadcastSupportNotification(entry.UserID, journalId, req.Type)
|
| 51 |
+
|
| 52 |
+
// Send push notification
|
| 53 |
+
go services.SendSupportNotification(entry.UserID, req.Type)
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
// If sender is identified, give them +2 points
|
| 57 |
+
senderIdStr := c.Get("User-ID")
|
| 58 |
+
if senderIdStr != "" {
|
| 59 |
+
if senderId, err := uuid.Parse(senderIdStr); err == nil {
|
| 60 |
+
var sender models.AnonymousUser
|
| 61 |
+
if err := database.DB.Where("id = ?", senderId).First(&sender).Error; err == nil {
|
| 62 |
+
sender.Points += 2
|
| 63 |
+
sender.UpdateLevel()
|
| 64 |
+
database.DB.Save(&sender)
|
| 65 |
+
|
| 66 |
+
// Assign achievements for sender
|
| 67 |
+
go services.AssignAchievements(senderId)
|
| 68 |
+
}
|
| 69 |
+
}
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
return utils.SuccessResponse(c, support)
|
| 73 |
+
}
|
handlers/user_reset.go
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package handlers
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"moodlink-backend/database"
|
| 5 |
+
"moodlink-backend/models"
|
| 6 |
+
"moodlink-backend/utils"
|
| 7 |
+
|
| 8 |
+
"github.com/gofiber/fiber/v2"
|
| 9 |
+
"github.com/google/uuid"
|
| 10 |
+
)
|
| 11 |
+
|
| 12 |
+
func ResetUserAccount(c *fiber.Ctx) error {
|
| 13 |
+
userIdStr := c.Get("User-ID")
|
| 14 |
+
if userIdStr == "" {
|
| 15 |
+
return utils.ErrorResponse(c, fiber.StatusBadRequest, "User-ID header is required")
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
userId, err := uuid.Parse(userIdStr)
|
| 19 |
+
if err != nil {
|
| 20 |
+
return utils.ErrorResponse(c, fiber.StatusBadRequest, "Invalid UUID format")
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
// Check if user exists
|
| 24 |
+
var user models.AnonymousUser
|
| 25 |
+
if err := database.DB.Where("id = ?", userId).First(&user).Error; err != nil {
|
| 26 |
+
return utils.ErrorResponse(c, fiber.StatusNotFound, "User not found")
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
// Start transaction
|
| 30 |
+
tx := database.DB.Begin()
|
| 31 |
+
|
| 32 |
+
// Delete all related data
|
| 33 |
+
if err := tx.Where("user_id = ?", userId).Delete(&models.MoodEntry{}).Error; err != nil {
|
| 34 |
+
tx.Rollback()
|
| 35 |
+
return utils.ErrorResponse(c, fiber.StatusInternalServerError, "Failed to delete mood entries")
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
if err := tx.Where("user_id = ?", userId).Delete(&models.UserAchievement{}).Error; err != nil {
|
| 39 |
+
tx.Rollback()
|
| 40 |
+
return utils.ErrorResponse(c, fiber.StatusInternalServerError, "Failed to delete achievements")
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
if err := tx.Where("user_id = ?", userId).Delete(&models.DeviceToken{}).Error; err != nil {
|
| 44 |
+
tx.Rollback()
|
| 45 |
+
return utils.ErrorResponse(c, fiber.StatusInternalServerError, "Failed to delete device tokens")
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
// Delete journal supports where user was the recipient
|
| 49 |
+
if err := tx.Where("journal_id IN (SELECT id FROM mood_entries WHERE user_id = ?)", userId).Delete(&models.JournalSupport{}).Error; err != nil {
|
| 50 |
+
tx.Rollback()
|
| 51 |
+
return utils.ErrorResponse(c, fiber.StatusInternalServerError, "Failed to delete journal supports")
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
// Delete journal reports
|
| 55 |
+
if err := tx.Where("journal_id IN (SELECT id FROM mood_entries WHERE user_id = ?)", userId).Delete(&models.JournalReport{}).Error; err != nil {
|
| 56 |
+
tx.Rollback()
|
| 57 |
+
return utils.ErrorResponse(c, fiber.StatusInternalServerError, "Failed to delete journal reports")
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
// Delete the user
|
| 61 |
+
if err := tx.Delete(&user).Error; err != nil {
|
| 62 |
+
tx.Rollback()
|
| 63 |
+
return utils.ErrorResponse(c, fiber.StatusInternalServerError, "Failed to delete user")
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
// Commit transaction
|
| 67 |
+
tx.Commit()
|
| 68 |
+
|
| 69 |
+
// Generate new UUID for the user
|
| 70 |
+
newUserId := uuid.New()
|
| 71 |
+
|
| 72 |
+
return utils.SuccessResponse(c, map[string]interface{}{
|
| 73 |
+
"message": "Account reset successfully. All data has been permanently deleted.",
|
| 74 |
+
"new_user_id": newUserId,
|
| 75 |
+
"warning": "This action is irreversible. Please save your new User-ID.",
|
| 76 |
+
})
|
| 77 |
+
}
|
handlers/user_stats.go
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package handlers
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"moodlink-backend/database"
|
| 5 |
+
"moodlink-backend/models"
|
| 6 |
+
"moodlink-backend/utils"
|
| 7 |
+
"time"
|
| 8 |
+
|
| 9 |
+
"github.com/gofiber/fiber/v2"
|
| 10 |
+
"github.com/google/uuid"
|
| 11 |
+
)
|
| 12 |
+
|
| 13 |
+
func GetUserStats(c *fiber.Ctx) error {
|
| 14 |
+
userIdStr := c.Get("User-ID")
|
| 15 |
+
if userIdStr == "" {
|
| 16 |
+
return utils.ErrorResponse(c, fiber.StatusBadRequest, "User-ID header is required")
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
userId, err := uuid.Parse(userIdStr)
|
| 20 |
+
if err != nil {
|
| 21 |
+
return utils.ErrorResponse(c, fiber.StatusBadRequest, "Invalid UUID format")
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
// Get user
|
| 25 |
+
var user models.AnonymousUser
|
| 26 |
+
if err := database.DB.Where("id = ?", userId).First(&user).Error; err != nil {
|
| 27 |
+
return utils.ErrorResponse(c, fiber.StatusNotFound, "User not found")
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
// Get total check-ins
|
| 31 |
+
var totalCheckins int64
|
| 32 |
+
database.DB.Model(&models.MoodEntry{}).Where("user_id = ?", userId).Count(&totalCheckins)
|
| 33 |
+
|
| 34 |
+
// Get most frequent mood
|
| 35 |
+
var moodFrequency []struct {
|
| 36 |
+
Mood string `json:"mood"`
|
| 37 |
+
Count int64 `json:"count"`
|
| 38 |
+
}
|
| 39 |
+
database.DB.Model(&models.MoodEntry{}).
|
| 40 |
+
Select("mood, COUNT(*) as count").
|
| 41 |
+
Where("user_id = ?", userId).
|
| 42 |
+
Group("mood").
|
| 43 |
+
Order("count desc").
|
| 44 |
+
Find(&moodFrequency)
|
| 45 |
+
|
| 46 |
+
mostFrequentMood := ""
|
| 47 |
+
if len(moodFrequency) > 0 {
|
| 48 |
+
mostFrequentMood = moodFrequency[0].Mood
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
// Get mood distribution in last 7 days
|
| 52 |
+
sevenDaysAgo := time.Now().AddDate(0, 0, -7)
|
| 53 |
+
var moodDistribution7Days []struct {
|
| 54 |
+
Mood string `json:"mood"`
|
| 55 |
+
Count int64 `json:"count"`
|
| 56 |
+
}
|
| 57 |
+
database.DB.Model(&models.MoodEntry{}).
|
| 58 |
+
Select("mood, COUNT(*) as count").
|
| 59 |
+
Where("user_id = ? AND created_at >= ?", userId, sevenDaysAgo).
|
| 60 |
+
Group("mood").
|
| 61 |
+
Find(&moodDistribution7Days)
|
| 62 |
+
|
| 63 |
+
// Get support sent (estimated from points)
|
| 64 |
+
var checkinCount int64
|
| 65 |
+
database.DB.Model(&models.MoodEntry{}).Where("user_id = ?", userId).Count(&checkinCount)
|
| 66 |
+
estimatedCheckinPoints := checkinCount * 10
|
| 67 |
+
estimatedSupportSent := (user.Points - int(estimatedCheckinPoints)) / 2
|
| 68 |
+
if estimatedSupportSent < 0 {
|
| 69 |
+
estimatedSupportSent = 0
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
// Get support received
|
| 73 |
+
var supportReceived int64
|
| 74 |
+
database.DB.Model(&models.JournalSupport{}).
|
| 75 |
+
Joins("JOIN mood_entries ON journal_supports.journal_id = mood_entries.id").
|
| 76 |
+
Where("mood_entries.user_id = ?", userId).
|
| 77 |
+
Count(&supportReceived)
|
| 78 |
+
|
| 79 |
+
// Get streak count
|
| 80 |
+
streakCount := user.GetStreakCount(database.DB)
|
| 81 |
+
|
| 82 |
+
// Get mood streak graph data (last 30 days)
|
| 83 |
+
thirtyDaysAgo := time.Now().AddDate(0, 0, -30)
|
| 84 |
+
var moodHistory []struct {
|
| 85 |
+
Date string `json:"date"`
|
| 86 |
+
Mood string `json:"mood"`
|
| 87 |
+
}
|
| 88 |
+
database.DB.Model(&models.MoodEntry{}).
|
| 89 |
+
Select("DATE(created_at) as date, mood").
|
| 90 |
+
Where("user_id = ? AND created_at >= ?", userId, thirtyDaysAgo).
|
| 91 |
+
Order("created_at desc").
|
| 92 |
+
Find(&moodHistory)
|
| 93 |
+
|
| 94 |
+
// Get achievements count
|
| 95 |
+
var achievementsCount int64
|
| 96 |
+
database.DB.Model(&models.UserAchievement{}).Where("user_id = ?", userId).Count(&achievementsCount)
|
| 97 |
+
|
| 98 |
+
stats := map[string]interface{}{
|
| 99 |
+
"user_id": userId,
|
| 100 |
+
"level": user.Level,
|
| 101 |
+
"total_points": user.Points,
|
| 102 |
+
"total_checkins": totalCheckins,
|
| 103 |
+
"most_frequent_mood": mostFrequentMood,
|
| 104 |
+
"mood_distribution_7d": moodDistribution7Days,
|
| 105 |
+
"support_sent": estimatedSupportSent,
|
| 106 |
+
"support_received": supportReceived,
|
| 107 |
+
"streak_count": streakCount,
|
| 108 |
+
"achievements_earned": achievementsCount,
|
| 109 |
+
"mood_history_30d": moodHistory,
|
| 110 |
+
"is_synced": user.IsSynced,
|
| 111 |
+
"last_checkin": user.LastCheckin,
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
return utils.SuccessResponse(c, stats)
|
| 115 |
+
}
|
handlers/websocket.go
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package handlers
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"encoding/json"
|
| 5 |
+
"log"
|
| 6 |
+
"moodlink-backend/models"
|
| 7 |
+
"sync"
|
| 8 |
+
"github.com/google/uuid"
|
| 9 |
+
|
| 10 |
+
"github.com/gofiber/websocket/v2"
|
| 11 |
+
)
|
| 12 |
+
|
| 13 |
+
type Client struct {
|
| 14 |
+
conn *websocket.Conn
|
| 15 |
+
userID uuid.UUID
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
type Hub struct {
|
| 19 |
+
clients map[*Client]bool
|
| 20 |
+
userClients map[uuid.UUID][]*Client
|
| 21 |
+
broadcast chan []byte
|
| 22 |
+
register chan *Client
|
| 23 |
+
unregister chan *Client
|
| 24 |
+
userMessage chan UserMessage
|
| 25 |
+
mutex sync.RWMutex
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
type UserMessage struct {
|
| 29 |
+
UserID uuid.UUID
|
| 30 |
+
Message []byte
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
var hub = &Hub{
|
| 34 |
+
clients: make(map[*Client]bool),
|
| 35 |
+
userClients: make(map[uuid.UUID][]*Client),
|
| 36 |
+
broadcast: make(chan []byte),
|
| 37 |
+
register: make(chan *Client),
|
| 38 |
+
unregister: make(chan *Client),
|
| 39 |
+
userMessage: make(chan UserMessage),
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
func init() {
|
| 43 |
+
go hub.run()
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
func (h *Hub) run() {
|
| 47 |
+
for {
|
| 48 |
+
select {
|
| 49 |
+
case client := <-h.register:
|
| 50 |
+
h.mutex.Lock()
|
| 51 |
+
h.clients[client] = true
|
| 52 |
+
h.userClients[client.userID] = append(h.userClients[client.userID], client)
|
| 53 |
+
h.mutex.Unlock()
|
| 54 |
+
log.Printf("WebSocket client connected for user: %s", client.userID)
|
| 55 |
+
|
| 56 |
+
case client := <-h.unregister:
|
| 57 |
+
h.mutex.Lock()
|
| 58 |
+
if _, ok := h.clients[client]; ok {
|
| 59 |
+
delete(h.clients, client)
|
| 60 |
+
client.conn.Close()
|
| 61 |
+
|
| 62 |
+
// Remove from user clients
|
| 63 |
+
userClients := h.userClients[client.userID]
|
| 64 |
+
for i, c := range userClients {
|
| 65 |
+
if c == client {
|
| 66 |
+
h.userClients[client.userID] = append(userClients[:i], userClients[i+1:]...)
|
| 67 |
+
break
|
| 68 |
+
}
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
// Clean up empty user client list
|
| 72 |
+
if len(h.userClients[client.userID]) == 0 {
|
| 73 |
+
delete(h.userClients, client.userID)
|
| 74 |
+
}
|
| 75 |
+
}
|
| 76 |
+
h.mutex.Unlock()
|
| 77 |
+
log.Printf("WebSocket client disconnected for user: %s", client.userID)
|
| 78 |
+
|
| 79 |
+
case message := <-h.broadcast:
|
| 80 |
+
h.mutex.RLock()
|
| 81 |
+
for client := range h.clients {
|
| 82 |
+
err := client.conn.WriteMessage(websocket.TextMessage, message)
|
| 83 |
+
if err != nil {
|
| 84 |
+
log.Printf("WebSocket write error: %v", err)
|
| 85 |
+
client.conn.Close()
|
| 86 |
+
delete(h.clients, client)
|
| 87 |
+
}
|
| 88 |
+
}
|
| 89 |
+
h.mutex.RUnlock()
|
| 90 |
+
|
| 91 |
+
case userMsg := <-h.userMessage:
|
| 92 |
+
h.mutex.RLock()
|
| 93 |
+
if clients, exists := h.userClients[userMsg.UserID]; exists {
|
| 94 |
+
for _, client := range clients {
|
| 95 |
+
err := client.conn.WriteMessage(websocket.TextMessage, userMsg.Message)
|
| 96 |
+
if err != nil {
|
| 97 |
+
log.Printf("WebSocket write error to user %s: %v", userMsg.UserID, err)
|
| 98 |
+
client.conn.Close()
|
| 99 |
+
delete(h.clients, client)
|
| 100 |
+
}
|
| 101 |
+
}
|
| 102 |
+
}
|
| 103 |
+
h.mutex.RUnlock()
|
| 104 |
+
}
|
| 105 |
+
}
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
func HandleWebSocket(c *websocket.Conn) {
|
| 109 |
+
// Get user ID from query parameter
|
| 110 |
+
userIDStr := c.Query("user_id")
|
| 111 |
+
if userIDStr == "" {
|
| 112 |
+
log.Println("WebSocket connection rejected: missing user_id parameter")
|
| 113 |
+
c.Close()
|
| 114 |
+
return
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
userID, err := uuid.Parse(userIDStr)
|
| 118 |
+
if err != nil {
|
| 119 |
+
log.Printf("WebSocket connection rejected: invalid user_id format: %v", err)
|
| 120 |
+
c.Close()
|
| 121 |
+
return
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
client := &Client{
|
| 125 |
+
conn: c,
|
| 126 |
+
userID: userID,
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
hub.register <- client
|
| 130 |
+
|
| 131 |
+
defer func() {
|
| 132 |
+
hub.unregister <- client
|
| 133 |
+
}()
|
| 134 |
+
|
| 135 |
+
for {
|
| 136 |
+
_, _, err := c.ReadMessage()
|
| 137 |
+
if err != nil {
|
| 138 |
+
log.Printf("WebSocket read error: %v", err)
|
| 139 |
+
break
|
| 140 |
+
}
|
| 141 |
+
}
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
func BroadcastNewEntry(entry models.MoodEntry) {
|
| 145 |
+
message := map[string]interface{}{
|
| 146 |
+
"type": "new_entry",
|
| 147 |
+
"data": models.FeedResponse{
|
| 148 |
+
ID: entry.ID,
|
| 149 |
+
Mood: entry.Mood,
|
| 150 |
+
Note: entry.Note,
|
| 151 |
+
CreatedAt: entry.CreatedAt,
|
| 152 |
+
SupportCount: 0,
|
| 153 |
+
},
|
| 154 |
+
}
|
| 155 |
+
|
| 156 |
+
jsonData, err := json.Marshal(message)
|
| 157 |
+
if err != nil {
|
| 158 |
+
log.Printf("Failed to marshal WebSocket message: %v", err)
|
| 159 |
+
return
|
| 160 |
+
}
|
| 161 |
+
|
| 162 |
+
hub.broadcast <- jsonData
|
| 163 |
+
}
|
| 164 |
+
|
| 165 |
+
func BroadcastSupportNotification(userID uuid.UUID, journalID uuid.UUID, supportType string) {
|
| 166 |
+
message := map[string]interface{}{
|
| 167 |
+
"type": "support_received",
|
| 168 |
+
"data": map[string]interface{}{
|
| 169 |
+
"journal_id": journalID,
|
| 170 |
+
"support": supportType,
|
| 171 |
+
},
|
| 172 |
+
}
|
| 173 |
+
|
| 174 |
+
jsonData, err := json.Marshal(message)
|
| 175 |
+
if err != nil {
|
| 176 |
+
log.Printf("Failed to marshal support notification: %v", err)
|
| 177 |
+
return
|
| 178 |
+
}
|
| 179 |
+
|
| 180 |
+
hub.userMessage <- UserMessage{
|
| 181 |
+
UserID: userID,
|
| 182 |
+
Message: jsonData,
|
| 183 |
+
}
|
| 184 |
+
}
|
main.go
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package main
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"log"
|
| 5 |
+
"os"
|
| 6 |
+
"moodlink-backend/database"
|
| 7 |
+
"moodlink-backend/routes"
|
| 8 |
+
"moodlink-backend/services"
|
| 9 |
+
|
| 10 |
+
"github.com/gofiber/fiber/v2"
|
| 11 |
+
"github.com/gofiber/fiber/v2/middleware/cors"
|
| 12 |
+
"github.com/gofiber/fiber/v2/middleware/logger"
|
| 13 |
+
"github.com/gofiber/fiber/v2/middleware/recover"
|
| 14 |
+
"github.com/joho/godotenv"
|
| 15 |
+
)
|
| 16 |
+
|
| 17 |
+
func main() {
|
| 18 |
+
// Load environment variables
|
| 19 |
+
if err := godotenv.Load(); err != nil {
|
| 20 |
+
log.Println("No .env file found")
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
// Connect to database
|
| 24 |
+
database.Connect()
|
| 25 |
+
|
| 26 |
+
// Initialize services
|
| 27 |
+
if err := services.InitModeration(); err != nil {
|
| 28 |
+
log.Printf("Warning: Failed to initialize moderation: %v", err)
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
if err := services.InitFCM(); err != nil {
|
| 32 |
+
log.Printf("Warning: Failed to initialize FCM: %v", err)
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
// Start background services
|
| 36 |
+
services.StartInactivityChecker()
|
| 37 |
+
|
| 38 |
+
// Create Fiber app
|
| 39 |
+
app := fiber.New(fiber.Config{
|
| 40 |
+
AppName: "MoodLink Backend v1.0",
|
| 41 |
+
ErrorHandler: func(c *fiber.Ctx, err error) error {
|
| 42 |
+
code := fiber.StatusInternalServerError
|
| 43 |
+
if e, ok := err.(*fiber.Error); ok {
|
| 44 |
+
code = e.Code
|
| 45 |
+
}
|
| 46 |
+
return c.Status(code).JSON(fiber.Map{
|
| 47 |
+
"status": "error",
|
| 48 |
+
"message": err.Error(),
|
| 49 |
+
})
|
| 50 |
+
},
|
| 51 |
+
})
|
| 52 |
+
|
| 53 |
+
// Middleware
|
| 54 |
+
app.Use(cors.New(cors.Config{
|
| 55 |
+
AllowOrigins: "*",
|
| 56 |
+
AllowMethods: "GET,POST,PUT,DELETE,OPTIONS",
|
| 57 |
+
AllowHeaders: "Origin,Content-Type,Accept,Authorization,User-ID",
|
| 58 |
+
}))
|
| 59 |
+
|
| 60 |
+
app.Use(logger.New())
|
| 61 |
+
app.Use(recover.New())
|
| 62 |
+
|
| 63 |
+
// Setup routes
|
| 64 |
+
routes.SetupRoutes(app)
|
| 65 |
+
|
| 66 |
+
// Health check
|
| 67 |
+
app.Get("/health", func(c *fiber.Ctx) error {
|
| 68 |
+
return c.JSON(fiber.Map{
|
| 69 |
+
"status": "success",
|
| 70 |
+
"message": "MoodLink Backend is running",
|
| 71 |
+
})
|
| 72 |
+
})
|
| 73 |
+
|
| 74 |
+
// Start server
|
| 75 |
+
port := os.Getenv("PORT")
|
| 76 |
+
if port == "" {
|
| 77 |
+
port = "3000"
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
log.Printf("Server starting on port %s", port)
|
| 81 |
+
log.Fatal(app.Listen(":" + port))
|
| 82 |
+
}
|
middleware/rate_limiter.go
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package middleware
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"moodlink-backend/utils"
|
| 5 |
+
"sync"
|
| 6 |
+
"time"
|
| 7 |
+
|
| 8 |
+
"github.com/gofiber/fiber/v2"
|
| 9 |
+
)
|
| 10 |
+
|
| 11 |
+
type UserRateLimiter struct {
|
| 12 |
+
Count int
|
| 13 |
+
ResetTime time.Time
|
| 14 |
+
mutex sync.RWMutex
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
type RateLimiter struct {
|
| 18 |
+
users map[string]*UserRateLimiter
|
| 19 |
+
mutex sync.RWMutex
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
var rateLimiter = &RateLimiter{
|
| 23 |
+
users: make(map[string]*UserRateLimiter),
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
func UserRateLimit() fiber.Handler {
|
| 27 |
+
return func(c *fiber.Ctx) error {
|
| 28 |
+
userID := c.Get("User-ID")
|
| 29 |
+
if userID == "" {
|
| 30 |
+
return utils.ErrorResponse(c, fiber.StatusBadRequest, "User-ID header is required")
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
rateLimiter.mutex.Lock()
|
| 34 |
+
userLimit, exists := rateLimiter.users[userID]
|
| 35 |
+
if !exists {
|
| 36 |
+
userLimit = &UserRateLimiter{
|
| 37 |
+
Count: 0,
|
| 38 |
+
ResetTime: time.Now().Add(time.Minute),
|
| 39 |
+
}
|
| 40 |
+
rateLimiter.users[userID] = userLimit
|
| 41 |
+
}
|
| 42 |
+
rateLimiter.mutex.Unlock()
|
| 43 |
+
|
| 44 |
+
userLimit.mutex.Lock()
|
| 45 |
+
defer userLimit.mutex.Unlock()
|
| 46 |
+
|
| 47 |
+
now := time.Now()
|
| 48 |
+
if now.After(userLimit.ResetTime) {
|
| 49 |
+
userLimit.Count = 0
|
| 50 |
+
userLimit.ResetTime = now.Add(time.Minute)
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
if userLimit.Count >= 5 {
|
| 54 |
+
return utils.ErrorResponse(c, fiber.StatusTooManyRequests, "Rate limit exceeded. Max 5 requests per minute.")
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
userLimit.Count++
|
| 58 |
+
return c.Next()
|
| 59 |
+
}
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
// Cleanup expired entries periodically
|
| 63 |
+
func init() {
|
| 64 |
+
go func() {
|
| 65 |
+
ticker := time.NewTicker(5 * time.Minute)
|
| 66 |
+
defer ticker.Stop()
|
| 67 |
+
|
| 68 |
+
for range ticker.C {
|
| 69 |
+
rateLimiter.mutex.Lock()
|
| 70 |
+
now := time.Now()
|
| 71 |
+
for userID, userLimit := range rateLimiter.users {
|
| 72 |
+
userLimit.mutex.RLock()
|
| 73 |
+
if now.After(userLimit.ResetTime.Add(time.Minute)) {
|
| 74 |
+
delete(rateLimiter.users, userID)
|
| 75 |
+
}
|
| 76 |
+
userLimit.mutex.RUnlock()
|
| 77 |
+
}
|
| 78 |
+
rateLimiter.mutex.Unlock()
|
| 79 |
+
}
|
| 80 |
+
}()
|
| 81 |
+
}
|
middleware/user_validation.go
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package middleware
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"moodlink-backend/utils"
|
| 5 |
+
"github.com/gofiber/fiber/v2"
|
| 6 |
+
"github.com/google/uuid"
|
| 7 |
+
)
|
| 8 |
+
|
| 9 |
+
func ValidateUserID() fiber.Handler {
|
| 10 |
+
return func(c *fiber.Ctx) error {
|
| 11 |
+
userID := c.Get("User-ID")
|
| 12 |
+
if userID == "" {
|
| 13 |
+
return utils.ErrorResponse(c, fiber.StatusBadRequest, "User-ID header is required")
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
if _, err := uuid.Parse(userID); err != nil {
|
| 17 |
+
return utils.ErrorResponse(c, fiber.StatusBadRequest, "Invalid User-ID format")
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
return c.Next()
|
| 21 |
+
}
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
func ValidateAdmin() fiber.Handler {
|
| 25 |
+
return func(c *fiber.Ctx) error {
|
| 26 |
+
isAdmin := c.Get("Is-Admin")
|
| 27 |
+
if isAdmin != "true" {
|
| 28 |
+
return utils.ErrorResponse(c, fiber.StatusForbidden, "Admin access required")
|
| 29 |
+
}
|
| 30 |
+
return c.Next()
|
| 31 |
+
}
|
| 32 |
+
}
|
models/achievement.go
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package models
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"time"
|
| 5 |
+
"github.com/google/uuid"
|
| 6 |
+
"gorm.io/gorm"
|
| 7 |
+
)
|
| 8 |
+
|
| 9 |
+
type UserAchievement struct {
|
| 10 |
+
ID uint `json:"id" gorm:"primaryKey"`
|
| 11 |
+
UserID uuid.UUID `json:"user_id" gorm:"type:char(36);index"`
|
| 12 |
+
BadgeName string `json:"badge_name" gorm:"type:varchar(100)"`
|
| 13 |
+
EarnedAt time.Time `json:"earned_at"`
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
func (a *UserAchievement) BeforeCreate(tx *gorm.DB) error {
|
| 17 |
+
a.EarnedAt = time.Now()
|
| 18 |
+
return nil
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
type Achievement struct {
|
| 22 |
+
Name string `json:"name"`
|
| 23 |
+
DisplayName string `json:"display_name"`
|
| 24 |
+
Description string `json:"description"`
|
| 25 |
+
Icon string `json:"icon"`
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
var AvailableAchievements = []Achievement{
|
| 29 |
+
{
|
| 30 |
+
Name: "reflective_seven",
|
| 31 |
+
DisplayName: "Streak 7 Hari",
|
| 32 |
+
Description: "Check-in selama 7 hari berturut-turut",
|
| 33 |
+
Icon: "🔥",
|
| 34 |
+
},
|
| 35 |
+
{
|
| 36 |
+
Name: "supporter",
|
| 37 |
+
DisplayName: "Supporter",
|
| 38 |
+
Description: "Memberikan 10 support kepada orang lain",
|
| 39 |
+
Icon: "❤️",
|
| 40 |
+
},
|
| 41 |
+
{
|
| 42 |
+
Name: "emotional_explorer",
|
| 43 |
+
DisplayName: "Emotional Explorer",
|
| 44 |
+
Description: "Merasakan 5 mood berbeda dalam 7 hari",
|
| 45 |
+
Icon: "🌈",
|
| 46 |
+
},
|
| 47 |
+
{
|
| 48 |
+
Name: "first_checkin",
|
| 49 |
+
DisplayName: "First Step",
|
| 50 |
+
Description: "Melakukan check-in pertama",
|
| 51 |
+
Icon: "🌟",
|
| 52 |
+
},
|
| 53 |
+
{
|
| 54 |
+
Name: "level_master",
|
| 55 |
+
DisplayName: "Level Master",
|
| 56 |
+
Description: "Mencapai level 5",
|
| 57 |
+
Icon: "👑",
|
| 58 |
+
},
|
| 59 |
+
}
|
models/ai_reflection.go
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package models
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"time"
|
| 5 |
+
|
| 6 |
+
"github.com/google/uuid"
|
| 7 |
+
"gorm.io/gorm"
|
| 8 |
+
)
|
| 9 |
+
|
| 10 |
+
type AIReflection struct {
|
| 11 |
+
ID uuid.UUID `json:"id" gorm:"type:char(36);primaryKey"`
|
| 12 |
+
UserID uuid.UUID `json:"user_id" gorm:"type:char(36);index"`
|
| 13 |
+
Question string `json:"question" gorm:"type:text"`
|
| 14 |
+
Answer string `json:"answer" gorm:"type:text"`
|
| 15 |
+
CreatedAt time.Time `json:"created_at"`
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
func (r *AIReflection) BeforeCreate(tx *gorm.DB) error {
|
| 19 |
+
if r.ID == uuid.Nil {
|
| 20 |
+
r.ID = uuid.New()
|
| 21 |
+
}
|
| 22 |
+
return nil
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
type AIReflectionRequest struct {
|
| 26 |
+
Prompt string `json:"prompt" validate:"required,min=1,max=1000"`
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
type AIReflectionResponse struct {
|
| 30 |
+
Question string `json:"question"`
|
| 31 |
+
Answer string `json:"answer"`
|
| 32 |
+
CreatedAt time.Time `json:"created_at"`
|
| 33 |
+
}
|
models/chat.go
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package models
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"time"
|
| 5 |
+
"github.com/google/uuid"
|
| 6 |
+
"gorm.io/gorm"
|
| 7 |
+
)
|
| 8 |
+
|
| 9 |
+
type ChatRoom struct {
|
| 10 |
+
ID uuid.UUID `json:"id" gorm:"type:char(36);primaryKey"`
|
| 11 |
+
UserA uuid.UUID `json:"user_a" gorm:"type:char(36);index"`
|
| 12 |
+
UserB uuid.UUID `json:"user_b" gorm:"type:char(36);index"`
|
| 13 |
+
IsActive bool `json:"is_active" gorm:"default:true"`
|
| 14 |
+
CreatedAt time.Time `json:"created_at"`
|
| 15 |
+
UpdatedAt time.Time `json:"updated_at"`
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
func (r *ChatRoom) BeforeCreate(tx *gorm.DB) error {
|
| 19 |
+
if r.ID == uuid.Nil {
|
| 20 |
+
r.ID = uuid.New()
|
| 21 |
+
}
|
| 22 |
+
return nil
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
type ChatMessage struct {
|
| 26 |
+
ID uuid.UUID `json:"id" gorm:"type:char(36);primaryKey"`
|
| 27 |
+
RoomID uuid.UUID `json:"room_id" gorm:"type:char(36);index"`
|
| 28 |
+
SenderID uuid.UUID `json:"sender_id" gorm:"type:char(36);index"`
|
| 29 |
+
Message string `json:"message" gorm:"type:text"`
|
| 30 |
+
CreatedAt time.Time `json:"created_at"`
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
func (m *ChatMessage) BeforeCreate(tx *gorm.DB) error {
|
| 34 |
+
if m.ID == uuid.Nil {
|
| 35 |
+
m.ID = uuid.New()
|
| 36 |
+
}
|
| 37 |
+
return nil
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
type ChatStartResponse struct {
|
| 41 |
+
RoomID uuid.UUID `json:"room_id"`
|
| 42 |
+
Message string `json:"message"`
|
| 43 |
+
CreatedAt time.Time `json:"created_at"`
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
type SendMessageRequest struct {
|
| 47 |
+
Message string `json:"message" validate:"required,min=1,max=1000"`
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
type MessageResponse struct {
|
| 51 |
+
ID uuid.UUID `json:"id"`
|
| 52 |
+
RoomID uuid.UUID `json:"room_id"`
|
| 53 |
+
SenderID uuid.UUID `json:"sender_id"`
|
| 54 |
+
Message string `json:"message"`
|
| 55 |
+
CreatedAt time.Time `json:"created_at"`
|
| 56 |
+
IsOwn bool `json:"is_own"`
|
| 57 |
+
}
|
models/device_token.go
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package models
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"time"
|
| 5 |
+
"github.com/google/uuid"
|
| 6 |
+
"gorm.io/gorm"
|
| 7 |
+
)
|
| 8 |
+
|
| 9 |
+
type DeviceToken struct {
|
| 10 |
+
ID uint `json:"id" gorm:"primaryKey"`
|
| 11 |
+
UserID uuid.UUID `json:"user_id" gorm:"type:char(36);index"`
|
| 12 |
+
Token string `json:"token" gorm:"type:text"`
|
| 13 |
+
CreatedAt time.Time `json:"created_at"`
|
| 14 |
+
UpdatedAt time.Time `json:"updated_at"`
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
func (d *DeviceToken) BeforeCreate(tx *gorm.DB) error {
|
| 18 |
+
d.CreatedAt = time.Now()
|
| 19 |
+
d.UpdatedAt = time.Now()
|
| 20 |
+
return nil
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
func (d *DeviceToken) BeforeUpdate(tx *gorm.DB) error {
|
| 24 |
+
d.UpdatedAt = time.Now()
|
| 25 |
+
return nil
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
type DeviceTokenRequest struct {
|
| 29 |
+
Token string `json:"token" validate:"required"`
|
| 30 |
+
}
|
models/google_auth.go
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package models
|
| 2 |
+
|
| 3 |
+
type GoogleAuthRequest struct {
|
| 4 |
+
IDToken string `json:"id_token" validate:"required"`
|
| 5 |
+
}
|
| 6 |
+
|
| 7 |
+
type GoogleTokenInfo struct {
|
| 8 |
+
Sub string `json:"sub"`
|
| 9 |
+
Email string `json:"email"`
|
| 10 |
+
EmailVerified bool `json:"email_verified"`
|
| 11 |
+
Name string `json:"name"`
|
| 12 |
+
Picture string `json:"picture"`
|
| 13 |
+
}
|
models/journal_comment.go
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package models
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"time"
|
| 5 |
+
|
| 6 |
+
"github.com/google/uuid"
|
| 7 |
+
"gorm.io/gorm"
|
| 8 |
+
)
|
| 9 |
+
|
| 10 |
+
type JournalComment struct {
|
| 11 |
+
ID uuid.UUID `json:"id" gorm:"type:char(36);primaryKey"`
|
| 12 |
+
JournalID uuid.UUID `json:"journal_id" gorm:"type:char(36);index"`
|
| 13 |
+
UserID uuid.UUID `json:"user_id" gorm:"type:char(36);index"`
|
| 14 |
+
Comment string `json:"comment" gorm:"type:text"`
|
| 15 |
+
IsFlagged bool `json:"is_flagged" gorm:"default:false"`
|
| 16 |
+
CreatedAt time.Time `json:"created_at"`
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
func (c *JournalComment) BeforeCreate(tx *gorm.DB) error {
|
| 20 |
+
if c.ID == uuid.Nil {
|
| 21 |
+
c.ID = uuid.New()
|
| 22 |
+
}
|
| 23 |
+
return nil
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
type CommentRequest struct {
|
| 27 |
+
JournalID string `json:"journal_id" validate:"required,uuid"`
|
| 28 |
+
Comment string `json:"comment" validate:"required,min=1,max=500"`
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
type CommentResponse struct {
|
| 32 |
+
ID uuid.UUID `json:"id"`
|
| 33 |
+
JournalID uuid.UUID `json:"journal_id"`
|
| 34 |
+
Comment string `json:"comment"`
|
| 35 |
+
CreatedAt time.Time `json:"created_at"`
|
| 36 |
+
IsFlagged bool `json:"is_flagged"`
|
| 37 |
+
}
|
models/journal_report.go
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package models
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"time"
|
| 5 |
+
"github.com/google/uuid"
|
| 6 |
+
"gorm.io/gorm"
|
| 7 |
+
)
|
| 8 |
+
|
| 9 |
+
type JournalReport struct {
|
| 10 |
+
ID uuid.UUID `json:"id" gorm:"type:char(36);primaryKey"`
|
| 11 |
+
JournalID uuid.UUID `json:"journal_id" gorm:"type:char(36);index"`
|
| 12 |
+
Reason string `json:"reason" gorm:"type:varchar(255)"`
|
| 13 |
+
CreatedAt time.Time `json:"created_at"`
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
func (r *JournalReport) BeforeCreate(tx *gorm.DB) error {
|
| 17 |
+
if r.ID == uuid.Nil {
|
| 18 |
+
r.ID = uuid.New()
|
| 19 |
+
}
|
| 20 |
+
return nil
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
type ReportRequest struct {
|
| 24 |
+
JournalID string `json:"journal_id" validate:"required,uuid"`
|
| 25 |
+
Reason string `json:"reason" validate:"required"`
|
| 26 |
+
}
|
models/journal_support.go
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package models
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"time"
|
| 5 |
+
"github.com/google/uuid"
|
| 6 |
+
// "gorm.io/gorm"
|
| 7 |
+
)
|
| 8 |
+
|
| 9 |
+
type JournalSupport struct {
|
| 10 |
+
ID uint `json:"id" gorm:"primaryKey"`
|
| 11 |
+
JournalID uuid.UUID `json:"journal_id" gorm:"type:char(36);index"`
|
| 12 |
+
Type string `json:"type" gorm:"type:enum('heart','hug','pray')"`
|
| 13 |
+
CreatedAt time.Time `json:"created_at"`
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
type SupportRequest struct {
|
| 17 |
+
JournalID string `json:"journal_id" validate:"required,uuid"`
|
| 18 |
+
Type string `json:"type" validate:"required,oneof=heart hug pray"`
|
| 19 |
+
}
|
models/mood_entry.go
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package models
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"time"
|
| 5 |
+
"github.com/google/uuid"
|
| 6 |
+
"gorm.io/gorm"
|
| 7 |
+
)
|
| 8 |
+
|
| 9 |
+
type MoodEntry struct {
|
| 10 |
+
ID uuid.UUID `json:"id" gorm:"type:char(36);primaryKey"`
|
| 11 |
+
UserID uuid.UUID `json:"user_id" gorm:"type:char(36);index"`
|
| 12 |
+
Mood string `json:"mood" gorm:"type:enum('happy','sad','tired','angry','anxious','neutral')"`
|
| 13 |
+
Note string `json:"note" gorm:"type:text"`
|
| 14 |
+
IsFlagged bool `json:"is_flagged" gorm:"default:false"`
|
| 15 |
+
LocationLat float64 `json:"location_lat"`
|
| 16 |
+
LocationLng float64 `json:"location_lng"`
|
| 17 |
+
CreatedAt time.Time `json:"created_at"`
|
| 18 |
+
|
| 19 |
+
// Virtual field for support count
|
| 20 |
+
SupportCount int `json:"support_count" gorm:"-"`
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
func (m *MoodEntry) BeforeCreate(tx *gorm.DB) error {
|
| 24 |
+
if m.ID == uuid.Nil {
|
| 25 |
+
m.ID = uuid.New()
|
| 26 |
+
}
|
| 27 |
+
return nil
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
type MoodEntryRequest struct {
|
| 31 |
+
UserID string `json:"user_id" validate:"required,uuid"`
|
| 32 |
+
Mood string `json:"mood" validate:"required,oneof=happy sad tired angry anxious neutral"`
|
| 33 |
+
Note string `json:"note"`
|
| 34 |
+
LocationLat float64 `json:"location_lat"`
|
| 35 |
+
LocationLng float64 `json:"location_lng"`
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
type FeedResponse struct {
|
| 39 |
+
ID uuid.UUID `json:"id"`
|
| 40 |
+
Mood string `json:"mood"`
|
| 41 |
+
Note string `json:"note"`
|
| 42 |
+
CreatedAt time.Time `json:"created_at"`
|
| 43 |
+
SupportCount int `json:"support_count"`
|
| 44 |
+
}
|
models/user.go
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package models
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"time"
|
| 5 |
+
|
| 6 |
+
"github.com/google/uuid"
|
| 7 |
+
"gorm.io/gorm"
|
| 8 |
+
)
|
| 9 |
+
|
| 10 |
+
type AnonymousUser struct {
|
| 11 |
+
ID uuid.UUID `json:"id" gorm:"type:char(36);primaryKey"`
|
| 12 |
+
GoogleID *string `json:"google_id,omitempty" gorm:"type:varchar(255);uniqueIndex"`
|
| 13 |
+
IsSynced bool `json:"is_synced" gorm:"default:false"`
|
| 14 |
+
CreatedAt time.Time `json:"created_at"`
|
| 15 |
+
Points int `json:"points" gorm:"default:0"`
|
| 16 |
+
Level int `json:"level" gorm:"default:1"`
|
| 17 |
+
LastCheckin *time.Time `json:"last_checkin" gorm:"type:date"`
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
func (u *AnonymousUser) BeforeCreate(tx *gorm.DB) error {
|
| 21 |
+
if u.ID == uuid.Nil {
|
| 22 |
+
u.ID = uuid.New()
|
| 23 |
+
}
|
| 24 |
+
return nil
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
func (u *AnonymousUser) UpdateLevel() {
|
| 28 |
+
if u.Points < 100 {
|
| 29 |
+
u.Level = 1
|
| 30 |
+
} else if u.Points < 300 {
|
| 31 |
+
u.Level = 2
|
| 32 |
+
} else if u.Points < 700 {
|
| 33 |
+
u.Level = 3
|
| 34 |
+
} else if u.Points < 1500 {
|
| 35 |
+
u.Level = 4
|
| 36 |
+
} else {
|
| 37 |
+
u.Level = 5
|
| 38 |
+
}
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
func (u *AnonymousUser) GetStreakCount(db *gorm.DB) int {
|
| 42 |
+
if u.LastCheckin == nil {
|
| 43 |
+
return 0
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
var entries []MoodEntry
|
| 47 |
+
db.Where("user_id = ?", u.ID).Order("created_at desc").Find(&entries)
|
| 48 |
+
|
| 49 |
+
if len(entries) == 0 {
|
| 50 |
+
return 0
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
streak := 1
|
| 54 |
+
// lastDate := entries[0].CreatedAt.Format("2006-01-02")
|
| 55 |
+
|
| 56 |
+
for i := 1; i < len(entries); i++ {
|
| 57 |
+
currentDate := entries[i].CreatedAt.Format("2006-01-02")
|
| 58 |
+
expectedDate := entries[i-1].CreatedAt.AddDate(0, 0, -1).Format("2006-01-02")
|
| 59 |
+
|
| 60 |
+
if currentDate == expectedDate {
|
| 61 |
+
streak++
|
| 62 |
+
} else {
|
| 63 |
+
break
|
| 64 |
+
}
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
return streak
|
| 68 |
+
}
|
moderation/words.txt
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Moderation Terms - one per line
|
| 2 |
+
# Lines starting with # are comments
|
| 3 |
+
spam
|
| 4 |
+
scam
|
| 5 |
+
hate
|
| 6 |
+
abuse
|
| 7 |
+
violence
|
| 8 |
+
suicide
|
| 9 |
+
self-harm
|
| 10 |
+
drugs
|
| 11 |
+
illegal
|
| 12 |
+
kill
|
| 13 |
+
death
|
| 14 |
+
bomb
|
| 15 |
+
weapon
|
| 16 |
+
terrorist
|
| 17 |
+
fraud
|
| 18 |
+
phishing
|
routes/routes.go
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package routes
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"moodlink-backend/handlers"
|
| 5 |
+
"moodlink-backend/middleware"
|
| 6 |
+
|
| 7 |
+
"github.com/gofiber/fiber/v2"
|
| 8 |
+
"github.com/gofiber/websocket/v2"
|
| 9 |
+
)
|
| 10 |
+
|
| 11 |
+
func SetupRoutes(app *fiber.App) {
|
| 12 |
+
api := app.Group("/api/v1")
|
| 13 |
+
|
| 14 |
+
// Auth routes
|
| 15 |
+
api.Get("/user", handlers.RegisterOrGetUser)
|
| 16 |
+
api.Post("/auth/google", middleware.ValidateUserID(), handlers.GoogleAuth)
|
| 17 |
+
api.Post("/auth/restore", handlers.RestoreFromGoogle)
|
| 18 |
+
|
| 19 |
+
// Mood routes
|
| 20 |
+
api.Post("/checkin", middleware.UserRateLimit(), handlers.CheckIn)
|
| 21 |
+
|
| 22 |
+
// Feed routes
|
| 23 |
+
api.Get("/feed", handlers.GetFeed)
|
| 24 |
+
|
| 25 |
+
// Support routes
|
| 26 |
+
api.Post("/support", middleware.UserRateLimit(), handlers.SendSupport)
|
| 27 |
+
|
| 28 |
+
// Dashboard routes
|
| 29 |
+
api.Get("/dashboard/:user_id", handlers.GetDashboard)
|
| 30 |
+
|
| 31 |
+
// User stats and management
|
| 32 |
+
api.Get("/user/stats", middleware.ValidateUserID(), handlers.GetUserStats)
|
| 33 |
+
api.Post("/user/reset", middleware.ValidateUserID(), handlers.ResetUserAccount)
|
| 34 |
+
|
| 35 |
+
// Achievements
|
| 36 |
+
api.Get("/achievements/:user_id", handlers.GetUserAchievements)
|
| 37 |
+
|
| 38 |
+
// Device tokens for push notifications
|
| 39 |
+
api.Post("/device-token", middleware.ValidateUserID(), handlers.RegisterDeviceToken)
|
| 40 |
+
api.Delete("/device-token", middleware.ValidateUserID(), handlers.UnregisterDeviceToken)
|
| 41 |
+
|
| 42 |
+
// Statistics routes
|
| 43 |
+
api.Get("/stats/global", handlers.GetGlobalStats)
|
| 44 |
+
|
| 45 |
+
// Report routes
|
| 46 |
+
api.Post("/report", handlers.ReportJournal)
|
| 47 |
+
api.Get("/reports", middleware.ValidateAdmin(), handlers.GetReports)
|
| 48 |
+
|
| 49 |
+
// Moderation routes (admin only)
|
| 50 |
+
api.Get("/moderation/flagged", middleware.ValidateAdmin(), handlers.GetFlaggedEntries)
|
| 51 |
+
api.Put("/moderation/unflag/:entry_id", middleware.ValidateAdmin(), handlers.UnflagEntry)
|
| 52 |
+
|
| 53 |
+
// Comment routes
|
| 54 |
+
api.Post("/comment", middleware.ValidateUserID(), middleware.UserRateLimit(), handlers.AddComment)
|
| 55 |
+
api.Get("/comment/:journal_id", handlers.GetComments)
|
| 56 |
+
|
| 57 |
+
// Chat routes
|
| 58 |
+
api.Post("/chat/start", middleware.ValidateUserID(), handlers.StartChat)
|
| 59 |
+
api.Post("/chat/:room_id/message", middleware.ValidateUserID(), middleware.UserRateLimit(), handlers.SendMessage)
|
| 60 |
+
api.Get("/chat/:room_id/messages", middleware.ValidateUserID(), handlers.GetMessages)
|
| 61 |
+
api.Delete("/chat/:room_id/end", middleware.ValidateUserID(), handlers.EndChat)
|
| 62 |
+
api.Get("/chat/active", middleware.ValidateUserID(), handlers.GetActiveChats)
|
| 63 |
+
|
| 64 |
+
// AI Reflection routes
|
| 65 |
+
api.Post("/ai/reflect", middleware.ValidateUserID(), middleware.UserRateLimit(), handlers.AIReflection)
|
| 66 |
+
api.Get("/ai/history", middleware.ValidateUserID(), handlers.GetReflectionHistory)
|
| 67 |
+
|
| 68 |
+
// WebSocket route
|
| 69 |
+
app.Get("/ws/feed", websocket.New(handlers.HandleWebSocket))
|
| 70 |
+
}
|
services/achievements.go
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package services
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"moodlink-backend/database"
|
| 5 |
+
"moodlink-backend/models"
|
| 6 |
+
"time"
|
| 7 |
+
"github.com/google/uuid"
|
| 8 |
+
)
|
| 9 |
+
|
| 10 |
+
func AssignAchievements(userID uuid.UUID) error {
|
| 11 |
+
var user models.AnonymousUser
|
| 12 |
+
if err := database.DB.Where("id = ?", userID).First(&user).Error; err != nil {
|
| 13 |
+
return err
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
// Check for first check-in achievement
|
| 17 |
+
checkFirstCheckin(userID)
|
| 18 |
+
|
| 19 |
+
// Check for streak achievement
|
| 20 |
+
checkStreakAchievement(userID)
|
| 21 |
+
|
| 22 |
+
// Check for supporter achievement
|
| 23 |
+
checkSupporterAchievement(userID)
|
| 24 |
+
|
| 25 |
+
// Check for emotional explorer achievement
|
| 26 |
+
checkEmotionalExplorerAchievement(userID)
|
| 27 |
+
|
| 28 |
+
// Check for level master achievement
|
| 29 |
+
checkLevelMasterAchievement(userID)
|
| 30 |
+
|
| 31 |
+
return nil
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
func checkFirstCheckin(userID uuid.UUID) {
|
| 35 |
+
// Check if user already has this achievement
|
| 36 |
+
var existing models.UserAchievement
|
| 37 |
+
if database.DB.Where("user_id = ? AND badge_name = ?", userID, "first_checkin").First(&existing).Error == nil {
|
| 38 |
+
return
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
// Check if user has any mood entries
|
| 42 |
+
var count int64
|
| 43 |
+
database.DB.Model(&models.MoodEntry{}).Where("user_id = ?", userID).Count(&count)
|
| 44 |
+
|
| 45 |
+
if count >= 1 {
|
| 46 |
+
achievement := models.UserAchievement{
|
| 47 |
+
UserID: userID,
|
| 48 |
+
BadgeName: "first_checkin",
|
| 49 |
+
}
|
| 50 |
+
database.DB.Create(&achievement)
|
| 51 |
+
}
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
func checkStreakAchievement(userID uuid.UUID) {
|
| 55 |
+
// Check if user already has this achievement
|
| 56 |
+
var existing models.UserAchievement
|
| 57 |
+
if database.DB.Where("user_id = ? AND badge_name = ?", userID, "reflective_seven").First(&existing).Error == nil {
|
| 58 |
+
return
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
var user models.AnonymousUser
|
| 62 |
+
database.DB.Where("id = ?", userID).First(&user)
|
| 63 |
+
|
| 64 |
+
streakCount := user.GetStreakCount(database.DB)
|
| 65 |
+
if streakCount >= 7 {
|
| 66 |
+
achievement := models.UserAchievement{
|
| 67 |
+
UserID: userID,
|
| 68 |
+
BadgeName: "reflective_seven",
|
| 69 |
+
}
|
| 70 |
+
database.DB.Create(&achievement)
|
| 71 |
+
}
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
func checkSupporterAchievement(userID uuid.UUID) {
|
| 75 |
+
// Check if user already has this achievement
|
| 76 |
+
var existing models.UserAchievement
|
| 77 |
+
if database.DB.Where("user_id = ? AND badge_name = ?", userID, "supporter").First(&existing).Error == nil {
|
| 78 |
+
return
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
// Count support given by user (we need to track this in support table)
|
| 82 |
+
// For now, we'll use a simple count based on points earned from giving support
|
| 83 |
+
var user models.AnonymousUser
|
| 84 |
+
database.DB.Where("id = ?", userID).First(&user)
|
| 85 |
+
|
| 86 |
+
// Estimate support given: (total_points - checkin_points) / 2
|
| 87 |
+
var checkinCount int64
|
| 88 |
+
database.DB.Model(&models.MoodEntry{}).Where("user_id = ?", userID).Count(&checkinCount)
|
| 89 |
+
|
| 90 |
+
estimatedCheckinPoints := checkinCount * 10 // Base points per checkin
|
| 91 |
+
estimatedSupportGiven := (user.Points - int(estimatedCheckinPoints)) / 2
|
| 92 |
+
|
| 93 |
+
if estimatedSupportGiven >= 10 {
|
| 94 |
+
achievement := models.UserAchievement{
|
| 95 |
+
UserID: userID,
|
| 96 |
+
BadgeName: "supporter",
|
| 97 |
+
}
|
| 98 |
+
database.DB.Create(&achievement)
|
| 99 |
+
}
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
func checkEmotionalExplorerAchievement(userID uuid.UUID) {
|
| 103 |
+
// Check if user already has this achievement
|
| 104 |
+
var existing models.UserAchievement
|
| 105 |
+
if database.DB.Where("user_id = ? AND badge_name = ?", userID, "emotional_explorer").First(&existing).Error == nil {
|
| 106 |
+
return
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
// Check distinct moods in last 7 days
|
| 110 |
+
sevenDaysAgo := time.Now().AddDate(0, 0, -7)
|
| 111 |
+
var distinctMoods []string
|
| 112 |
+
|
| 113 |
+
database.DB.Model(&models.MoodEntry{}).
|
| 114 |
+
Where("user_id = ? AND created_at >= ?", userID, sevenDaysAgo).
|
| 115 |
+
Distinct("mood").
|
| 116 |
+
Pluck("mood", &distinctMoods)
|
| 117 |
+
|
| 118 |
+
if len(distinctMoods) >= 5 {
|
| 119 |
+
achievement := models.UserAchievement{
|
| 120 |
+
UserID: userID,
|
| 121 |
+
BadgeName: "emotional_explorer",
|
| 122 |
+
}
|
| 123 |
+
database.DB.Create(&achievement)
|
| 124 |
+
}
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
func checkLevelMasterAchievement(userID uuid.UUID) {
|
| 128 |
+
// Check if user already has this achievement
|
| 129 |
+
var existing models.UserAchievement
|
| 130 |
+
if database.DB.Where("user_id = ? AND badge_name = ?", userID, "level_master").First(&existing).Error == nil {
|
| 131 |
+
return
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
var user models.AnonymousUser
|
| 135 |
+
database.DB.Where("id = ?", userID).First(&user)
|
| 136 |
+
|
| 137 |
+
if user.Level >= 5 {
|
| 138 |
+
achievement := models.UserAchievement{
|
| 139 |
+
UserID: userID,
|
| 140 |
+
BadgeName: "level_master",
|
| 141 |
+
}
|
| 142 |
+
database.DB.Create(&achievement)
|
| 143 |
+
}
|
| 144 |
+
}
|
services/chat_matching.go
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package services
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"moodlink-backend/database"
|
| 5 |
+
"moodlink-backend/models"
|
| 6 |
+
"time"
|
| 7 |
+
|
| 8 |
+
"github.com/google/uuid"
|
| 9 |
+
// "gorm.io/gorm"
|
| 10 |
+
)
|
| 11 |
+
|
| 12 |
+
// Simple in-memory queue for waiting users
|
| 13 |
+
var waitingUsers = make(map[uuid.UUID]time.Time)
|
| 14 |
+
|
| 15 |
+
func FindOrCreateChatRoom(userID uuid.UUID) (*models.ChatRoom, bool, error) {
|
| 16 |
+
// Check if user already has an active chat room
|
| 17 |
+
var existingRoom models.ChatRoom
|
| 18 |
+
result := database.DB.Where("(user_a = ? OR user_b = ?) AND is_active = ?", userID, userID, true).First(&existingRoom)
|
| 19 |
+
if result.Error == nil {
|
| 20 |
+
return &existingRoom, false, nil
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
// Clean up old waiting users (older than 5 minutes)
|
| 24 |
+
now := time.Now()
|
| 25 |
+
for waitingUserID, waitTime := range waitingUsers {
|
| 26 |
+
if now.Sub(waitTime) > 5*time.Minute {
|
| 27 |
+
delete(waitingUsers, waitingUserID)
|
| 28 |
+
}
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
// Try to match with a waiting user
|
| 32 |
+
for waitingUserID, _ := range waitingUsers {
|
| 33 |
+
if waitingUserID != userID {
|
| 34 |
+
// Create new chat room
|
| 35 |
+
room := models.ChatRoom{
|
| 36 |
+
UserA: waitingUserID,
|
| 37 |
+
UserB: userID,
|
| 38 |
+
IsActive: true,
|
| 39 |
+
CreatedAt: now,
|
| 40 |
+
UpdatedAt: now,
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
if err := database.DB.Create(&room).Error; err != nil {
|
| 44 |
+
return nil, false, err
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
// Remove both users from waiting queue
|
| 48 |
+
delete(waitingUsers, waitingUserID)
|
| 49 |
+
delete(waitingUsers, userID)
|
| 50 |
+
|
| 51 |
+
return &room, true, nil
|
| 52 |
+
}
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
// No match found, add user to waiting queue
|
| 56 |
+
waitingUsers[userID] = now
|
| 57 |
+
return nil, false, nil
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
func IsUserInRoom(userID, roomID uuid.UUID) bool {
|
| 61 |
+
var room models.ChatRoom
|
| 62 |
+
result := database.DB.Where("id = ? AND (user_a = ? OR user_b = ?) AND is_active = ?",
|
| 63 |
+
roomID, userID, userID, true).First(&room)
|
| 64 |
+
return result.Error == nil
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
func EndChatRoom(roomID uuid.UUID) error {
|
| 68 |
+
return database.DB.Model(&models.ChatRoom{}).
|
| 69 |
+
Where("id = ?", roomID).
|
| 70 |
+
Update("is_active", false).Error
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
func GetActiveRoomsForUser(userID uuid.UUID) ([]models.ChatRoom, error) {
|
| 74 |
+
var rooms []models.ChatRoom
|
| 75 |
+
err := database.DB.Where("(user_a = ? OR user_b = ?) AND is_active = ?",
|
| 76 |
+
userID, userID, true).Find(&rooms).Error
|
| 77 |
+
return rooms, err
|
| 78 |
+
}
|
services/gemini.go
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package services
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"bytes"
|
| 5 |
+
"encoding/json"
|
| 6 |
+
"fmt"
|
| 7 |
+
"io"
|
| 8 |
+
"net/http"
|
| 9 |
+
"os"
|
| 10 |
+
"time"
|
| 11 |
+
)
|
| 12 |
+
|
| 13 |
+
type GeminiRequest struct {
|
| 14 |
+
Contents []GeminiContent `json:"contents"`
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
type GeminiContent struct {
|
| 18 |
+
Parts []GeminiPart `json:"parts"`
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
type GeminiPart struct {
|
| 22 |
+
Text string `json:"text"`
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
type GeminiResponse struct {
|
| 26 |
+
Candidates []GeminiCandidate `json:"candidates"`
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
type GeminiCandidate struct {
|
| 30 |
+
Content GeminiContent `json:"content"`
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
func CallGeminiAPI(prompt string) (string, error) {
|
| 34 |
+
apiKey := os.Getenv("GEMINI_API_KEY")
|
| 35 |
+
if apiKey == "" {
|
| 36 |
+
return "", fmt.Errorf("GEMINI_API_KEY not configured")
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
// Enhance prompt for emotional reflection context
|
| 40 |
+
enhancedPrompt := fmt.Sprintf(`You are a compassionate AI assistant helping with emotional reflection and mental wellness.
|
| 41 |
+
Please provide a thoughtful, supportive response to this reflection prompt. Keep your response warm, understanding, and helpful.
|
| 42 |
+
|
| 43 |
+
User's reflection: %s`, prompt)
|
| 44 |
+
|
| 45 |
+
url := fmt.Sprintf("https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=%s", apiKey)
|
| 46 |
+
|
| 47 |
+
requestBody := GeminiRequest{
|
| 48 |
+
Contents: []GeminiContent{
|
| 49 |
+
{
|
| 50 |
+
Parts: []GeminiPart{
|
| 51 |
+
{Text: enhancedPrompt},
|
| 52 |
+
},
|
| 53 |
+
},
|
| 54 |
+
},
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
jsonData, err := json.Marshal(requestBody)
|
| 58 |
+
if err != nil {
|
| 59 |
+
return "", fmt.Errorf("failed to marshal request: %v", err)
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
client := &http.Client{
|
| 63 |
+
Timeout: 30 * time.Second,
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData))
|
| 67 |
+
if err != nil {
|
| 68 |
+
return "", fmt.Errorf("failed to create request: %v", err)
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
req.Header.Set("Content-Type", "application/json")
|
| 72 |
+
|
| 73 |
+
resp, err := client.Do(req)
|
| 74 |
+
if err != nil {
|
| 75 |
+
return "", fmt.Errorf("failed to call Gemini API: %v", err)
|
| 76 |
+
}
|
| 77 |
+
defer resp.Body.Close()
|
| 78 |
+
|
| 79 |
+
if resp.StatusCode != http.StatusOK {
|
| 80 |
+
body, _ := io.ReadAll(resp.Body)
|
| 81 |
+
return "", fmt.Errorf("Gemini API error (status %d): %s", resp.StatusCode, string(body))
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
body, err := io.ReadAll(resp.Body)
|
| 85 |
+
if err != nil {
|
| 86 |
+
return "", fmt.Errorf("failed to read response: %v", err)
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
var geminiResp GeminiResponse
|
| 90 |
+
if err := json.Unmarshal(body, &geminiResp); err != nil {
|
| 91 |
+
return "", fmt.Errorf("failed to parse response: %v", err)
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
if len(geminiResp.Candidates) == 0 || len(geminiResp.Candidates[0].Content.Parts) == 0 {
|
| 95 |
+
return "", fmt.Errorf("no response from Gemini API")
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
return geminiResp.Candidates[0].Content.Parts[0].Text, nil
|
| 99 |
+
}
|
services/google_auth.go
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package services
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"encoding/json"
|
| 5 |
+
"fmt"
|
| 6 |
+
"io"
|
| 7 |
+
"moodlink-backend/models"
|
| 8 |
+
"net/http"
|
| 9 |
+
)
|
| 10 |
+
|
| 11 |
+
func VerifyGoogleToken(idToken string) (*models.GoogleTokenInfo, error) {
|
| 12 |
+
url := fmt.Sprintf("https://oauth2.googleapis.com/tokeninfo?id_token=%s", idToken)
|
| 13 |
+
|
| 14 |
+
resp, err := http.Get(url)
|
| 15 |
+
if err != nil {
|
| 16 |
+
return nil, fmt.Errorf("failed to verify token: %v", err)
|
| 17 |
+
}
|
| 18 |
+
defer resp.Body.Close()
|
| 19 |
+
|
| 20 |
+
if resp.StatusCode != http.StatusOK {
|
| 21 |
+
return nil, fmt.Errorf("invalid token")
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
body, err := io.ReadAll(resp.Body)
|
| 25 |
+
if err != nil {
|
| 26 |
+
return nil, fmt.Errorf("failed to read response: %v", err)
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
var tokenInfo models.GoogleTokenInfo
|
| 30 |
+
if err := json.Unmarshal(body, &tokenInfo); err != nil {
|
| 31 |
+
return nil, fmt.Errorf("failed to parse token info: %v", err)
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
return &tokenInfo, nil
|
| 35 |
+
}
|
services/moderation.go
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package services
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"bufio"
|
| 5 |
+
"log"
|
| 6 |
+
"os"
|
| 7 |
+
"regexp"
|
| 8 |
+
"strings"
|
| 9 |
+
)
|
| 10 |
+
|
| 11 |
+
var blockedTerms []string
|
| 12 |
+
var blockedRegexes []*regexp.Regexp
|
| 13 |
+
|
| 14 |
+
func InitModeration() error {
|
| 15 |
+
file, err := os.Open("moderation/words.txt")
|
| 16 |
+
if err != nil {
|
| 17 |
+
log.Println("Moderation words file not found, creating default...")
|
| 18 |
+
return createDefaultModerationFile()
|
| 19 |
+
}
|
| 20 |
+
defer file.Close()
|
| 21 |
+
|
| 22 |
+
scanner := bufio.NewScanner(file)
|
| 23 |
+
for scanner.Scan() {
|
| 24 |
+
line := strings.TrimSpace(scanner.Text())
|
| 25 |
+
if line == "" || strings.HasPrefix(line, "#") {
|
| 26 |
+
continue
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
blockedTerms = append(blockedTerms, strings.ToLower(line))
|
| 30 |
+
|
| 31 |
+
// Compile as regex for more flexible matching
|
| 32 |
+
regex, err := regexp.Compile("(?i)" + regexp.QuoteMeta(line))
|
| 33 |
+
if err == nil {
|
| 34 |
+
blockedRegexes = append(blockedRegexes, regex)
|
| 35 |
+
}
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
log.Printf("Loaded %d moderation terms", len(blockedTerms))
|
| 39 |
+
return scanner.Err()
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
func createDefaultModerationFile() error {
|
| 43 |
+
err := os.MkdirAll("moderation", 0755)
|
| 44 |
+
if err != nil {
|
| 45 |
+
return err
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
defaultTerms := []string{
|
| 49 |
+
"# Moderation Terms - one per line",
|
| 50 |
+
"# Lines starting with # are comments",
|
| 51 |
+
"spam",
|
| 52 |
+
"scam",
|
| 53 |
+
"hate",
|
| 54 |
+
"abuse",
|
| 55 |
+
"violence",
|
| 56 |
+
"suicide",
|
| 57 |
+
"self-harm",
|
| 58 |
+
"drugs",
|
| 59 |
+
"illegal",
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
file, err := os.Create("moderation/words.txt")
|
| 63 |
+
if err != nil {
|
| 64 |
+
return err
|
| 65 |
+
}
|
| 66 |
+
defer file.Close()
|
| 67 |
+
|
| 68 |
+
for _, term := range defaultTerms {
|
| 69 |
+
file.WriteString(term + "\n")
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
log.Println("Created default moderation file")
|
| 73 |
+
return nil
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
func CheckContent(content string) bool {
|
| 77 |
+
if len(blockedRegexes) == 0 {
|
| 78 |
+
return false
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
contentLower := strings.ToLower(content)
|
| 82 |
+
|
| 83 |
+
for _, regex := range blockedRegexes {
|
| 84 |
+
if regex.MatchString(contentLower) {
|
| 85 |
+
log.Printf("Content flagged by moderation: matched pattern")
|
| 86 |
+
return true
|
| 87 |
+
}
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
return false
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
func GetBlockedTermsCount() int {
|
| 94 |
+
return len(blockedTerms)
|
| 95 |
+
}
|
services/push_notification.go
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package services
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"context"
|
| 5 |
+
"log"
|
| 6 |
+
"moodlink-backend/database"
|
| 7 |
+
"moodlink-backend/models"
|
| 8 |
+
"os"
|
| 9 |
+
"time"
|
| 10 |
+
|
| 11 |
+
firebase "firebase.google.com/go/v4"
|
| 12 |
+
"firebase.google.com/go/v4/messaging"
|
| 13 |
+
"github.com/google/uuid"
|
| 14 |
+
"google.golang.org/api/option"
|
| 15 |
+
)
|
| 16 |
+
|
| 17 |
+
var fcmClient *messaging.Client
|
| 18 |
+
|
| 19 |
+
func InitFCM() error {
|
| 20 |
+
serviceAccountPath := os.Getenv("FIREBASE_SERVICE_ACCOUNT_PATH")
|
| 21 |
+
if serviceAccountPath == "" {
|
| 22 |
+
log.Println("Firebase service account path not configured, push notifications disabled")
|
| 23 |
+
return nil
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
opt := option.WithCredentialsFile(serviceAccountPath)
|
| 27 |
+
app, err := firebase.NewApp(context.Background(), nil, opt)
|
| 28 |
+
if err != nil {
|
| 29 |
+
return err
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
fcmClient, err = app.Messaging(context.Background())
|
| 33 |
+
if err != nil {
|
| 34 |
+
return err
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
log.Println("FCM initialized successfully")
|
| 38 |
+
return nil
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
func SendSupportNotification(userID uuid.UUID, supportType string) {
|
| 42 |
+
if fcmClient == nil {
|
| 43 |
+
return
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
var tokens []models.DeviceToken
|
| 47 |
+
database.DB.Where("user_id = ?", userID).Find(&tokens)
|
| 48 |
+
|
| 49 |
+
if len(tokens) == 0 {
|
| 50 |
+
return
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
var fcmTokens []string
|
| 54 |
+
for _, token := range tokens {
|
| 55 |
+
fcmTokens = append(fcmTokens, token.Token)
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
message := &messaging.MulticastMessage{
|
| 59 |
+
Notification: &messaging.Notification{
|
| 60 |
+
Title: "Dukungan Diterima! 💝",
|
| 61 |
+
Body: getSupportMessage(supportType),
|
| 62 |
+
},
|
| 63 |
+
Data: map[string]string{
|
| 64 |
+
"type": "support_received",
|
| 65 |
+
"support": supportType,
|
| 66 |
+
},
|
| 67 |
+
Tokens: fcmTokens,
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
response, err := fcmClient.SendMulticast(context.Background(), message)
|
| 71 |
+
if err != nil {
|
| 72 |
+
log.Printf("Failed to send FCM message: %v", err)
|
| 73 |
+
return
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
log.Printf("Successfully sent FCM message. Success: %d, Failure: %d",
|
| 77 |
+
response.SuccessCount, response.FailureCount)
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
func SendInactivityReminder(userID uuid.UUID) {
|
| 81 |
+
if fcmClient == nil {
|
| 82 |
+
return
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
var tokens []models.DeviceToken
|
| 86 |
+
database.DB.Where("user_id = ?", userID).Find(&tokens)
|
| 87 |
+
|
| 88 |
+
if len(tokens) == 0 {
|
| 89 |
+
return
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
var fcmTokens []string
|
| 93 |
+
for _, token := range tokens {
|
| 94 |
+
fcmTokens = append(fcmTokens, token.Token)
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
message := &messaging.MulticastMessage{
|
| 98 |
+
Notification: &messaging.Notification{
|
| 99 |
+
Title: "Jangan Lupa Check-in! 🌟",
|
| 100 |
+
Body: "Bagaimana perasaanmu hari ini? Yuk ceritakan di MoodLink!",
|
| 101 |
+
},
|
| 102 |
+
Data: map[string]string{
|
| 103 |
+
"type": "inactivity_reminder",
|
| 104 |
+
},
|
| 105 |
+
Tokens: fcmTokens,
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
response, err := fcmClient.SendMulticast(context.Background(), message)
|
| 109 |
+
if err != nil {
|
| 110 |
+
log.Printf("Failed to send inactivity reminder: %v", err)
|
| 111 |
+
return
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
log.Printf("Successfully sent inactivity reminder. Success: %d, Failure: %d",
|
| 115 |
+
response.SuccessCount, response.FailureCount)
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
func getSupportMessage(supportType string) string {
|
| 119 |
+
switch supportType {
|
| 120 |
+
case "heart":
|
| 121 |
+
return "Seseorang memberikan ❤️ untuk journalmu!"
|
| 122 |
+
case "hug":
|
| 123 |
+
return "Seseorang memberikan 🤗 untuk journalmu!"
|
| 124 |
+
case "pray":
|
| 125 |
+
return "Seseorang memberikan 🙏 untuk journalmu!"
|
| 126 |
+
default:
|
| 127 |
+
return "Seseorang memberikan dukungan untuk journalmu!"
|
| 128 |
+
}
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
// Background job to send inactivity reminders
|
| 132 |
+
func StartInactivityChecker() {
|
| 133 |
+
go func() {
|
| 134 |
+
ticker := time.NewTicker(24 * time.Hour)
|
| 135 |
+
defer ticker.Stop()
|
| 136 |
+
|
| 137 |
+
for range ticker.C {
|
| 138 |
+
checkInactiveUsers()
|
| 139 |
+
}
|
| 140 |
+
}()
|
| 141 |
+
}
|
| 142 |
+
|
| 143 |
+
func checkInactiveUsers() {
|
| 144 |
+
yesterday := time.Now().AddDate(0, 0, -1)
|
| 145 |
+
|
| 146 |
+
var inactiveUsers []models.AnonymousUser
|
| 147 |
+
database.DB.Where("last_checkin IS NULL OR last_checkin < ?", yesterday).Find(&inactiveUsers)
|
| 148 |
+
|
| 149 |
+
for _, user := range inactiveUsers {
|
| 150 |
+
SendInactivityReminder(user.ID)
|
| 151 |
+
}
|
| 152 |
+
|
| 153 |
+
log.Printf("Checked %d inactive users for reminders", len(inactiveUsers))
|
| 154 |
+
}
|
utils/response.go
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package utils
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"github.com/gofiber/fiber/v2"
|
| 5 |
+
)
|
| 6 |
+
|
| 7 |
+
type Response struct {
|
| 8 |
+
Status string `json:"status"`
|
| 9 |
+
Message string `json:"message,omitempty"`
|
| 10 |
+
Data interface{} `json:"data,omitempty"`
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
func SuccessResponse(c *fiber.Ctx, data interface{}) error {
|
| 14 |
+
return c.JSON(Response{
|
| 15 |
+
Status: "success",
|
| 16 |
+
Data: data,
|
| 17 |
+
})
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
func ErrorResponse(c *fiber.Ctx, status int, message string) error {
|
| 21 |
+
return c.Status(status).JSON(Response{
|
| 22 |
+
Status: "error",
|
| 23 |
+
Message: message,
|
| 24 |
+
})
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
func MessageResponse(c *fiber.Ctx, message string) error {
|
| 28 |
+
return c.JSON(Response{
|
| 29 |
+
Status: "success",
|
| 30 |
+
Message: message,
|
| 31 |
+
})
|
| 32 |
+
}
|