chore(release): configure deployment architecture, environment security, and release signing
This commit is contained in:
parent
e5d3fdcaa8
commit
921ddc9ac9
21
.gitignore
vendored
21
.gitignore
vendored
@ -1,3 +1,24 @@
|
||||
__pycache__/
|
||||
*.pyc
|
||||
.DS_Store
|
||||
|
||||
# Environment and secrets
|
||||
.env
|
||||
.env.*
|
||||
*.key
|
||||
*.pem
|
||||
*.p12
|
||||
*.jks
|
||||
*.keystore
|
||||
key.properties
|
||||
|
||||
# Database dumps and backup archives
|
||||
*.dump
|
||||
*.sql
|
||||
*.tar.gz
|
||||
|
||||
# IDE and build artifacts
|
||||
.idea/
|
||||
.vscode/
|
||||
build/
|
||||
.dart_tool/
|
||||
|
||||
110
deploy/backup_restore.sh
Executable file
110
deploy/backup_restore.sh
Executable file
@ -0,0 +1,110 @@
|
||||
#!/usr/bin/env bash
|
||||
# ==============================================================================
|
||||
# Metatron Student ERP - Production Backup & Recovery Script
|
||||
# Usage:
|
||||
# ./backup_restore.sh backup [DEST_DIR]
|
||||
# ./backup_restore.sh restore <DB_DUMP_FILE> <FILESTORE_ARCHIVE> <TARGET_DB>
|
||||
# ./backup_restore.sh verify <DB_DUMP_FILE> <FILESTORE_ARCHIVE>
|
||||
# ==============================================================================
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
DB_CONTAINER="${DB_CONTAINER:-metatron_academic_db}"
|
||||
ODOO_CONTAINER="${ODOO_CONTAINER:-metatron_academic}"
|
||||
DB_USER="${POSTGRES_USER:-odoo}"
|
||||
DB_NAME="${POSTGRES_DB:-metatron_aca_erp}"
|
||||
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
|
||||
|
||||
ACTION="${1:-help}"
|
||||
|
||||
case "$ACTION" in
|
||||
backup)
|
||||
BACKUP_DIR="${2:-./backups}"
|
||||
mkdir -p "$BACKUP_DIR"
|
||||
|
||||
DB_FILE="$BACKUP_DIR/${DB_NAME}_${TIMESTAMP}.dump"
|
||||
FS_FILE="$BACKUP_DIR/filestore_${DB_NAME}_${TIMESTAMP}.tar.gz"
|
||||
|
||||
echo "[+] Starting PostgreSQL database backup for '$DB_NAME'..."
|
||||
docker exec -t "$DB_CONTAINER" pg_dump -U "$DB_USER" -Fc "$DB_NAME" > "$DB_FILE"
|
||||
echo "[✓] Database backup created: $DB_FILE ($(du -h "$DB_FILE" | cut -f1))"
|
||||
|
||||
echo "[+] Starting Odoo filestore archive for '$DB_NAME'..."
|
||||
docker exec "$ODOO_CONTAINER" tar -czf - -C /var/lib/odoo/filestore "$DB_NAME" > "$FS_FILE"
|
||||
echo "[✓] Filestore archive created: $FS_FILE ($(du -h "$FS_FILE" | cut -f1))"
|
||||
|
||||
echo "[+] Calculating SHA256 checksums..."
|
||||
if command -v shasum >/dev/null 2>&1; then
|
||||
shasum -a 256 "$DB_FILE" "$FS_FILE" > "$BACKUP_DIR/checksums_${TIMESTAMP}.sha256"
|
||||
elif command -v sha256sum >/dev/null 2>&1; then
|
||||
sha256sum "$DB_FILE" "$FS_FILE" > "$BACKUP_DIR/checksums_${TIMESTAMP}.sha256"
|
||||
fi
|
||||
|
||||
echo "=================================================================="
|
||||
echo "BACKUP SUCCESSFUL: $TIMESTAMP"
|
||||
echo "Database: $DB_FILE"
|
||||
echo "Filestore: $FS_FILE"
|
||||
echo "=================================================================="
|
||||
;;
|
||||
|
||||
verify)
|
||||
DB_FILE="${2:-}"
|
||||
FS_FILE="${3:-}"
|
||||
|
||||
if [[ -z "$DB_FILE" || -z "$FS_FILE" ]]; then
|
||||
echo "[-] Error: Provide both database dump and filestore archive to verify."
|
||||
echo " Usage: $0 verify <DB_DUMP_FILE> <FILESTORE_ARCHIVE>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "[+] Verifying database archive integrity..."
|
||||
# pg_restore -l lists table of contents without restoring
|
||||
docker exec -i "$DB_CONTAINER" pg_restore -l < "$DB_FILE" > /dev/null
|
||||
echo "[✓] Database dump archive is valid."
|
||||
|
||||
echo "[+] Verifying filestore tar.gz integrity..."
|
||||
tar -tzf "$FS_FILE" > /dev/null
|
||||
echo "[✓] Filestore tar.gz archive is valid."
|
||||
echo "All backup archives verified intact."
|
||||
;;
|
||||
|
||||
restore)
|
||||
DB_FILE="${2:-}"
|
||||
FS_FILE="${3:-}"
|
||||
TARGET_DB="${4:-${DB_NAME}_restored}"
|
||||
|
||||
if [[ -z "$DB_FILE" || -z "$FS_FILE" ]]; then
|
||||
echo "[-] Error: Provide DB dump file, filestore archive, and target database name."
|
||||
echo " Usage: $0 restore <DB_DUMP_FILE> <FILESTORE_ARCHIVE> [TARGET_DB]"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "[!] WARNING: You are about to restore to database '$TARGET_DB'."
|
||||
read -p "Are you sure you want to continue? (y/N): " CONFIRM
|
||||
if [[ "$CONFIRM" != "y" && "$CONFIRM" != "Y" ]]; then
|
||||
echo "Restore aborted by user."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "[+] Creating database '$TARGET_DB' if not exists..."
|
||||
docker exec -i "$DB_CONTAINER" psql -U "$DB_USER" -c "CREATE DATABASE \"$TARGET_DB\" OWNER \"$DB_USER\";" 2>/dev/null || true
|
||||
|
||||
echo "[+] Restoring PostgreSQL database from $DB_FILE..."
|
||||
docker exec -i "$DB_CONTAINER" pg_restore -U "$DB_USER" -d "$TARGET_DB" --clean --if-exists < "$DB_FILE" || true
|
||||
echo "[✓] Database restore completed."
|
||||
|
||||
echo "[+] Restoring filestore to container..."
|
||||
docker exec -i "$ODOO_CONTAINER" mkdir -p /var/lib/odoo/filestore/"$TARGET_DB"
|
||||
docker exec -i "$ODOO_CONTAINER" tar -xzf - -C /var/lib/odoo/filestore/"$TARGET_DB" --strip-components=1 < "$FS_FILE"
|
||||
echo "[✓] Filestore restored to /var/lib/odoo/filestore/$TARGET_DB."
|
||||
;;
|
||||
|
||||
*)
|
||||
echo "Metatron Student ERP Backup & Recovery Utility"
|
||||
echo "Usage:"
|
||||
echo " $0 backup [DEST_DIR] - Take full DB and filestore backup"
|
||||
echo " $0 verify <DB_DUMP_FILE> <FILESTORE_ARCHIVE> - Test backup archive integrity"
|
||||
echo " $0 restore <DB_DUMP_FILE> <FS_ARCHIVE> <TARGET_DB> - Restore to target database"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
87
deploy/docker-compose.prod.yml
Normal file
87
deploy/docker-compose.prod.yml
Normal file
@ -0,0 +1,87 @@
|
||||
services:
|
||||
db:
|
||||
image: postgres:15-alpine
|
||||
container_name: metatron_erp_db_prod
|
||||
restart: always
|
||||
environment:
|
||||
POSTGRES_DB: ${POSTGRES_DB:-metatron_aca_erp}
|
||||
POSTGRES_USER: ${POSTGRES_USER:-odoo}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||
volumes:
|
||||
- prod_pgdata:/var/lib/postgresql/data
|
||||
networks:
|
||||
- internal_network
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-odoo} -d ${POSTGRES_DB:-metatron_aca_erp}"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
start_period: 10s
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "50m"
|
||||
max-file: "5"
|
||||
|
||||
odoo:
|
||||
image: odoo:19.0
|
||||
container_name: metatron_erp_app_prod
|
||||
restart: always
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
HOST: db
|
||||
PORT: 5432
|
||||
USER: ${POSTGRES_USER:-odoo}
|
||||
PASSWORD: ${POSTGRES_PASSWORD}
|
||||
volumes:
|
||||
- prod_odoo_data:/var/lib/odoo
|
||||
- ../addons:/mnt/extra-addons:ro
|
||||
- ./odoo.prod.conf:/etc/odoo/odoo.conf:ro
|
||||
command: odoo -c /etc/odoo/odoo.conf
|
||||
networks:
|
||||
- internal_network
|
||||
- proxy_network
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "curl -f http://127.0.0.1:8069/web/health || exit 1"]
|
||||
interval: 15s
|
||||
timeout: 10s
|
||||
retries: 5
|
||||
start_period: 30s
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "50m"
|
||||
max-file: "5"
|
||||
|
||||
nginx:
|
||||
image: nginx:alpine
|
||||
container_name: metatron_erp_nginx_prod
|
||||
restart: always
|
||||
depends_on:
|
||||
odoo:
|
||||
condition: service_healthy
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
volumes:
|
||||
- ./nginx/metatron_erp.conf:/etc/nginx/conf.d/default.conf:ro
|
||||
- ./certs:/etc/nginx/certs:ro
|
||||
networks:
|
||||
- proxy_network
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "50m"
|
||||
max-file: "5"
|
||||
|
||||
networks:
|
||||
internal_network:
|
||||
internal: true
|
||||
proxy_network:
|
||||
driver: bridge
|
||||
|
||||
volumes:
|
||||
prod_pgdata:
|
||||
prod_odoo_data:
|
||||
93
deploy/nginx/metatron_erp.conf
Normal file
93
deploy/nginx/metatron_erp.conf
Normal file
@ -0,0 +1,93 @@
|
||||
# Upstream servers
|
||||
upstream odoo_backend {
|
||||
server odoo:8069;
|
||||
}
|
||||
|
||||
upstream odoo_chat {
|
||||
server odoo:8072;
|
||||
}
|
||||
|
||||
# Redirect all HTTP traffic to HTTPS
|
||||
server {
|
||||
listen 80;
|
||||
server_name erp.yourdomain.edu;
|
||||
|
||||
# ACME-challenge for Let's Encrypt certificates
|
||||
location /.well-known/acme-challenge/ {
|
||||
root /var/www/certbot;
|
||||
}
|
||||
|
||||
location / {
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
}
|
||||
|
||||
# HTTPS server
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
server_name erp.yourdomain.edu;
|
||||
|
||||
# SSL certificate paths
|
||||
ssl_certificate /etc/nginx/certs/fullchain.pem;
|
||||
ssl_certificate_key /etc/nginx/certs/privkey.pem;
|
||||
|
||||
# Modern TLS configuration
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384';
|
||||
ssl_prefer_server_ciphers on;
|
||||
ssl_session_cache shared:SSL:10m;
|
||||
ssl_session_timeout 1d;
|
||||
ssl_session_tickets off;
|
||||
|
||||
# Security Headers
|
||||
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||
add_header X-XSS-Protection "1; mode=block" always;
|
||||
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||
|
||||
# Request body limits for mobile document and certificate uploads
|
||||
client_max_body_size 25M;
|
||||
|
||||
# Buffer sizes
|
||||
proxy_read_timeout 720s;
|
||||
proxy_connect_timeout 720s;
|
||||
proxy_send_timeout 720s;
|
||||
|
||||
# Log format
|
||||
access_log /var/log/nginx/odoo.access.log;
|
||||
error_log /var/log/nginx/odoo.error.log;
|
||||
|
||||
# Longpolling / WebSocket requests
|
||||
location /websocket {
|
||||
proxy_pass http://odoo_chat;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
# Main Odoo application and Mobile API endpoints
|
||||
location / {
|
||||
proxy_pass http://odoo_backend;
|
||||
proxy_redirect off;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
# Gzip compression for API payloads and static files
|
||||
gzip on;
|
||||
gzip_types text/css text/less text/plain text/xml application/xml application/json application/javascript;
|
||||
}
|
||||
|
||||
# Cache static files
|
||||
location ~* /web/static/ {
|
||||
proxy_cache_valid 200 60m;
|
||||
proxy_buffering on;
|
||||
expires 864000;
|
||||
proxy_pass http://odoo_backend;
|
||||
}
|
||||
}
|
||||
35
deploy/odoo.prod.conf
Normal file
35
deploy/odoo.prod.conf
Normal file
@ -0,0 +1,35 @@
|
||||
[options]
|
||||
; Database server settings
|
||||
db_host = db
|
||||
db_port = 5432
|
||||
db_user = odoo
|
||||
db_name = metatron_aca_erp
|
||||
|
||||
; Security: Disable Database Manager interface in production
|
||||
list_db = False
|
||||
|
||||
; Master Password for administrative database actions
|
||||
admin_passwd = CHANGE_ME_STRONG_ODOO_MASTER_PASSWORD
|
||||
|
||||
; Addons paths
|
||||
addons_path = /usr/lib/python3/dist-packages/odoo/addons,/mnt/extra-addons
|
||||
|
||||
; Multi-processing and concurrency for production
|
||||
workers = 5
|
||||
max_cron_threads = 2
|
||||
|
||||
; Reverse proxy support (trusts X-Forwarded-For and X-Forwarded-Proto)
|
||||
proxy_mode = True
|
||||
|
||||
; Time and memory limits
|
||||
limit_time_cpu = 120
|
||||
limit_time_real = 240
|
||||
limit_memory_hard = 2684354560
|
||||
limit_memory_soft = 2147483648
|
||||
limit_request = 8192
|
||||
|
||||
; Logging configuration
|
||||
log_level = info
|
||||
log_handler = :INFO,odoo.addons.education_mobile_api:INFO
|
||||
logfile = False
|
||||
log_db = False
|
||||
@ -1,3 +1,12 @@
|
||||
import java.util.Properties
|
||||
import java.io.FileInputStream
|
||||
|
||||
val keystoreProperties = Properties()
|
||||
val keystorePropertiesFile = rootProject.file("key.properties")
|
||||
if (keystorePropertiesFile.exists()) {
|
||||
keystoreProperties.load(FileInputStream(keystorePropertiesFile))
|
||||
}
|
||||
|
||||
plugins {
|
||||
id("com.android.application")
|
||||
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
|
||||
@ -14,29 +23,36 @@ android {
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
create("release") {
|
||||
keyAlias = keystoreProperties.getProperty("keyAlias")
|
||||
keyPassword = keystoreProperties.getProperty("keyPassword")
|
||||
storeFile = keystoreProperties.getProperty("storeFile")?.let { file(it) }
|
||||
storePassword = keystoreProperties.getProperty("storePassword")
|
||||
}
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
||||
// Application ID matches reverse-domain package identifier
|
||||
applicationId = "in.metatroncube.metatron_student_erp"
|
||||
// You can update the following values to match your application needs.
|
||||
// For more information, see: https://flutter.dev/to/review-gradle-config.
|
||||
minSdk = flutter.minSdkVersion
|
||||
targetSdk = flutter.targetSdkVersion
|
||||
// Uses the version code from pubspec.yaml. When using split APKs, 1000 * ABI_VERSION
|
||||
// is added automatically by Flutter. (https://developer.android.com/studio/build/configure-apk-splits#configure-APK-versions)
|
||||
// You can force using the value of versionCode by specifying the `-P force-version-code-ignoring-abi=true`
|
||||
// flag during build.
|
||||
versionCode = flutter.versionCode
|
||||
versionName = flutter.versionName
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
// TODO: Add your own signing config for the release build.
|
||||
// Signing with the debug keys for now, so `flutter run --release` works.
|
||||
signingConfig = signingConfigs.getByName("debug")
|
||||
val releaseConfig = signingConfigs.getByName("release")
|
||||
signingConfig = if (keystorePropertiesFile.exists() && releaseConfig.storeFile?.exists() == true) {
|
||||
releaseConfig
|
||||
} else {
|
||||
signingConfigs.getByName("debug")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
kotlin {
|
||||
compilerOptions {
|
||||
|
||||
@ -2,9 +2,10 @@
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
||||
<application
|
||||
android:label="metatron_student_erp"
|
||||
android:label="Metatron Student ERP"
|
||||
android:name="${applicationName}"
|
||||
android:icon="@mipmap/ic_launcher">
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:networkSecurityConfig="@xml/network_security_config">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
|
||||
@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<network-security-config>
|
||||
<!-- Base configuration: Enforce secure HTTPS connections by default -->
|
||||
<base-config cleartextTrafficPermitted="false">
|
||||
<trust-anchors>
|
||||
<certificates src="system" />
|
||||
</trust-anchors>
|
||||
</base-config>
|
||||
|
||||
<!-- Debug/Development and Local Staging Overrides -->
|
||||
<debug-overrides>
|
||||
<trust-anchors>
|
||||
<certificates src="system" />
|
||||
<certificates src="user" />
|
||||
</trust-anchors>
|
||||
</debug-overrides>
|
||||
|
||||
<!-- Allow cleartext HTTP only for local emulator and localhost loopbacks during development -->
|
||||
<domain-config cleartextTrafficPermitted="true">
|
||||
<domain includeSubdomains="true">10.0.2.2</domain>
|
||||
<domain includeSubdomains="true">localhost</domain>
|
||||
<domain includeSubdomains="true">127.0.0.1</domain>
|
||||
</domain-config>
|
||||
</network-security-config>
|
||||
10
flutter_app/android/key.properties.example
Normal file
10
flutter_app/android/key.properties.example
Normal file
@ -0,0 +1,10 @@
|
||||
# Metatron Student ERP - Android Release Keystore Configuration Template
|
||||
# DO NOT COMMIT key.properties WITH REAL SECRETS TO SOURCE CONTROL
|
||||
# Copy this file to key.properties and fill in your actual production keystore details:
|
||||
# cp key.properties.example key.properties
|
||||
# Ensure key.properties and *.keystore/*.jks files are in .gitignore (already configured).
|
||||
|
||||
storePassword=<YOUR_KEYSTORE_PASSWORD>
|
||||
keyPassword=<YOUR_KEY_PASSWORD>
|
||||
keyAlias=<YOUR_KEY_ALIAS>
|
||||
storeFile=<PATH_TO_YOUR_KEYSTORE_FILE_E_G_upload_keystore_jks>
|
||||
@ -7,7 +7,7 @@
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>Metatron Student Erp</string>
|
||||
<string>Metatron Student ERP</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
|
||||
@ -34,6 +34,16 @@ class ApiClient {
|
||||
// Dynamic base URL check in case it was updated
|
||||
options.baseUrl = ApiConfig.baseUrl;
|
||||
|
||||
if (options.baseUrl.isEmpty) {
|
||||
return handler.reject(
|
||||
DioException(
|
||||
requestOptions: options,
|
||||
type: DioExceptionType.cancel,
|
||||
error: 'API Server URL is not configured. Please configure server URL in Settings.',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Attach session cookie if available
|
||||
_sessionCookie ??= await _storage.getSessionCookie();
|
||||
if (_sessionCookie != null && _sessionCookie!.isNotEmpty) {
|
||||
@ -175,6 +185,11 @@ class ApiClient {
|
||||
return ApiException.fromCode('SERVER_ERROR', statusCode: statusCode);
|
||||
}
|
||||
|
||||
if (error.error != null && error.error.toString().contains('API Server URL is not configured')) {
|
||||
return ApiException.fromCode('NOT_CONFIGURED', customMessage: error.error.toString());
|
||||
}
|
||||
|
||||
|
||||
if (error.type == DioExceptionType.connectionTimeout ||
|
||||
error.type == DioExceptionType.receiveTimeout ||
|
||||
error.type == DioExceptionType.sendTimeout ||
|
||||
|
||||
@ -1,24 +1,63 @@
|
||||
import 'dart:io';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
enum AppEnvironment {
|
||||
development,
|
||||
staging,
|
||||
production;
|
||||
|
||||
static AppEnvironment get current {
|
||||
const env = String.fromEnvironment('ENVIRONMENT', defaultValue: '');
|
||||
switch (env.toLowerCase()) {
|
||||
case 'production':
|
||||
case 'prod':
|
||||
return AppEnvironment.production;
|
||||
case 'staging':
|
||||
case 'stage':
|
||||
return AppEnvironment.staging;
|
||||
case 'development':
|
||||
case 'dev':
|
||||
return AppEnvironment.development;
|
||||
default:
|
||||
return kReleaseMode ? AppEnvironment.production : AppEnvironment.development;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ApiConfig {
|
||||
static const String _envBaseUrl = String.fromEnvironment('API_BASE_URL', defaultValue: '');
|
||||
|
||||
static String _customBaseUrl = '';
|
||||
|
||||
static AppEnvironment get environment => AppEnvironment.current;
|
||||
|
||||
static bool get isProduction => environment == AppEnvironment.production;
|
||||
static bool get isStaging => environment == AppEnvironment.staging;
|
||||
static bool get isDevelopment => environment == AppEnvironment.development;
|
||||
|
||||
/// Returns the active API base URL.
|
||||
/// Priority:
|
||||
/// 1. Custom runtime configured URL (e.g. from Settings screen for physical devices)
|
||||
/// 2. Compile-time --dart-define=API_BASE_URL=...
|
||||
/// 3. Platform default: 10.0.2.2 on Android Emulator, localhost on iOS/macOS/Web
|
||||
/// 3. In development/testing: platform default (10.0.2.2 on Android Emulator, localhost on iOS/macOS/Web)
|
||||
/// 4. In production: returns empty if unconfigured (no hardcoded localhost fallback in production builds)
|
||||
static String get baseUrl {
|
||||
if (_customBaseUrl.isNotEmpty) {
|
||||
return _customBaseUrl;
|
||||
}
|
||||
|
||||
if (_envBaseUrl.isNotEmpty) {
|
||||
if (isProduction && !_envBaseUrl.startsWith('https://')) {
|
||||
debugPrint('WARNING: Production API_BASE_URL should use HTTPS: $_envBaseUrl');
|
||||
}
|
||||
return _envBaseUrl;
|
||||
}
|
||||
|
||||
// In production without compile-time or runtime override, do not default to localhost
|
||||
if (isProduction) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (kIsWeb) {
|
||||
return 'http://localhost:10008';
|
||||
}
|
||||
@ -47,3 +86,4 @@ class ApiConfig {
|
||||
static const Duration connectTimeout = Duration(seconds: 15);
|
||||
static const Duration receiveTimeout = Duration(seconds: 15);
|
||||
}
|
||||
|
||||
|
||||
@ -18,6 +18,9 @@ class ApiException implements Exception {
|
||||
case 'AUTH_REQUIRED':
|
||||
message = 'Your session has expired. Please login again.';
|
||||
break;
|
||||
case 'NOT_CONFIGURED':
|
||||
message = 'API Server URL is not configured. Please configure server URL in Settings.';
|
||||
break;
|
||||
case 'STUDENT_NOT_LINKED':
|
||||
message = 'No student profile is linked to this account.';
|
||||
break;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user