chandafa commited on
Commit
1b18b9e
·
1 Parent(s): 56f5655

deployment v0.1

Browse files
.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
- title: Moodlink Gofiber
3
- emoji: 😻
4
- colorFrom: purple
5
- colorTo: blue
6
- sdk: docker
7
- pinned: false
8
- ---
9
-
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ }