Stack: Spring Boot · MongoDB · JWT Authentication · Google Gemini AI · OpenWeather API
Package: capstoneBackend.ca.sheridancollege
Make sure you have the following installed:
- Java 21
- Maven
- MongoDB (local) or a MongoDB Atlas connection string
- A code editor (IntelliJ IDEA recommended)
git clone (https://github.com/amninderkaur/FASHIONAPP.git)
git checkout backend
cd ca.sheridancollegeYou need the following before running the app:
| Key | Where to get it |
|---|---|
MONGODB_URI |
MongoDB Atlas → Create cluster → Connect → Copy connection string |
GEMINI_API_KEY |
Google AI Studio → Get API Key |
OPENWEATHER_API_KEY |
OpenWeatherMap → Sign up → API Keys tab |
GOOGLE_PLACES_API_KEY |
Google Cloud Console → APIs & Services → Credentials → Enable Places API (New) |
MAIL_USERNAME |
Gmail address used to send emails |
MAIL_PASSWORD |
Gmail App Password (not your login password) — Generate one here |
TWILIO_ACCOUNT_SID |
Twilio Console → Account Info |
TWILIO_AUTH_TOKEN |
Twilio Console → Account Info |
TWILIO_PHONE_NUMBER |
Twilio Console → Phone Numbers — must be in E.164 format |
JWT_SECRET |
Any Base64-encoded secret string — a default is provided for local dev |
Create a file called .env or set these as environment variables on your machine. Never commit these to Git.
MONGODB_URI=mongodb+srv://<username>:<password>@cluster.mongodb.net/fashionapp
GEMINI_API_KEY=your_gemini_api_key_here
OPENWEATHER_API_KEY=your_openweather_api_key_here
MAIL_USERNAME=your_gmail@gmail.com
MAIL_PASSWORD=your_gmail_app_password
TWILIO_ACCOUNT_SID=your_twilio_account_sid
TWILIO_AUTH_TOKEN=your_twilio_auth_token
TWILIO_PHONE_NUMBER=+1XXXXXXXXXX
GOOGLE_PLACES_API_KEY=your_google_places_api_key_here
Option A — IntelliJ (recommended):
- Run → Edit Configurations → Your Spring Boot config
- Click Modify options → Environment variables
- Paste all variables
Option B — Terminal (Mac/Linux):
export MONGODB_URI="mongodb+srv://..."
export GEMINI_API_KEY="..."
export OPENWEATHER_API_KEY="..."Option B — Terminal (Windows):
set MONGODB_URI=mongodb+srv://...
set GEMINI_API_KEY=...
set OPENWEATHER_API_KEY=...mvn clean installmvn spring-boot:runThe server starts on http://localhost:8080
GET http://localhost:8080/api/profile
You should get a 401 Unauthorized — that means the server is up and JWT auth is working.
- Max file upload size is 10MB (configured in
application.properties) - Wardrobe images are stored as base64 in MongoDB — no local file system required
- Clothing detection and image generation use Gemini Vision + Imagen via the
GEMINI_API_KEY JWT_SECREThas a default value inapplication.propertiesso you don't need to set it locally- JWT tokens expire after 24 hours (configurable via
jwt.expiration.msinapplication.properties) - OTP codes expire after 10 minutes — use
/resend-otpto get a fresh one (configurable viaotp.expiry.minutes)
| Environment | URL |
|---|---|
| Local | http://localhost:8080 |
| Production (Azure) | https://fashionapp-backend-gtatg0hjbwh4c2dk.canadacentral-01.azurewebsites.net |
All endpoints except Register and Login require a JWT Bearer token:
Authorization: Bearer <token>
Get the token from the login or verify-otp response and include it in every request header.
POST /api/v1/auth/register
Body:
{
"email": "jane@example.com",
"password": "password123",
"name": "Jane",
"phoneNumber": "+16471234567",
"deliveryMethod": "email"
}
nameis the user's display name — collected at sign-up.
phoneNumberis optional. Required only ifdeliveryMethodis"sms". Must be in E.164 format.
deliveryMethodis"email"(default) or"sms".
Response:
{ "message": "Registration successful. Please log in." }POST /api/v1/auth/authenticate
Body:
{
"email": "jane@example.com",
"password": "password123",
"deliveryMethod": "email"
}Response — first login (OTP required):
{
"message": "OTP sent",
"requiresOtp": true,
"deliveryMethod": "email"
}Response — returning user:
{
"token": "eyJhbGci...",
"userId": "abc123",
"role": "USER",
"name": "Jane",
"profilePicture": "<base64-encoded image or null>",
"profilePictureType": "image/jpeg",
"requiresOtp": false
}POST /api/v1/auth/verify-otp
Body:
{
"email": "jane@example.com",
"otp": "123456"
}Response:
{
"token": "eyJhbGci...",
"userId": "abc123"
}POST /api/v1/auth/resend-otp
Generates and sends a fresh OTP. The previous OTP is immediately invalidated. Use this when the 10-minute window has expired.
Body:
{
"email": "jane@example.com"
}Response:
{ "message": "A new OTP has been sent" }POST /api/v1/auth/forgot-password
Sends an OTP to the user's email to begin the password reset flow. The response is identical whether or not the email exists (to avoid revealing registered accounts).
Body:
{
"email": "jane@example.com"
}Response:
{ "message": "If that email is registered, an OTP has been sent" }POST /api/v1/auth/reset-password
Verifies the OTP from the forgot-password step, then sets the new password.
Body:
{
"email": "jane@example.com",
"otp": "123456",
"newPassword": "newSecurePassword"
}
newPasswordmust be at least 6 characters.
Response:
{ "message": "Password reset successful. Please log in." }POST /api/profile
Body:
{
"displayName": "Jane",
"ageGroup": "18–24",
"gender": "Women",
"styles": ["Casual", "Streetwear"],
"favoriteColors": ["Black", "White"],
"colorsToAvoid": ["Neon Yellow"],
"shoppingFor": ["Tops", "Bottoms", "Shoes"],
"preferredFit": ["Oversized", "Regular"],
"preferredFabrics": ["Cotton", "Denim"],
"topSize": "S",
"bottomSize": "28",
"shoeSize": "7",
"fitConcerns": ["Petite"],
"dressFor": ["College/School", "Casual outings"],
"climate": "Mixed seasons",
"budgetPerItem": "$25–$50",
"shoppingFrequency": "Monthly",
"shoppingPriorities": ["Price", "Quality"],
"favoriteBrands": ["Zara", "H&M"],
"brandsToAvoid": [],
"recommendationBases": ["Current weather", "AI outfit generation"],
"styleNotifications": true
}Response: Saved UserProfile document with id and userId
GET /api/profile
Response: Saved UserProfile document
PATCH /api/profile/preferences
Body:
{
"genderAesthetic": "feminine",
"modestyLevel": "high",
"culturalPreferences": ["modest coverage", "no sleeveless"]
}Valid values:
genderAesthetic:"masculine"|"feminine"|"androgynous"|"mixed"modestyLevel:"low"|"medium"|"high"|"full"
Response:
{
"message": "Preferences updated successfully",
"genderAesthetic": "feminine",
"modestyLevel": "high",
"culturalPreferences": ["modest coverage", "no sleeveless"]
}GET /api/wardrobe
Response: Array of ClothingItem documents, each with id, userId, tags (type, color, style, occasion), generatedImageBase64, and createdAt.
POST /api/wardrobe/add
Content-Type: multipart/form-data
Sends the photo to Gemini Vision which detects all clothing items in the image, then Imagen generates a clean product photo for each one. All detected items are saved to the wardrobe in one request.
| Field | Type | Description |
|---|---|---|
file |
File | JPEG, PNG, or WebP image — can contain multiple clothing items |
Response:
{
"message": "Wardrobe updated successfully",
"itemsAdded": 2,
"items": [
{
"id": "abc123",
"tags": { "type": "jacket", "color": "black", "style": "casual", "occasion": ["casual", "work"] },
"generatedImageBase64": "<base64 PNG>",
"createdAt": "2025-01-01T00:00:00.000Z"
}
]
}If no clothing is detected:
{ "message": "No clothing items detected in the photo", "items": [] }DELETE /api/wardrobe/{id}
Permanently removes the item from the user's wardrobe. Returns 404 if not found or belongs to another user.
Response: 204 No Content
POST /api/outfit/suggest
Fetches live weather for the city, selects the best outfit from the user's wardrobe using Gemini AI, and respects saved style preferences.
Body:
{
"occasion": "work",
"city": "Toronto"
}Response:
{
"selectedItems": [
{ "itemId": "abc123", "type": "shirt", "color": "white", "imageBase64": "..." }
],
"reasoning": "This outfit works well for a work occasion in cool weather.",
"weatherSummary": "12.0°C, light rain in Toronto"
}POST /api/outfit/history
Only saves outfits the user explicitly likes. Nothing is auto-saved. Send the suggestion response body plus occasion and city.
Body:
{
"occasion": "work",
"city": "Toronto",
"weatherSummary": "12.0°C, light rain in Toronto",
"reasoning": "Great layered look for the weather.",
"selectedItems": [
{ "itemId": "abc123", "type": "shirt", "color": "white", "imageBase64": "..." }
]
}Response: Saved OutfitHistory document with id and savedAt
GET /api/outfit/history
Returns all outfits the user has saved, newest first.
Response: Array of OutfitHistory documents
DELETE /api/outfit/history/{id}
Response: 204 No Content
POST /api/outfit/analyze
Content-Type: multipart/form-data
Uploads a photo and uses Gemini Vision to evaluate the outfit against weather, style preferences, modesty, and colour season.
| Field | Type | Required | Description |
|---|---|---|---|
image |
File | Yes | JPEG, PNG, or WebP photo of the outfit |
occasion |
String | Yes | What the user is dressing for e.g. "job interview", "casual brunch", "wedding" |
city |
String | No | City name for live weather e.g. "Toronto" — if omitted, weather evaluation is skipped |
Response:
{
"occasion": "casual brunch",
"styleScore": 7,
"weatherVerdict": "not suitable",
"weatherReason": "It's 4°C and raining — this outfit will leave you cold",
"whatWorksWell": ["The colour palette is cohesive"],
"suggestions": ["Add a warm coat", "Swap sandals for ankle boots"],
"overallVerdict": "Great casual look, but needs layers for today's weather.",
"currentWeather": "Light rain, 4°C, Toronto"
}POST /api/colour/analyze
Content-Type: multipart/form-data
Combines Gemini Vision analysis of the face photo with the user's self-reported details to determine their 12-season colour type. Result is automatically saved to the user's profile.
| Field | Type | Required | Description |
|---|---|---|---|
file |
File | Yes | JPEG, PNG, or WebP photo of the person's face |
naturalHair |
String | No | Natural hair colour e.g. "dark brown" |
currentHair |
String | No | Current/dyed hair colour if different |
eyeColor |
String | No | Eye colour e.g. "hazel" |
jewelry |
String | No | Preferred jewelry e.g. "gold" |
veins |
String | No | Wrist vein colour e.g. "blue-green" |
sunReaction |
String | No | How skin reacts to sun e.g. "tans easily" |
Response:
{
"season": "Soft Autumn",
"undertone": "Warm",
"contrast": "Low",
"bestJewelry": "Gold",
"summary": "You have warm, muted colouring with low contrast between your skin, hair, and eyes — classic Soft Autumn.",
"recommendedColors": ["#C4622D", "#8B5E3C", "#D4A853", "#7A6652", "#A0522D", "#5C4033", "#6B4226", "#B8975A", "#9B7653"]
}GET /api/profile
Returns the full user profile including colourSeason and colourPalette fields. No separate endpoint needed — read these fields from the profile response.
DELETE /api/profile/colour
Removes the saved colour season and palette from the user's profile. All other profile data is kept.
Response:
{ "message": "Colour analysis cleared" }GET /api/trends
Fetches current-season fashion trends from Gemini AI and cross-references the user's wardrobe to find which items they already own that are on-trend. Always fetched fresh — no caching.
Response:
{
"season": "Autumn",
"year": "2025",
"summary": "This season leans into earthy tones and quiet luxury...",
"fetchedAt": "2025-10-01T14:30:00",
"trends": [
{
"trendName": "Quiet Luxury",
"description": "Understated, high-quality pieces with minimal branding.",
"keyPieces": ["cashmere sweater", "tailored trousers", "loafers"],
"colors": ["camel", "cream", "taupe"],
"wearItHow": "Keep it simple — one focal piece, neutral base, clean lines."
}
],
"wardrobeMatches": [
{
"itemId": "abc123",
"itemType": "trousers",
"color": "camel",
"imageBase64": "...",
"matchingTrends": ["Quiet Luxury"],
"stylingTip": "Pair with a cream blouse and loafers for an effortless on-trend look."
}
]
}POST /api/outfit/rating
Rate an outfit from your saved history. Ratings are used to build a personalised taste profile.
Body:
{
"outfitHistoryId": "abc123",
"rating": 5
}
ratingmust be an integer from1to5.
Response: Saved OutfitRating document with id, ratedAt, and extracted metadata (occasion, colors, styles).
GET /api/outfit/rating
Returns all of the user's outfit ratings, newest first.
Response: Array of OutfitRating documents
GET /api/outfit/rating/taste-profile
Returns an AI-derived taste profile based on the user's ratings. Requires at least 3 ratings.
Response (enough ratings):
{
"userId": "abc123",
"lovedCombinations": ["white shirt + tailored trousers"],
"dislikedCombinations": ["oversized hoodie + joggers"],
"favoriteColors": ["white", "camel", "black"],
"avoidedColors": ["neon green"],
"favoriteStyles": ["casual", "business casual"],
"favoriteOccasions": ["work", "brunch"],
"totalRatings": 7,
"averageRating": 4.3,
"lastUpdated": "2025-10-01T14:00:00"
}Response (not enough ratings yet):
{ "message": "Rate at least 3 outfits to unlock your taste profile" }DELETE /api/outfit/rating/{id}
Removes the rating and rebuilds the taste profile automatically.
Response: 204 No Content
POST /api/packing/suggest
Body:
{
"destination": "Paris, France",
"tripLengthDays": 7,
"activities": ["sightseeing", "fine dining", "museum visits"]
}Response:
{
"destination": "Paris, France",
"weatherSummary": "14.0°C, overcast clouds in Paris",
"packingList": {
"tops": ["Light knit sweater x3", "Long sleeve shirt x2"],
"bottoms": ["Tailored trousers x2", "Dark jeans x1"],
"outerwear": ["Trench coat x1"],
"shoes": ["Comfortable walking flats x1", "Ankle boots x1"],
"accessories": ["Scarf x1", "Compact umbrella x1"],
"extras": ["Adapter plug"]
},
"tips": "Pack neutral colours that mix and match easily."
}POST /api/shopping/suggest
Uses Gemini AI with Google Search grounding to find real products in Canada. Automatically uses all profile preferences — styles, colours, fit, fabrics, brands, modesty, budget per item, and more.
Body:
{
"destination": "Canada",
"budget": 200.00,
"currency": "CAD",
"location": "Toronto, Ontario",
"focusCategory": "outerwear",
"preferOnline": false
}| Field | Type | Description |
|---|---|---|
budget |
Number | Total budget for all suggestions |
currency |
String | e.g. "CAD", "USD" |
location |
String | City for nearby store suggestions |
focusCategory |
String | Optional — target a specific category. If empty, gaps are auto-detected from wardrobe |
preferOnline |
Boolean | true = online only, false = include physical stores |
Response:
{
"season": "Autumn",
"gapsIdentified": ["outerwear", "shoes"],
"totalEstimate": "$185.00 CAD",
"withinBudget": true,
"suggestions": [
{
"item": "Camel wool trench coat",
"category": "outerwear",
"whyItFits": "Earthy camel tone matches your Autumn palette",
"estimatedPrice": "$120 CAD",
"storeName": "Zara Canada",
"storeType": "Online + In-store",
"link": "https://www.zara.com/ca/...",
"nearbyLocation": "Eaton Centre, Toronto"
}
]
}POST /api/shopping/saved
Save a suggestion the user likes (triggered by the Save button on each result).
Body: Any suggestion object from the shopping response:
{
"item": "Camel wool trench coat",
"category": "outerwear",
"whyItFits": "Earthy camel tone matches your Autumn palette",
"estimatedPrice": "$120 CAD",
"storeName": "Zara Canada",
"storeType": "Online + In-store",
"link": "https://www.zara.com/ca/...",
"nearbyLocation": "Eaton Centre, Toronto"
}Response: Saved SavedShoppingItem document with id and savedAt
GET /api/shopping/saved
Returns all saved items, newest first.
Response: Array of SavedShoppingItem documents
DELETE /api/shopping/saved/{id}
Response: 204 No Content
GET /api/location/autocomplete?q=<query>
Returns location name suggestions using the Google Places API (New). Used to autocomplete city/location input fields across the app (outfit suggestions, packing, shopping).
| Parameter | Type | Description |
|---|---|---|
q |
String | Partial location string e.g. "Toron" |
Response:
["Toronto, Ontario, Canada", "Toronto, Ohio, US", "Toronto, Kansas, US"]POST /api/chat
Multi-turn Gemini-powered fashion assistant. Send the full conversation history with each request.
Body:
{
"message": "What should I wear to a casual brunch?",
"history": []
}Follow-up:
{
"message": "What shoes go with that?",
"history": [
{ "role": "user", "text": "What should I wear to a casual brunch?" },
{ "role": "model", "text": "I'd suggest a linen blouse with straight-leg jeans..." }
]
}Response:
{ "reply": "White sneakers or loafers would work perfectly..." }POST /api/v1/reviews
Body:
{
"message": "Love the outfit suggestions!",
"rating": 5
}A confirmation email with the case number is sent to the user automatically.
GET /api/v1/admin/reviews
POST /api/v1/admin/reviews/{caseNumber}/reply
Body:
{ "reply": "Thank you for your feedback!" }GET /api/v1/admin/users
Response: Array of { id, email, name, role, phoneNumber }
GET /api/v1/admin/users/count
Response:
{ "totalUsers": 25, "users": 23, "admins": 2 }GET /api/v1/admin/stats
Response:
{
"totalUsers": 25,
"totalClothingItems": 142,
"totalOutfitsGenerated": 87,
"totalReviews": 12,
"totalSavedItems": 56
}PATCH /api/v1/admin/users/{id}
Body (any subset):
{ "name": "Jane", "email": "jane@example.com", "phoneNumber": "+16471234567", "role": "ADMIN" }Valid role values: USER, ADMIN
DELETE /api/v1/admin/users/{id}
Deletes the user and all their data (clothing, outfit history, saved shopping, profile, OTP tokens).
Response: 204 No Content
POST /api/v1/admin/users/{id}/email
Body:
{ "subject": "Important update", "content": "Hi Jane, just wanted to let you know..." }Response:
{ "message": "Email sent to jane@example.com" }GET /api/v1/user/me
Response:
{
"userId": "abc123",
"email": "jane@example.com",
"name": "Jane",
"profilePicture": "<base64-encoded image or empty string>",
"profilePictureType": "image/jpeg"
}PATCH /api/v1/user/me
Updates name, phone number, and/or OTP delivery method. Send only the fields you want to change.
Body (any subset):
{
"name": "Jane Smith",
"phoneNumber": "+16471234567",
"deliveryMethod": "sms"
}
deliveryMethodaccepts"email"or"sms".
Response:
{
"name": "Jane Smith",
"phoneNumber": "+16471234567",
"deliveryMethod": "sms"
}POST /api/v1/user/me/change-password
Verifies the current password, sets the new one, sends a security alert email, and re-triggers OTP verification on next login.
Body:
{
"currentPassword": "oldPassword123",
"newPassword": "newSecurePassword"
}
newPasswordmust be at least 6 characters.
Response:
{ "message": "Password changed. An OTP has been sent to verify your identity." }POST /api/v1/user/me/profile-picture
Content-Type: multipart/form-data
| Field | Type | Description |
|---|---|---|
file |
File | JPEG, PNG, or WebP — stored as binary in MongoDB |
Response:
{
"message": "Profile picture updated",
"profilePictureType": "image/jpeg"
}DELETE /api/v1/user/me
Permanently deletes the user's account and all associated data — wardrobe, outfit history, saved shopping items, profile, OTP tokens. A farewell email is sent to the user's registered address automatically.
Response: 204 No Content
| Status | Meaning |
|---|---|
400 |
Bad request — missing or invalid fields |
401 |
Unauthorized — missing, invalid, or expired JWT |
403 |
Forbidden — valid token but accessing another user's resource |
404 |
Resource not found |
405 |
Wrong HTTP method |
500 |
Server error — check Azure logs |