Comprehensive guide for Flutter developers to integrate with HR System API
Production: https://api.mohammedzom.online/api
π¦ API Root (/api)
β
βββ π Authentication Required: API Key (x-api-key header)
β
βββ¬β π /auth (Authentication & User Management)
β β
β βββ π Public Endpoints (No Bearer Token Required)
β β βββ POST /auth/register
β β βββ POST /auth/verify-otp
β β βββ POST /auth/login
β β βββ POST /auth/forgot-password
β β βββ POST /auth/verify-forgot-password-otp
β β
β βββ π Protected Endpoints (Bearer Token Required)
β βββ POST /auth/logout
β βββ POST /auth/logout-all
β βββ DELETE /auth/delete-account
β
ββββ π User Endpoints (Bearer Token Required)
βββ GET /user (Get current user profile)
| Method | Endpoint | Auth Level | Description |
|---|---|---|---|
| Authentication - Public | |||
| POST | /auth/register |
API Key Only | Create new account |
| POST | /auth/verify-otp |
API Key Only | Verify OTP code |
| POST | /auth/login |
API Key Only | User login |
| POST | /auth/forgot-password |
API Key Only | Request password reset OTP |
| POST | /auth/verify-forgot-password-otp |
API Key Only | Reset password with OTP |
| Authentication - Protected | |||
| POST | /auth/logout |
API Key + Token | Logout from current device |
| POST | /auth/logout-all |
API Key + Token | Logout from all devices |
| DELETE | /auth/delete-account |
API Key + Token | Delete user account |
| User Management | |||
| GET | /user |
API Key + Token | Get current user profile |
π Layer 1: API Key (Required for ALL requests)
βββ Header: x-api-key: {your-api-key}
π Layer 2: Bearer Token (Required for protected endpoints)
βββ Header: Authorization: Bearer {access_token}
Purpose: Create a new user account
Request:
{
"email": "user@example.com",
"password": "password123",
"login_type": "email"
}Response (201):
{
"success": true,
"message": "Account created successfully. Please verify your OTP code.",
"data": {
"user_id": 1,
"identifier": "user@example.com",
"login_type": "email",
"otp_type": "registration",
"dev_otp": "123456"
}
}Purpose: Verify OTP and activate account
Request:
{
"identifier": "user@example.com",
"code": "123456",
"type": "registration"
}Response (200):
{
"success": true,
"message": "OTP verified successfully.",
"data": {
"user": { "id": 1, "email": "user@example.com", "is_active": true },
"access_token": "1|xxxxx",
"token_type": "Bearer"
}
}Purpose: Authenticate user
Request:
{
"identifier": "user@example.com",
"password": "password123",
"login_type": "email"
}Response (200):
{
"success": true,
"message": "Login successful.",
"data": {
"user": { "id": 1, "email": "user@example.com" },
"access_token": "1|xxxxx",
"token_type": "Bearer"
}
}Purpose: Request password reset OTP
Request:
{
"identifier": "user@example.com",
"type": "forget_password",
"login_type": "email"
}Response (200):
{
"success": true,
"message": "OTP code sent successfully.",
"data": {
"identifier": "user@example.com",
"otp_type": "forget_password",
"dev_otp": "123456"
}
}Purpose: Reset password with OTP
Request:
{
"identifier": "user@example.com",
"code": "123456",
"password": "newpassword123",
"password_confirmation": "newpassword123"
}Response (200):
{
"success": true,
"message": "Password has been reset successfully.",
"data": {
"user": { "id": 1, "email": "user@example.com" },
"access_token": "1|xxxxx",
"token_type": "Bearer"
}
}Purpose: Logout from current device
Headers: Authorization: Bearer {token}
Response (200):
{
"success": true,
"message": "Logout successful.",
"data": null
}Purpose: Logout from all devices
Headers: Authorization: Bearer {token}
Response (200):
{
"success": true,
"message": "Logged out from all devices successfully.",
"data": null
}Purpose: Permanently delete user account
Headers: Authorization: Bearer {token}
Response (200):
{
"success": true,
"message": "Account has been deleted successfully.",
"data": null
}Purpose: Get current user profile
Headers: Authorization: Bearer {token}
Response (200):
{
"success": true,
"message": "User retrieved successfully.",
"data": {
"id": 1,
"email": "user@example.com",
"phone": null,
"is_active": true,
"role": "employee"
}
}- API Endpoints Structure
- Requirements
- Initial Setup
- Creating API Service
- Authentication
- Usage Examples
- Error Handling
- Best Practices
Add these packages to your pubspec.yaml:
dependencies:
flutter:
sdk: flutter
# HTTP Client
dio: ^5.4.0
# State Management (optional)
provider: ^6.1.1
# Secure Storage for tokens
flutter_secure_storage: ^9.0.0
# JSON Serialization
json_annotation: ^4.8.1
dev_dependencies:
build_runner: ^2.4.7
json_serializable: ^6.7.1Then run:
flutter pub getCreate a file lib/core/config/api_config.dart:
class ApiConfig {
// Base URLs
static const String productionUrl = 'https://api.mohammedzom.online';
static const String localUrl = 'http://localhost:8000';
// Current environment
static const String baseUrl = productionUrl; // Change based on environment
// API Key (IMPORTANT!)
static const String apiKey = '';
// Endpoints
static const String loginEndpoint = '/api/auth/login';
static const String registerEndpoint = '/api/auth/register';
static const String logoutEndpoint = '/api/auth/logout';
static const String profileEndpoint = '/api/user/profile';
static const String updateProfileEndpoint = '/api/user/profile';
// Timeouts
static const Duration connectionTimeout = Duration(seconds: 30);
static const Duration receiveTimeout = Duration(seconds: 30);
}Create a file lib/core/models/api_response.dart:
class ApiResponse<T> {
final bool success;
final String message;
final T? data;
final String? errorCode;
ApiResponse({
required this.success,
required this.message,
this.data,
this.errorCode,
});
factory ApiResponse.fromJson(
Map<String, dynamic> json,
T Function(dynamic)? fromJsonT,
) {
return ApiResponse<T>(
success: json['success'] ?? false,
message: json['message'] ?? '',
data: json['data'] != null && fromJsonT != null
? fromJsonT(json['data'])
: json['data'],
errorCode: json['error_code'],
);
}
}Create a file lib/core/services/dio_client.dart:
import 'package:dio/dio.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import '../config/api_config.dart';
class DioClient {
static final DioClient _instance = DioClient._internal();
factory DioClient() => _instance;
late Dio _dio;
final _storage = const FlutterSecureStorage();
DioClient._internal() {
_dio = Dio(
BaseOptions(
baseUrl: ApiConfig.baseUrl,
connectTimeout: ApiConfig.connectionTimeout,
receiveTimeout: ApiConfig.receiveTimeout,
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
// API Key header (IMPORTANT!)
'x-api-key': ApiConfig.apiKey,
},
),
);
// Add interceptors
_dio.interceptors.add(
InterceptorsWrapper(
onRequest: (options, handler) async {
// Add Bearer token if exists
final token = await _storage.read(key: 'auth_token');
if (token != null) {
options.headers['Authorization'] = 'Bearer $token';
}
print('π Request: \${options.method} \${options.path}');
print('π¦ Data: \${options.data}');
return handler.next(options);
},
onResponse: (response, handler) {
print('β
Response: \${response.statusCode}');
return handler.next(response);
},
onError: (error, handler) async {
print('β Error: \${error.response?.statusCode}');
// Handle 401 Unauthorized
if (error.response?.statusCode == 401) {
await _storage.delete(key: 'auth_token');
}
return handler.next(error);
},
),
);
}
Dio get dio => _dio;
Future<void> saveToken(String token) async {
await _storage.write(key: 'auth_token', value: token);
}
Future<String?> getToken() async {
return await _storage.read(key: 'auth_token');
}
Future<void> clearToken() async {
await _storage.delete(key: 'auth_token');
}
}Create a file lib/core/services/auth_service.dart:
import 'package:dio/dio.dart';
import '../config/api_config.dart';
import '../models/api_response.dart';
import 'dio_client.dart';
class AuthService {
final DioClient _dioClient = DioClient();
/// Login
Future<ApiResponse<Map<String, dynamic>>> login({
required String identifier, // email or phone
required String password,
required String loginType, // 'email' or 'phone'
String? fcmToken,
}) async {
try {
final response = await _dioClient.dio.post(
ApiConfig.loginEndpoint,
data: {
'identifier': identifier,
'password': password,
'login_type': loginType,
if (fcmToken != null) 'fcm_token': fcmToken,
},
);
final apiResponse = ApiResponse<Map<String, dynamic>>.fromJson(
response.data,
(data) => data as Map<String, dynamic>,
);
// Save token if success
if (apiResponse.success && apiResponse.data?['token'] != null) {
await _dioClient.saveToken(apiResponse.data!['token']);
}
return apiResponse;
} on DioException catch (e) {
return ApiResponse(
success: false,
message: e.response?.data['message'] ?? 'Network error',
errorCode: e.response?.data['error_code'],
);
}
}
/// Get user profile
Future<ApiResponse<Map<String, dynamic>>> getProfile() async {
try {
final response = await _dioClient.dio.get(
ApiConfig.profileEndpoint,
);
return ApiResponse<Map<String, dynamic>>.fromJson(
response.data,
(data) => data as Map<String, dynamic>,
);
} on DioException catch (e) {
return ApiResponse(
success: false,
message: e.response?.data['message'] ?? 'Network error',
errorCode: e.response?.data['error_code'],
);
}
}
/// Logout
Future<ApiResponse<void>> logout() async {
try {
final response = await _dioClient.dio.post(
ApiConfig.logoutEndpoint,
);
await _dioClient.clearToken();
return ApiResponse<void>.fromJson(response.data, null);
} on DioException catch (e) {
return ApiResponse(
success: false,
message: e.response?.data['message'] ?? 'Network error',
errorCode: e.response?.data['error_code'],
);
}
}
}Complete login screen example:
import 'package:flutter/material.dart';
import '../core/services/auth_service.dart';
class LoginScreen extends StatefulWidget {
const LoginScreen({Key? key}) : super(key: key);
@override
State<LoginScreen> createState() => _LoginScreenState();
}
class _LoginScreenState extends State<LoginScreen> {
final _authService = AuthService();
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
bool _isLoading = false;
Future<void> _handleLogin() async {
setState(() => _isLoading = true);
try {
final response = await _authService.login(
identifier: _emailController.text.trim(),
password: _passwordController.text,
loginType: 'email',
);
if (response.success) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Welcome!'),
backgroundColor: Colors.green,
),
);
Navigator.pushReplacementNamed(context, '/home');
}
} else {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(response.message),
backgroundColor: Colors.red,
),
);
}
}
} finally {
if (mounted) {
setState(() => _isLoading = false);
}
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Login')),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
TextField(
controller: _emailController,
decoration: const InputDecoration(
labelText: 'Email',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 16),
TextField(
controller: _passwordController,
decoration: const InputDecoration(
labelText: 'Password',
border: OutlineInputBorder(),
),
obscureText: true,
),
const SizedBox(height: 24),
SizedBox(
width: double.infinity,
height: 50,
child: ElevatedButton(
onPressed: _isLoading ? null : _handleLogin,
child: _isLoading
? const CircularProgressIndicator(color: Colors.white)
: const Text('Login'),
),
),
],
),
),
);
}
}Handle API error codes:
class ApiErrorHandler {
static String getErrorMessage(String? errorCode) {
switch (errorCode) {
case 'INVALID_API_KEY':
return 'Invalid API key';
case 'UNAUTHENTICATED':
return 'Please login';
case 'INVALID_CREDENTIALS':
return 'Invalid credentials';
case 'MODEL_NOT_FOUND':
return 'Not found';
default:
return 'An error occurred';
}
}
}- Add Dio dependency
- Create ApiConfig with API key
- Create DioClient with interceptors
- Create AuthService
- Use secure storage for tokens
- Handle errors properly
- Test on production
Good luck! π