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__/
|
__pycache__/
|
||||||
*.pyc
|
*.pyc
|
||||||
.DS_Store
|
.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 {
|
plugins {
|
||||||
id("com.android.application")
|
id("com.android.application")
|
||||||
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
|
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
|
||||||
@ -14,30 +23,37 @@ android {
|
|||||||
targetCompatibility = JavaVersion.VERSION_17
|
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 {
|
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"
|
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
|
minSdk = flutter.minSdkVersion
|
||||||
targetSdk = flutter.targetSdkVersion
|
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
|
versionCode = flutter.versionCode
|
||||||
versionName = flutter.versionName
|
versionName = flutter.versionName
|
||||||
}
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
release {
|
release {
|
||||||
// TODO: Add your own signing config for the release build.
|
val releaseConfig = signingConfigs.getByName("release")
|
||||||
// Signing with the debug keys for now, so `flutter run --release` works.
|
signingConfig = if (keystorePropertiesFile.exists() && releaseConfig.storeFile?.exists() == true) {
|
||||||
signingConfig = signingConfigs.getByName("debug")
|
releaseConfig
|
||||||
|
} else {
|
||||||
|
signingConfigs.getByName("debug")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
compilerOptions {
|
compilerOptions {
|
||||||
jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17
|
jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17
|
||||||
|
|||||||
@ -2,9 +2,10 @@
|
|||||||
<uses-permission android:name="android.permission.INTERNET"/>
|
<uses-permission android:name="android.permission.INTERNET"/>
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
||||||
<application
|
<application
|
||||||
android:label="metatron_student_erp"
|
android:label="Metatron Student ERP"
|
||||||
android:name="${applicationName}"
|
android:name="${applicationName}"
|
||||||
android:icon="@mipmap/ic_launcher">
|
android:icon="@mipmap/ic_launcher"
|
||||||
|
android:networkSecurityConfig="@xml/network_security_config">
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:exported="true"
|
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>
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||||
<key>CFBundleDisplayName</key>
|
<key>CFBundleDisplayName</key>
|
||||||
<string>Metatron Student Erp</string>
|
<string>Metatron Student ERP</string>
|
||||||
<key>CFBundleExecutable</key>
|
<key>CFBundleExecutable</key>
|
||||||
<string>$(EXECUTABLE_NAME)</string>
|
<string>$(EXECUTABLE_NAME)</string>
|
||||||
<key>CFBundleIdentifier</key>
|
<key>CFBundleIdentifier</key>
|
||||||
|
|||||||
@ -34,6 +34,16 @@ class ApiClient {
|
|||||||
// Dynamic base URL check in case it was updated
|
// Dynamic base URL check in case it was updated
|
||||||
options.baseUrl = ApiConfig.baseUrl;
|
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
|
// Attach session cookie if available
|
||||||
_sessionCookie ??= await _storage.getSessionCookie();
|
_sessionCookie ??= await _storage.getSessionCookie();
|
||||||
if (_sessionCookie != null && _sessionCookie!.isNotEmpty) {
|
if (_sessionCookie != null && _sessionCookie!.isNotEmpty) {
|
||||||
@ -175,6 +185,11 @@ class ApiClient {
|
|||||||
return ApiException.fromCode('SERVER_ERROR', statusCode: statusCode);
|
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 ||
|
if (error.type == DioExceptionType.connectionTimeout ||
|
||||||
error.type == DioExceptionType.receiveTimeout ||
|
error.type == DioExceptionType.receiveTimeout ||
|
||||||
error.type == DioExceptionType.sendTimeout ||
|
error.type == DioExceptionType.sendTimeout ||
|
||||||
|
|||||||
@ -1,24 +1,63 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'package:flutter/foundation.dart';
|
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 {
|
class ApiConfig {
|
||||||
static const String _envBaseUrl = String.fromEnvironment('API_BASE_URL', defaultValue: '');
|
static const String _envBaseUrl = String.fromEnvironment('API_BASE_URL', defaultValue: '');
|
||||||
|
|
||||||
static String _customBaseUrl = '';
|
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.
|
/// Returns the active API base URL.
|
||||||
/// Priority:
|
/// Priority:
|
||||||
/// 1. Custom runtime configured URL (e.g. from Settings screen for physical devices)
|
/// 1. Custom runtime configured URL (e.g. from Settings screen for physical devices)
|
||||||
/// 2. Compile-time --dart-define=API_BASE_URL=...
|
/// 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 {
|
static String get baseUrl {
|
||||||
if (_customBaseUrl.isNotEmpty) {
|
if (_customBaseUrl.isNotEmpty) {
|
||||||
return _customBaseUrl;
|
return _customBaseUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_envBaseUrl.isNotEmpty) {
|
if (_envBaseUrl.isNotEmpty) {
|
||||||
|
if (isProduction && !_envBaseUrl.startsWith('https://')) {
|
||||||
|
debugPrint('WARNING: Production API_BASE_URL should use HTTPS: $_envBaseUrl');
|
||||||
|
}
|
||||||
return _envBaseUrl;
|
return _envBaseUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// In production without compile-time or runtime override, do not default to localhost
|
||||||
|
if (isProduction) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
if (kIsWeb) {
|
if (kIsWeb) {
|
||||||
return 'http://localhost:10008';
|
return 'http://localhost:10008';
|
||||||
}
|
}
|
||||||
@ -47,3 +86,4 @@ class ApiConfig {
|
|||||||
static const Duration connectTimeout = Duration(seconds: 15);
|
static const Duration connectTimeout = Duration(seconds: 15);
|
||||||
static const Duration receiveTimeout = Duration(seconds: 15);
|
static const Duration receiveTimeout = Duration(seconds: 15);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -18,6 +18,9 @@ class ApiException implements Exception {
|
|||||||
case 'AUTH_REQUIRED':
|
case 'AUTH_REQUIRED':
|
||||||
message = 'Your session has expired. Please login again.';
|
message = 'Your session has expired. Please login again.';
|
||||||
break;
|
break;
|
||||||
|
case 'NOT_CONFIGURED':
|
||||||
|
message = 'API Server URL is not configured. Please configure server URL in Settings.';
|
||||||
|
break;
|
||||||
case 'STUDENT_NOT_LINKED':
|
case 'STUDENT_NOT_LINKED':
|
||||||
message = 'No student profile is linked to this account.';
|
message = 'No student profile is linked to this account.';
|
||||||
break;
|
break;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user