import 'dart:io'; import 'dart:convert'; import 'dart:math'; import 'package:yaml/yaml.dart'; class Config { static late String bindHost; static late int bindPort; static late String publicBaseUrl; static late String serverName; static late String jwtSecret; static late bool configured; static late bool setupMode; static late int bootstrapAdminLimit; static late bool devDbTools; static const Duration tokenExpiry = Duration(days: 7); static void initialize() { final configFile = File('config.yaml'); if (!configFile.existsSync()) { final generatedSecret = _generateJwtSecret(); configFile.writeAsStringSync(_defaultConfig(generatedSecret)); print('✅ Generated config.yaml (first run).'); print('⚠️ Review server.public_base_url and security.jwt_secret before production use.'); } final yamlString = configFile.readAsStringSync(); final yamlMap = loadYaml(yamlString) as YamlMap; final serverMap = yamlMap['server'] as YamlMap?; final securityMap = yamlMap['security'] as YamlMap?; serverName = (serverMap?['name'] as String?)?.trim() ?? ''; bindHost = (serverMap?['bind_host'] as String?)?.trim() ?? (serverMap?['host'] as String?)?.trim() ?? '127.0.0.1'; bindPort = (serverMap?['bind_port'] as int?) ?? (serverMap?['port'] as int?) ?? 9870; final rawPublicBaseUrl = (serverMap?['public_base_url'] as String?)?.trim() ?? _legacyPublicBaseUrlFromHost((serverMap?['host'] as String?)?.trim()); publicBaseUrl = _normalizePublicBaseUrl(rawPublicBaseUrl) ?? ''; configured = (serverMap?['configured'] as bool?) ?? false; setupMode = (serverMap?['first_setup'] as bool?) ?? false; bootstrapAdminLimit = (serverMap?['bootstrap_admin_limit'] as int?) ?? 1; devDbTools = (serverMap?['dev_db_tools'] as bool?) ?? false; jwtSecret = (securityMap?['jwt_secret'] as String?)?.trim() ?? ''; if (serverName.isEmpty) { throw StateError('config.yaml: server.name is required.'); } if (bindHost.isEmpty) { throw StateError('config.yaml: server.bind_host is required.'); } if (bindPort <= 0 || bindPort > 65535) { throw StateError('config.yaml: server.bind_port must be between 1 and 65535.'); } if (publicBaseUrl.isEmpty) { throw StateError( 'config.yaml: server.public_base_url is required (example: https://chat.example.com).', ); } if (jwtSecret.isEmpty) { throw StateError('config.yaml: security.jwt_secret is required.'); } if (!configured) { throw StateError( 'Server setup is not complete.\n' 'Edit config.yaml and set:\n' ' server.configured: true\n' 'After reviewing server.public_base_url and security.jwt_secret.', ); } if (bootstrapAdminLimit < 0) { throw StateError('config.yaml: server.bootstrap_admin_limit must be >= 0.'); } print( '🚀 Server Config Loaded: $serverName (bind=$bindHost:$bindPort, public=$publicBaseUrl, configured=$configured, setupMode=$setupMode, bootstrapAdminLimit=$bootstrapAdminLimit, devDbTools=$devDbTools)', ); } static String _generateJwtSecret() { final random = Random.secure(); final bytes = List.generate(32, (_) => random.nextInt(256)); return base64UrlEncode(bytes); } static String _defaultConfig(String generatedSecret) => ''' # Reliquary Server Configuration server: # Display name shown in clients name: "Reliquary Server" # Local bind address for the HTTP server bind_host: "127.0.0.1" # Local bind port for the HTTP server bind_port: 9870 # Public base URL used for identity, handles, and cross-server references # Dev example: http://localhost:9870 # Prod example: https://chat.example.com public_base_url: "http://localhost:9870" # Set to true after reviewing this file; server refuses to start while false configured: false # First local registration becomes admin first_setup: true # Number of local registrations auto-promoted to admin while first_setup=true bootstrap_admin_limit: 1 # Testing-only DB admin/debug endpoints dev_db_tools: true security: # Auto-generated on first run; replace for production jwt_secret: "$generatedSecret" '''; static String get publicHost { final uri = Uri.tryParse(publicBaseUrl); if (uri == null || uri.host.isEmpty) { return publicBaseUrl; } return uri.hasPort ? '${uri.host}:${uri.port}' : uri.host; } static String? _normalizePublicBaseUrl(String? value) { if (value == null || value.trim().isEmpty) { return null; } final uri = Uri.tryParse(value.trim()); if (uri == null || uri.host.isEmpty) { return null; } if (uri.scheme != 'http' && uri.scheme != 'https') { return null; } return Uri( scheme: uri.scheme, host: uri.host, port: uri.hasPort ? uri.port : null, ).toString().replaceAll(RegExp(r'/$'), ''); } static String? _legacyPublicBaseUrlFromHost(String? hostValue) { if (hostValue == null || hostValue.trim().isEmpty) { return null; } final host = hostValue.trim(); if (host.contains('://')) { return host; } final lower = host.toLowerCase(); final isLocal = lower.startsWith('localhost') || lower.startsWith('127.') || lower.startsWith('[::1]'); final scheme = isLocal ? 'http' : 'https'; return '$scheme://$host'; } }