An ASP.NET Core backend for SkinScan, a mobile platform that provides AI-powered preliminary assessment of skin conditions from user-uploaded skin images. The backend handles authentication, prediction management, scan history, medical recommendations, and dermatologist discovery across all 27 Egyptian governorates.
Graduation Project — Faculty of Computers & Informatics, Zagazig University (2025–2026)
| Live | Link |
|---|---|
| API / Swagger | https://skinscan.runasp.net/swagger |
| Web Demo | https://skinscan-eg.vercel.app |
The following diagram provides a high-level overview of the SkinScan system architecture and illustrates how the Flutter mobile application, ASP.NET Core backend, external services, AI inference service, and SQL Server database interact during a prediction request.
| Feature | Description |
|---|---|
| JWT Authentication | Short-lived access tokens (30 min) with rotating refresh tokens (14 days) |
| Account Lockout | 5 consecutive failed login attempts locks the account for 5 minutes |
| Email Confirmation | Required before login, with resend support |
| OTP Password Reset | 6-digit CSPRNG code, salted hash, 3-minute expiry, 5-attempt limit, 2-minute resend cooldown |
| AI Skin Prediction | Uploaded images are sent to an external AI inference service, returning the top 5 predictions ranked by confidence |
| Parallel Processing | Image upload (Cloudinary) and AI prediction run concurrently via Task.WhenAll |
| Orphaned Resource Cleanup | Cloudinary image is automatically deleted if the AI call fails after a successful upload |
| GradCAM & TTA | Optional heatmap overlay and test-time augmentation via query parameters |
| Curated Disease Recommendations | 35 seeded entries with description, severity, symptoms, do's/don'ts, and doctor-consultation guidance |
| Scan History | Full CRUD — list, detail, delete (cascades to Cloudinary asset removal) |
| Find a Doctor | Dermatologist directory across all 27 Egyptian governorates, filterable by location |
| Result Pattern | Typed errors (Result<T> / Error) mapped to RFC 7807 ProblemDetails |
| Validation | FluentValidation with automatic validation middleware on every request DTO |
| Structured Logging | Serilog with request logging middleware |
| Swagger / OpenAPI | Enabled in both Development and Production environments |
SkinScan.Api/
├── Controllers/
│ ├── AuthController.cs # Auth flows (login, register, refresh, confirm email, OTP reset)
│ ├── AccountController.cs # Profile management (get, update, change password)
│ ├── PredictionController.cs # AI skin prediction endpoint (image upload, top-5 results)
│ ├── ScansController.cs # Scan history CRUD (list, detail, delete)
│ ├── DoctorsController.cs # Dermatologist directory, filterable by governorate
│ └── GovernoratesController.cs # Governorate reference data
│
├── Services/
│ ├── AuthService.cs # JWT, refresh token rotation, OTP-based password reset
│ ├── UserService.cs # Profile retrieval, update, password change
│ ├── PredictionService.cs # AI model orchestration, image + prediction pipeline
│ ├── ScanService.cs # Scan CRUD, cascade delete of Cloudinary assets
│ ├── DoctorService.cs # Doctor directory retrieval & governorate filtering
│ ├── ImageService.cs # File upload/delete via Cloudinary
│ └── EmailService.cs # SMTP email sending via MailKit
│
├── Authentication/
│ └── JwtProvider.cs # JWT generation & token validation
│
├── Clients/
│ └── IScanDiseaseClient.cs # Refit client for the AI model (Hugging Face)
│
├── Persistance/
│ ├── ApplicationDbContext.cs
│ ├── Migrations/
│ └── Seeders/ # DiseaseRecommendationSeeder, DoctorSeeder, GovernorateSeeder
│
├── Contracts/ # Request & Response DTOs + FluentValidation validators
├── Entities/ # Domain models (User, Scan, Doctor, Governorate, ...)
├── Errors/ # Typed error classes + GlobalExceptionHandler
├── Helpers/ # Email templates, HTML response builders
├── Extensions/ # Claims extraction, other helpers
├── Mapping/ # Mapster configuration
├── Settings/ # Strongly-typed configuration (Options Pattern)
├── DependencyInjection.cs
├── GlobalUsings.cs
└── Program.cs
| Entity | Description |
|---|---|
ApplicationUser |
Extends ASP.NET Identity — stores first/last name, gender, phone |
RefreshToken |
Owned entity — rotating refresh tokens with expiry & revocation tracking |
PasswordResetOtp |
Hashed OTP record with expiry and failed-attempt counter |
Scan |
A single skin scan — image URL, top prediction, confidence, timestamp |
ScanPrediction |
One of the top-5 predictions belonging to a Scan |
DiseaseRecommendation |
Seeded medical reference data for all 35 detectable disease classes |
Governorate |
Reference data — all 27 Egyptian governorates |
Doctor |
Dermatologist profile — name, address, phone, WhatsApp, linked governorate |
- .NET 10 SDK
- SQL Server (LocalDB or full instance)
- A Cloudinary account
- A Gmail account (or other SMTP provider) for transactional email
- Access to a compatible external AI prediction service (the production system uses one hosted on Hugging Face Spaces)
git clone https://github.com/a7medhazem/SkinScan.git
cd SkinScan{
"ConnectionStrings": {
"DefaultConnection": "Server=(localdb)\\MSSQLLocalDB;Database=SkinScan;Trusted_Connection=True;Encrypt=False"
},
"Jwt": {
"key": "YOUR_SUPER_SECRET_KEY_MIN_32_CHARS",
"Issuer": "SkinScan",
"Audience": "SkinScan users",
"ExpiryMinutes": 30
},
"EmailSettings": {
"Mail": "your@gmail.com",
"DisplayName": "Skin Scan",
"Password": "your-app-password",
"Host": "smtp.gmail.com",
"Port": 587
},
"AppSettings": {
"BaseUrl": "https://your-domain.com"
},
"SkinDiseaseApi": {
"BaseUrl": "https://your-model-endpoint",
"PredictEndpoint": "/predict",
"TimeoutSeconds": 180,
"ApiKey": "your-model-api-key"
},
"Cloudinary": {
"CloudName": "your_cloud_name",
"ApiKey": "your_api_key",
"ApiSecret": "your_api_secret"
}
}dotnet ef database update --project SkinScan.ApiOn first run, UseSeedDataAsync() automatically seeds the disease recommendation database, all 27 governorates, and the dermatologist directory.
dotnet run --project SkinScan.ApiSwagger UI: https://localhost:{port}/swagger
| Method | Endpoint | Auth | Description |
|---|---|---|---|
| POST | /Auth |
— | Login with email & password |
| POST | /Auth/register |
— | Register a new account |
| GET | /Auth/confirm-email |
— | Confirm email via link token |
| POST | /Auth/resend-confirmation-email |
— | Resend confirmation email |
| POST | /Auth/check-email-confirmation |
— | Check whether an email is confirmed |
| POST | /Auth/refresh |
— | Rotate access & refresh tokens |
| POST | /Auth/revoke-refresh-token |
— | Logout / revoke refresh token |
| POST | /Auth/forget-password |
— | Send OTP reset code to email |
| POST | /Auth/verify-otp |
— | Verify OTP and receive a reset token |
| POST | /Auth/reset-password |
— | Reset password using the reset token |
| POST | /Auth/resend-otp |
— | Resend OTP (2-minute cooldown) |
| Method | Endpoint | Description |
|---|---|---|
| GET | /Account/profile |
Get current user profile |
| PUT | /Account/update-profile |
Update first name, last name, phone |
| PUT | /Account/change-password |
Change account password |
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/Prediction/predict?gradcam={bool}&tta={bool} |
Upload a skin image and receive top-5 AI predictions + recommendation |
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/Scans |
List the current user's scan history |
| GET | /api/Scans/{scanId} |
Get full details of a specific scan |
| DELETE | /api/Scans/{scanId} |
Delete a scan and its Cloudinary image |
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/Governorates |
List all 27 Egyptian governorates |
| GET | /api/Doctors |
List all dermatologists |
| GET | /api/Doctors?governorateId={id} |
List dermatologists filtered by governorate |
| GET | /api/Doctors/{id} |
Get details of a specific doctor |
Token Strategy
- Access Token — JWT, HMAC-SHA256, 30-minute expiry,
ClockSkew = TimeSpan.Zero(no tolerance window) - Refresh Token — 14-day expiry, rotated on every use, revoked on logout
ValidateLifetime = falseis set intentionally on the refresh endpoint, so an already-expired access token can still be presented to trigger rotation JWT Claims
sub → User ID
email → User email
given_name → First name
family_name → Last name
jti → Unique token ID
Authentication Flow
Register → Email Confirmation (24h token) → Login → JWT (30 min) + Refresh Token (14 days)
↓
Refresh → Old token revoked → New JWT + New Refresh Token
Login Protection
5 consecutive failed attempts → account locked for 5 minutes
Password Reset Flow
User requests reset
↓
6-digit OTP generated (CSPRNG)
↓
OTP hashed via ASP.NET Core Identity PasswordHasher (salted)
↓
Raw OTP sent via email (Gmail SMTP over STARTTLS)
↓
User submits OTP → verified against hash (3-minute expiry, max 5 attempts)
↓
Reset token issued (valid only after successful OTP verification)
↓
Password updated via ASP.NET Core Identity
Image Upload Security
File validation runs in three layers before a single byte is stored:
FileNameValidator— filename must match safe character regexFileSizeValidator— max 10 MB enforcedImageSignatureValidator— reads magic bytes (file header) to verify actual format — a renamed.exereturns400 Bad Requestregardless of extension
Other Controls
- All authenticated endpoints extract the user ID exclusively from JWT claims — never from client-supplied identifiers (prevents IDOR)
- Users can only access their own scans (
UserIdfilter on every query) - Email existence is never revealed on forgot-password or resend-OTP endpoints (always returns
200 OK) - Passwords hashed with PBKDF2 via ASP.NET Core Identity
- All external communication (SMTP, AI model, Cloudinary) over TLS/HTTPS
- API keys and secrets stored in configuration, never in source control
- Global exception handler returns a generic 500 response — no internal details ever leak to the client
SkinScan integrates with an external AI inference service for skin disease classification. The backend does not host or train the model — it calls the service through a typed client and returns the results to the user.
| Property | Detail |
|---|---|
| Integration | External AI inference service, hosted on Hugging Face Spaces |
| Client | Refit-typed IScanDiseaseClient, 30-second timeout, Bearer-authenticated |
| Request | Skin image (multipart/form-data) |
| Response | Top 5 disease predictions ranked by confidence, matched against a curated recommendation |
| Optional Parameters | tta (Test-Time Augmentation), gradcam (heatmap overlay) |
| Layer | Library |
|---|---|
| Framework | ASP.NET Core 10, C# 13 |
| Data Access | Entity Framework Core 10 (SQL Server) |
| Authentication | ASP.NET Core Identity, System.IdentityModel.Tokens.Jwt |
| Validation | FluentValidation |
| Mapping | Mapster |
| External Services | Refit (AI model client), CloudinaryDotNet (image CDN), MailKit (SMTP email) |
| Logging | Serilog |
| Documentation | Swashbuckle.AspNetCore (Swagger / OpenAPI) |
| Resource | Link | Notes |
|---|---|---|
| Swagger UI | skinscan.runasp.net/swagger | Live API reference |
| Web Client | skinscan-eg.vercel.app | Experimental React/Vite frontend — primary client is the Flutter mobile app |
| Demo Video | Watch on YouTube | Full walkthrough |
Pull requests are welcome. For major changes, please open an issue first.
- Fork the repository
- Create your branch:
git checkout -b feature/your-feature - Commit your changes:
git commit -m 'feat: add some feature' - Push to the branch:
git push origin feature/your-feature - Open a Pull Request
This project is licensed under the MIT License.
Developed as a Graduation Project at the Faculty of Computers and Informatics, Zagazig University (2025–2026).
