Rebrand CommGate to gsmnode and adopt new design system
Rename the whole project from CommGate to gsmnode and re-skin every surface onto the new signal-green + ink design system (Space Grotesk / IBM Plex Sans / JetBrains Mono, flat surfaces, two-arrow routing mark, lowercase gsm+node wordmark). - Web App + API panel: rewrite token layer, gn-* utilities, data-gsm-theme, new logo/favicon assets; both builds verified. - Phone App (Flutter): green theme, new mark/wordmark widgets, adaptive launcher icons; rename Android applicationId/package to app.gsmnode.phone; bump AGP to 8.6.0 and google_fonts to 8.1.0; debug APK build verified. - API Server (Go): panel routes, User-Agent, health service id, branding. - Home Assistant Plugin: rename integration domain sms_gateway to gsmnode (folder, DOMAIN, services, entity ids, classes); py_compile verified. - Update all READMEs; add .gitignore (excludes Design/, build artifacts). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,130 @@
|
||||
import 'dart:convert';
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
import '../models/message.dart';
|
||||
import 'storage.dart';
|
||||
|
||||
class ApiException implements Exception {
|
||||
final int status;
|
||||
final String message;
|
||||
ApiException(this.status, this.message);
|
||||
@override
|
||||
String toString() => 'ApiException($status): $message';
|
||||
}
|
||||
|
||||
/// Client for the API Server. Uses the user JWT for login/registration and the
|
||||
/// opaque device token for the mobile gateway endpoints.
|
||||
class ApiClient {
|
||||
final Storage storage;
|
||||
final http.Client _http;
|
||||
|
||||
ApiClient(this.storage, {http.Client? client})
|
||||
: _http = client ?? http.Client();
|
||||
|
||||
String get _base => (storage.apiBase ?? '').replaceAll(RegExp(r'/$'), '');
|
||||
|
||||
Uri _uri(String path) => Uri.parse('$_base$path');
|
||||
|
||||
Map<String, String> _headers(String? token) => {
|
||||
'Content-Type': 'application/json',
|
||||
if (token != null && token.isNotEmpty) 'Authorization': 'Bearer $token',
|
||||
};
|
||||
|
||||
dynamic _decode(http.Response res) {
|
||||
final body = res.body.isNotEmpty ? jsonDecode(res.body) : null;
|
||||
if (res.statusCode < 200 || res.statusCode >= 300) {
|
||||
final msg = (body is Map && body['error'] != null)
|
||||
? body['error'].toString()
|
||||
: 'HTTP ${res.statusCode}';
|
||||
throw ApiException(res.statusCode, msg);
|
||||
}
|
||||
return body;
|
||||
}
|
||||
|
||||
/// Authenticates a user and stores the JWT. Returns the user email.
|
||||
Future<String> login(String email, String password) async {
|
||||
final res = await _http.post(
|
||||
_uri('/api/auth/login'),
|
||||
headers: _headers(null),
|
||||
body: jsonEncode({'email': email, 'password': password}),
|
||||
);
|
||||
final data = _decode(res) as Map<String, dynamic>;
|
||||
storage.jwt = data['access_token'] as String?;
|
||||
final user = data['user'] as Map<String, dynamic>?;
|
||||
storage.userEmail = user?['email'] as String?;
|
||||
return storage.userEmail ?? email;
|
||||
}
|
||||
|
||||
/// Registers this device and stores the returned device token.
|
||||
Future<void> registerDevice({
|
||||
required String deviceId,
|
||||
required String name,
|
||||
String platform = 'android',
|
||||
String appVersion = '1.0.0',
|
||||
String? pushToken,
|
||||
}) async {
|
||||
final res = await _http.post(
|
||||
_uri('/api/mobile/v1/device'),
|
||||
headers: _headers(storage.jwt),
|
||||
body: jsonEncode({
|
||||
'device_id': deviceId,
|
||||
'name': name,
|
||||
'platform': platform,
|
||||
'app_version': appVersion,
|
||||
if (pushToken != null) 'push_token': pushToken,
|
||||
}),
|
||||
);
|
||||
final data = _decode(res) as Map<String, dynamic>;
|
||||
storage.deviceId = deviceId;
|
||||
storage.deviceName = name;
|
||||
storage.deviceToken = data['auth_token'] as String?;
|
||||
}
|
||||
|
||||
/// Pulls pending messages for this device (the server marks them Processed).
|
||||
Future<List<GatewayMessage>> pullMessages() async {
|
||||
final res = await _http.get(
|
||||
_uri('/api/mobile/v1/messages'),
|
||||
headers: _headers(storage.deviceToken),
|
||||
);
|
||||
final data = _decode(res) as Map<String, dynamic>;
|
||||
final items = (data['items'] as List<dynamic>? ?? []);
|
||||
return items
|
||||
.map((e) => GatewayMessage.fromJson(e as Map<String, dynamic>))
|
||||
.toList();
|
||||
}
|
||||
|
||||
/// Reports a message's delivery state back to the server.
|
||||
Future<void> reportMessage(String id, String status, {String? error}) async {
|
||||
final res = await _http.patch(
|
||||
_uri('/api/mobile/v1/messages/$id'),
|
||||
headers: _headers(storage.deviceToken),
|
||||
body: jsonEncode({'status': status, if (error != null) 'error': error}),
|
||||
);
|
||||
_decode(res);
|
||||
}
|
||||
|
||||
/// Posts an incoming SMS to the server's inbox.
|
||||
Future<void> postInbox(String phoneNumber, String message,
|
||||
{DateTime? receivedAt}) async {
|
||||
final res = await _http.post(
|
||||
_uri('/api/mobile/v1/inbox'),
|
||||
headers: _headers(storage.deviceToken),
|
||||
body: jsonEncode({
|
||||
'phone_number': phoneNumber,
|
||||
'message': message,
|
||||
if (receivedAt != null)
|
||||
'received_at': receivedAt.toUtc().toIso8601String(),
|
||||
}),
|
||||
);
|
||||
_decode(res);
|
||||
}
|
||||
|
||||
/// Sends a heartbeat ping.
|
||||
Future<void> ping() async {
|
||||
final res = await _http.post(
|
||||
_uri('/api/mobile/v1/ping'),
|
||||
headers: _headers(storage.deviceToken),
|
||||
);
|
||||
_decode(res);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user