schema fix

Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
Sven laptop
2026-05-05 19:55:51 +02:00
parent c847d32743
commit 8d4e8b3075
7 changed files with 99 additions and 44 deletions
+3
View File
@@ -0,0 +1,3 @@
description: This file stores settings for Dart & Flutter DevTools.
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
extensions:
+44 -6
View File
@@ -1,7 +1,7 @@
/// User model /// User model
class User { class User {
final int id; final int id;
final String userCode; final int userCode;
final String? job; final String? job;
User({ User({
@@ -11,9 +11,20 @@ class User {
}); });
factory User.fromJson(Map<String, dynamic> json) { factory User.fromJson(Map<String, dynamic> json) {
// userCode may be provided as 'userCode' or 'usercode' and may be int or string
final dynamic rawUserCode = json['userCode'] ?? json['usercode'] ?? json['user_code'];
int parsedUserCode;
if (rawUserCode is int) {
parsedUserCode = rawUserCode;
} else if (rawUserCode is String) {
parsedUserCode = int.tryParse(rawUserCode) ?? (throw FormatException('Invalid userCode'));
} else {
throw FormatException('Missing userCode');
}
return User( return User(
id: json['id'] as int, id: json['id'] as int,
userCode: json['userCode'] as String, userCode: parsedUserCode,
job: json['job'] as String?, job: json['job'] as String?,
); );
} }
@@ -30,17 +41,44 @@ class User {
/// Login response model /// Login response model
class AuthResponse { class AuthResponse {
final String token; final String token;
final User user; final User? user;
AuthResponse({ AuthResponse({
required this.token, required this.token,
required this.user, this.user,
}); });
factory AuthResponse.fromJson(Map<String, dynamic> json) { factory AuthResponse.fromJson(Map<String, dynamic> json) {
final dynamic rawToken = json['token'] ?? json['Token'] ?? json['authToken'];
final token = rawToken?.toString();
if (token == null || token.isEmpty) throw FormatException('Missing token in auth response');
// Determine if a user object exists and appears valid
Map<String, dynamic>? userMap;
if (json.containsKey('user') && json['user'] is Map<String, dynamic>) {
userMap = json['user'] as Map<String, dynamic>;
} else {
// maybe user fields are top-level (id, userCode, job)
final hasUserFields = json.containsKey('id') || json.containsKey('userCode') || json.containsKey('usercode') || json.containsKey('user_code');
if (hasUserFields) {
// treat whole json as user map
userMap = json.cast<String, dynamic>();
}
}
User? parsedUser;
if (userMap != null) {
try {
parsedUser = User.fromJson(userMap);
} catch (e) {
// If parsing user fails, ignore user but keep token
parsedUser = null;
}
}
return AuthResponse( return AuthResponse(
token: json['token'] as String, token: token,
user: User.fromJson(json['user'] as Map<String, dynamic>), user: parsedUser,
); );
} }
} }
+12 -2
View File
@@ -34,8 +34,14 @@ class _LoginScreenState extends State<LoginScreen> {
}); });
try { try {
final input = _userCodeController.text.trim();
final parsedUserCode = int.tryParse(input);
if (parsedUserCode == null) {
throw Exception('User code must be a number');
}
final response = await _authService.login( final response = await _authService.login(
userCode: _userCodeController.text.trim(), userCode: parsedUserCode,
password: _passwordController.text, password: _passwordController.text,
); );
@@ -57,9 +63,13 @@ class _LoginScreenState extends State<LoginScreen> {
} }
String? _validateUserCode(String? value) { String? _validateUserCode(String? value) {
if (value?.isEmpty ?? true) { final v = value?.trim();
if (v?.isEmpty ?? true) {
return 'User code is required'; return 'User code is required';
} }
if (int.tryParse(v!) == null) {
return 'User code must be a number';
}
return null; return null;
} }
+14 -7
View File
@@ -1,4 +1,5 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:ffi';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import '../models/user.dart'; import '../models/user.dart';
@@ -6,7 +7,7 @@ import '../models/user.dart';
/// Authentication Service /// Authentication Service
class AuthService { class AuthService {
// TODO: Implement API connector - configure base URL from env/config // TODO: Implement API connector - configure base URL from env/config
static const String _apiBaseUrl = 'http://localhost:5000'; static const String _apiBaseUrl = 'http://192.168.2.66:5000';
static const String _tokenKey = 'auth_token'; static const String _tokenKey = 'auth_token';
late SharedPreferences _prefs; late SharedPreferences _prefs;
@@ -17,7 +18,7 @@ class AuthService {
/// Login with userCode and password /// Login with userCode and password
Future<AuthResponse> login({ Future<AuthResponse> login({
required String userCode, required int userCode,
required String password, required String password,
}) async { }) async {
await _initPrefs(); await _initPrefs();
@@ -33,13 +34,19 @@ class AuthService {
); );
if (response.statusCode == 200) { if (response.statusCode == 200) {
final data = jsonDecode(response.body) as Map<String, dynamic>; final body = response.body;
final authResponse = AuthResponse.fromJson(data); try {
final data = jsonDecode(body) as Map<String, dynamic>;
final authResponse = AuthResponse.fromJson(data);
// Store token // Store token
await _prefs.setString(_tokenKey, authResponse.token); await _prefs.setString(_tokenKey, authResponse.token);
return authResponse; return authResponse;
} catch (e) {
// Surface the response body to help debug backend field names / formats
throw Exception('Login parse error: $e -- response body: $body');
}
} else if (response.statusCode == 401) { } else if (response.statusCode == 401) {
throw Exception('Invalid credentials'); throw Exception('Invalid credentials');
} else { } else {
@@ -6,7 +6,8 @@ import 'auth_service.dart';
/// Schedule/Route Service /// Schedule/Route Service
class ScheduleService { class ScheduleService {
// TODO: Implement API connector - configure base URL from env/config // TODO: Implement API connector - configure base URL from env/config
static const String _apiBaseUrl = 'http://localhost:5000'; // Transit API runs on port 5001
static const String _apiBaseUrl = 'http://192.168.2.66:5001';
final _authService = AuthService(); final _authService = AuthService();
/// Get daily schedule for a specific date /// Get daily schedule for a specific date
@@ -43,8 +44,9 @@ class ScheduleService {
try { try {
final headers = await _authService.getAuthHeaders(); final headers = await _authService.getAuthHeaders();
// Transit API exposes route details at /routes/:routeId
final response = await http.get( final response = await http.get(
Uri.parse('$_apiBaseUrl/schedule/route/$routeId'), Uri.parse('$_apiBaseUrl/routes/$routeId'),
headers: headers, headers: headers,
); );
@@ -66,8 +68,9 @@ class ScheduleService {
try { try {
final headers = await _authService.getAuthHeaders(); final headers = await _authService.getAuthHeaders();
// Transit API updates status at /routes/:routeId/status
final response = await http.patch( final response = await http.patch(
Uri.parse('$_apiBaseUrl/schedule/route/$routeId/status'), Uri.parse('$_apiBaseUrl/routes/$routeId/status'),
headers: headers, headers: headers,
body: jsonEncode({'status': status}), body: jsonEncode({'status': status}),
); );
+18 -24
View File
@@ -2,51 +2,45 @@ version: '3.9'
services: services:
auth_api: auth_api:
image: ghcr.io/Sven491/XTransit/auth_api:latest image: ghcr.io/sven491/xtransit/auth_api:latest
container_name: auth_api container_name: auth_api
ports: ports:
- "5000:5000" - "5000:5000"
environment: environment:
PORT: 5000 PORT: 5000
JWT_SECRET: ${JWT_SECRET} JWT_SECRET: ${JWT_SECRET}
DB_HOST: ${POSTGRES_HOST} PG_HOST: ${POSTGRES_HOST}
DB_PORT: ${POSTGRES_PORT} PG_PORT: ${POSTGRES_PORT}
DB_NAME: ${POSTGRES_DB} PG_DB: ${POSTGRES_DB}
DB_USER: ${POSTGRES_USER_AUTH} PG_USER: ${POSTGRES_USER_AUTH}
DB_PASSWORD: ${POSTGRES_PASSWORD_AUTH} PG_PASS: ${POSTGRES_PASSWORD_AUTH}
depends_on:
db:
condition: service_healthy
restart: unless-stopped restart: unless-stopped
employee_api: employee_api:
image: ghcr.io/Sven491/XTransit/employee_api:latest image: ghcr.io/sven491/xtransit/employee_api:latest
container_name: employee_api container_name: employee_api
ports: ports:
- "4000:4000" - "4000:4000"
environment: environment:
PORT: 4000 PORT: 4000
DB_HOST: ${POSTGRES_HOST} PG_HOST: ${POSTGRES_HOST}
DB_PORT: ${POSTGRES_PORT} PG_PORT: ${POSTGRES_PORT}
DB_NAME: ${POSTGRES_DB} PG_DB: ${POSTGRES_DB}
DB_USER: ${POSTGRES_USER_EMPLOYEE} PG_USER: ${POSTGRES_USER_EMPLOYEE}
DB_PASSWORD: ${POSTGRES_PASSWORD_EMPLOYEE} PG_PASS: ${POSTGRES_PASSWORD_EMPLOYEE}
depends_on:
db:
condition: service_healthy
restart: unless-stopped restart: unless-stopped
transit_api: transit_api:
image: ghcr.io/Sven491/XTransit/transit_api:latest image: ghcr.io/sven491/xtransit/transit_api:latest
container_name: transit_api container_name: transit_api
ports: ports:
- "5001:5001" - "5001:5001"
environment: environment:
PORT: 5001 PORT: 5001
JWT_SECRET: ${JWT_SECRET} JWT_SECRET: ${JWT_SECRET}
DB_HOST: ${POSTGRES_HOST} PG_HOST: ${POSTGRES_HOST}
DB_PORT: ${POSTGRES_PORT} PG_PORT: ${POSTGRES_PORT}
DB_NAME: ${POSTGRES_DB} PG_DB: ${POSTGRES_DB}
DB_USER: ${POSTGRES_USER_TRANSIT} PG_USER: ${POSTGRES_USER_TRANSIT}
DB_PASSWORD: ${POSTGRES_PASSWORD_TRANSIT} PG_PASS: ${POSTGRES_PASSWORD_TRANSIT}
restart: unless-stopped restart: unless-stopped
+1 -1
View File
@@ -37,7 +37,7 @@ app.get('/schedule/daily', authMiddleware, async (req, res) => {
bt.name as bus_type, bt.name as bus_type,
bt.seat_capacity, bt.seat_capacity,
bt.license_plate bt.license_plate
FROM routes r FROM transit.routes r
JOIN bus_lines bl ON r.bus_line_id = bl.id JOIN bus_lines bl ON r.bus_line_id = bl.id
JOIN bus_types bt ON r.bus_type_id = bt.id JOIN bus_types bt ON r.bus_type_id = bt.id
WHERE r.user_id = $1 AND DATE(r.start_time) = $2 WHERE r.user_id = $1 AND DATE(r.start_time) = $2