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:
dbastrikin
2026-02-20 13:14:58 +02:00
commit 24219b611e
140 changed files with 13062 additions and 0 deletions

View File

@@ -0,0 +1,158 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../../core/auth/auth_provider.dart';
import '../../core/theme/app_colors.dart';
class RegisterScreen extends ConsumerStatefulWidget {
const RegisterScreen({super.key});
@override
ConsumerState<RegisterScreen> createState() => _RegisterScreenState();
}
class _RegisterScreenState extends ConsumerState<RegisterScreen> {
final _formKey = GlobalKey<FormState>();
final _nameController = TextEditingController();
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
final _confirmPasswordController = TextEditingController();
@override
void dispose() {
_nameController.dispose();
_emailController.dispose();
_passwordController.dispose();
_confirmPasswordController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final authState = ref.watch(authProvider);
ref.listen<AuthState>(authProvider, (previous, next) {
if (next.status == AuthStatus.authenticated) {
context.go('/home');
}
if (next.error != null) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(next.error!),
backgroundColor: AppColors.error,
),
);
}
});
return Scaffold(
appBar: AppBar(title: const Text('Регистрация')),
body: SafeArea(
child: SingleChildScrollView(
padding: const EdgeInsets.all(24),
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const SizedBox(height: 24),
TextFormField(
controller: _nameController,
textCapitalization: TextCapitalization.words,
decoration: const InputDecoration(
labelText: 'Имя',
prefixIcon: Icon(Icons.person_outlined),
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Введите имя';
}
return null;
},
),
const SizedBox(height: 16),
TextFormField(
controller: _emailController,
keyboardType: TextInputType.emailAddress,
decoration: const InputDecoration(
labelText: 'Email',
prefixIcon: Icon(Icons.email_outlined),
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Введите email';
}
if (!RegExp(r'^[^@]+@[^@]+\.[^@]+').hasMatch(value)) {
return 'Некорректный email';
}
return null;
},
),
const SizedBox(height: 16),
TextFormField(
controller: _passwordController,
obscureText: true,
decoration: const InputDecoration(
labelText: 'Пароль',
prefixIcon: Icon(Icons.lock_outlined),
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Введите пароль';
}
if (value.length < 6) {
return 'Минимум 6 символов';
}
return null;
},
),
const SizedBox(height: 16),
TextFormField(
controller: _confirmPasswordController,
obscureText: true,
decoration: const InputDecoration(
labelText: 'Подтверждение пароля',
prefixIcon: Icon(Icons.lock_outlined),
),
validator: (value) {
if (value != _passwordController.text) {
return 'Пароли не совпадают';
}
return null;
},
),
const SizedBox(height: 32),
ElevatedButton(
onPressed: authState.isLoading ? null : _register,
child: authState.isLoading
? const SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Text('Зарегистрироваться'),
),
const SizedBox(height: 16),
TextButton(
onPressed: () => context.go('/auth/login'),
child: const Text('Уже есть аккаунт? Войти'),
),
],
),
),
),
),
);
}
void _register() {
if (_formKey.currentState!.validate()) {
ref.read(authProvider.notifier).register(
_emailController.text.trim(),
_passwordController.text,
_nameController.text.trim(),
);
}
}
}