feat: implement Iteration 0 foundation (backend + Flutter client)
Backend (Go): - Project structure with chi router, pgxpool, goose migrations - JWT auth (access/refresh tokens) with Firebase token verification - NoopTokenVerifier for local dev without Firebase credentials - PostgreSQL user repository with atomic profile updates (transactions) - Mifflin-St Jeor calorie calculation based on profile data - REST API: POST /auth/login, /auth/refresh, /auth/logout, GET/PUT /profile, GET /health - Middleware: auth, CORS (localhost wildcard), logging, recovery, request_id - Unit tests (51 passing) and integration tests (testcontainers) - Docker Compose setup with postgres healthcheck and graceful shutdown Flutter client: - Riverpod state management with GoRouter navigation - Firebase Auth (email/password + Google sign-in with web popup support) - Platform-aware API URLs (web/Android/iOS) - Dio HTTP client with JWT auth interceptor and concurrent refresh handling - Secure token storage - Screens: Login, Register, Home (tabs: Menu, Recipes, Products, Profile) - Unit tests (17 passing) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
47
client/lib/shared/models/user.dart
Normal file
47
client/lib/shared/models/user.dart
Normal file
@@ -0,0 +1,47 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
part 'user.g.dart';
|
||||
|
||||
@JsonSerializable()
|
||||
class User {
|
||||
final String id;
|
||||
final String email;
|
||||
final String name;
|
||||
@JsonKey(name: 'avatar_url')
|
||||
final String? avatarUrl;
|
||||
@JsonKey(name: 'height_cm')
|
||||
final int? heightCm;
|
||||
@JsonKey(name: 'weight_kg')
|
||||
final double? weightKg;
|
||||
final int? age;
|
||||
final String? gender;
|
||||
final String? activity;
|
||||
final String? goal;
|
||||
@JsonKey(name: 'daily_calories')
|
||||
final int? dailyCalories;
|
||||
final String plan;
|
||||
@JsonKey(defaultValue: {})
|
||||
final Map<String, dynamic> preferences;
|
||||
|
||||
const User({
|
||||
required this.id,
|
||||
required this.email,
|
||||
required this.name,
|
||||
this.avatarUrl,
|
||||
this.heightCm,
|
||||
this.weightKg,
|
||||
this.age,
|
||||
this.gender,
|
||||
this.activity,
|
||||
this.goal,
|
||||
this.dailyCalories,
|
||||
required this.plan,
|
||||
this.preferences = const {},
|
||||
});
|
||||
|
||||
factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
|
||||
Map<String, dynamic> toJson() => _$UserToJson(this);
|
||||
|
||||
bool get hasCompletedOnboarding =>
|
||||
heightCm != null && weightKg != null && age != null && gender != null;
|
||||
}
|
||||
39
client/lib/shared/models/user.g.dart
Normal file
39
client/lib/shared/models/user.g.dart
Normal file
@@ -0,0 +1,39 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'user.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
User _$UserFromJson(Map<String, dynamic> json) => User(
|
||||
id: json['id'] as String,
|
||||
email: json['email'] as String,
|
||||
name: json['name'] as String,
|
||||
avatarUrl: json['avatar_url'] as String?,
|
||||
heightCm: (json['height_cm'] as num?)?.toInt(),
|
||||
weightKg: (json['weight_kg'] as num?)?.toDouble(),
|
||||
age: (json['age'] as num?)?.toInt(),
|
||||
gender: json['gender'] as String?,
|
||||
activity: json['activity'] as String?,
|
||||
goal: json['goal'] as String?,
|
||||
dailyCalories: (json['daily_calories'] as num?)?.toInt(),
|
||||
plan: json['plan'] as String,
|
||||
preferences: json['preferences'] as Map<String, dynamic>? ?? {},
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$UserToJson(User instance) => <String, dynamic>{
|
||||
'id': instance.id,
|
||||
'email': instance.email,
|
||||
'name': instance.name,
|
||||
'avatar_url': instance.avatarUrl,
|
||||
'height_cm': instance.heightCm,
|
||||
'weight_kg': instance.weightKg,
|
||||
'age': instance.age,
|
||||
'gender': instance.gender,
|
||||
'activity': instance.activity,
|
||||
'goal': instance.goal,
|
||||
'daily_calories': instance.dailyCalories,
|
||||
'plan': instance.plan,
|
||||
'preferences': instance.preferences,
|
||||
};
|
||||
Reference in New Issue
Block a user