feat(online): session compte, stockage sécurisé et adapter API (ticket #64)
Ajoute l'adapter API distant (infrastructure/remote/auth_api.dart, http_api_client.dart) et le stockage sécurisé du token d'auth (infrastructure/security/secure_storage_auth_token_store.dart), câblés dans app_bootstrap.dart. Étend le modèle Drift (migration schemaVersion 9→10) et la couche application (ports, use cases, entités) pour la session de compte. flutter pub get OK (ajout http, flutter_secure_storage), build_runner OK, dart format appliqué, analyze propre (mêmes infos préexistantes), 109/109 tests verts, build APK debug validé. Premier ticket du chantier "Ajouter les features serveur au client" (#63). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -1,7 +1,10 @@
|
||||
import '../infrastructure/local/local.dart';
|
||||
import '../infrastructure/remote/remote.dart';
|
||||
import '../infrastructure/security/security.dart';
|
||||
import 'application.dart';
|
||||
|
||||
abstract interface class AppDependencies {
|
||||
AuthUseCases get authUseCases;
|
||||
ExerciseUseCases get exerciseUseCases;
|
||||
MediaUseCases get mediaUseCases;
|
||||
ProgramUseCases get programUseCases;
|
||||
@ -15,6 +18,7 @@ abstract interface class AppDependencies {
|
||||
final class AppBootstrap implements AppDependencies {
|
||||
AppBootstrap._({
|
||||
required this.database,
|
||||
required this.authUseCases,
|
||||
required this.exerciseUseCases,
|
||||
required this.mediaUseCases,
|
||||
required this.programUseCases,
|
||||
@ -28,6 +32,8 @@ final class AppBootstrap implements AppDependencies {
|
||||
|
||||
final AppDatabase database;
|
||||
@override
|
||||
final AuthUseCases authUseCases;
|
||||
@override
|
||||
final ExerciseUseCases exerciseUseCases;
|
||||
@override
|
||||
final MediaUseCases mediaUseCases;
|
||||
@ -50,6 +56,7 @@ final class AppBootstrap implements AppDependencies {
|
||||
await database.customSelect('SELECT 1').get();
|
||||
final exerciseRepository = DriftExerciseRepository(database);
|
||||
final mediaRepository = DriftMediaAssetRepository(database);
|
||||
final onlineAccountRepository = DriftOnlineAccountRepository(database);
|
||||
final programRepository = DriftProgramRepository(database);
|
||||
final templateRepository = DriftWorkoutTemplateRepository(database);
|
||||
final activeSessionRepository = DriftActiveSessionRepository(database);
|
||||
@ -57,9 +64,19 @@ final class AppBootstrap implements AppDependencies {
|
||||
final ids = LocalIdGenerator();
|
||||
const clock = SystemClock();
|
||||
const originDeviceId = 'local-device';
|
||||
final remoteAuthApi = HttpRemoteAuthApi(
|
||||
HttpApiClient(baseUrl: Uri.parse(HttpApiClient.defaultBaseUrl)),
|
||||
);
|
||||
|
||||
return AppBootstrap._(
|
||||
database: database,
|
||||
authUseCases: AuthUseCases(
|
||||
tokenStore: const SecureStorageAuthTokenStore(),
|
||||
accountRepository: onlineAccountRepository,
|
||||
remoteAuthApi: remoteAuthApi,
|
||||
clock: clock,
|
||||
ids: ids,
|
||||
),
|
||||
exerciseUseCases: ExerciseUseCases(
|
||||
repository: exerciseRepository,
|
||||
programRepository: programRepository,
|
||||
|
||||
@ -15,6 +15,64 @@ abstract interface class IdGenerator {
|
||||
String newId();
|
||||
}
|
||||
|
||||
enum RemoteAuthFailure {
|
||||
invalidCredentials,
|
||||
emailAlreadyUsed,
|
||||
network,
|
||||
unknown,
|
||||
}
|
||||
|
||||
final class RemoteAuthException implements Exception {
|
||||
const RemoteAuthException(this.failure, [this.message]);
|
||||
|
||||
final RemoteAuthFailure failure;
|
||||
final String? message;
|
||||
|
||||
@override
|
||||
String toString() => message ?? failure.name;
|
||||
}
|
||||
|
||||
final class RemoteAuthResult {
|
||||
const RemoteAuthResult({
|
||||
required this.userId,
|
||||
required this.email,
|
||||
required this.token,
|
||||
required this.expiresAt,
|
||||
});
|
||||
|
||||
final String userId;
|
||||
final String email;
|
||||
final String token;
|
||||
final DateTime expiresAt;
|
||||
}
|
||||
|
||||
abstract interface class AuthTokenStore {
|
||||
Future<void> saveToken(String token, DateTime expiresAt);
|
||||
Future<String?> readToken();
|
||||
Future<void> clearToken();
|
||||
}
|
||||
|
||||
abstract interface class OnlineAccountRepository {
|
||||
Future<UserAccountSession?> currentSession();
|
||||
Future<void> saveSession(UserAccountSession session);
|
||||
Future<void> clearSession();
|
||||
}
|
||||
|
||||
abstract interface class RemoteAuthApi {
|
||||
Future<RemoteAuthResult> register({
|
||||
required String email,
|
||||
required String password,
|
||||
String? displayName,
|
||||
});
|
||||
|
||||
Future<RemoteAuthResult> login({
|
||||
required String email,
|
||||
required String password,
|
||||
});
|
||||
|
||||
Future<void> logout(String token);
|
||||
}
|
||||
|
||||
abstract interface class ExerciseRepository {
|
||||
Future<Exercise?> findById(String id);
|
||||
Future<List<Exercise>> listActive();
|
||||
|
||||
@ -5,6 +5,107 @@ import 'ports.dart';
|
||||
|
||||
const Object _useCaseUnchanged = Object();
|
||||
|
||||
final class AuthUseCases {
|
||||
const AuthUseCases({
|
||||
required this.tokenStore,
|
||||
required this.accountRepository,
|
||||
required this.remoteAuthApi,
|
||||
required this.clock,
|
||||
required this.ids,
|
||||
});
|
||||
|
||||
final AuthTokenStore tokenStore;
|
||||
final OnlineAccountRepository accountRepository;
|
||||
final RemoteAuthApi remoteAuthApi;
|
||||
final Clock clock;
|
||||
final IdGenerator ids;
|
||||
|
||||
Future<UserAccountSession> register({
|
||||
required String email,
|
||||
required String password,
|
||||
String? displayName,
|
||||
}) async {
|
||||
_validateAuthInput(email: email, password: password);
|
||||
final result = await remoteAuthApi.register(
|
||||
email: email.trim(),
|
||||
password: password,
|
||||
displayName: displayName,
|
||||
);
|
||||
return _persistAuthenticatedSession(result, displayName: displayName);
|
||||
}
|
||||
|
||||
Future<UserAccountSession> login({
|
||||
required String email,
|
||||
required String password,
|
||||
}) async {
|
||||
_validateAuthInput(email: email, password: password);
|
||||
final result = await remoteAuthApi.login(
|
||||
email: email.trim(),
|
||||
password: password,
|
||||
);
|
||||
return _persistAuthenticatedSession(result);
|
||||
}
|
||||
|
||||
Future<void> logout() async {
|
||||
final token = await tokenStore.readToken();
|
||||
if (token != null) {
|
||||
try {
|
||||
await remoteAuthApi.logout(token);
|
||||
} on RemoteAuthException {
|
||||
// Local logout must not depend on remote reachability.
|
||||
}
|
||||
}
|
||||
await tokenStore.clearToken();
|
||||
final session = await accountRepository.currentSession();
|
||||
if (session != null) {
|
||||
final now = clock.now();
|
||||
await accountRepository.saveSession(
|
||||
session.copyWith(isLoggedIn: false, updatedAt: now),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<UserAccountSession?> currentSession() async {
|
||||
final session = await accountRepository.currentSession();
|
||||
if (session == null) {
|
||||
return null;
|
||||
}
|
||||
final token = await tokenStore.readToken();
|
||||
if (token != null || !session.isLoggedIn) {
|
||||
return session;
|
||||
}
|
||||
final updated = session.copyWith(isLoggedIn: false, updatedAt: clock.now());
|
||||
await accountRepository.saveSession(updated);
|
||||
return updated;
|
||||
}
|
||||
|
||||
Future<UserAccountSession> _persistAuthenticatedSession(
|
||||
RemoteAuthResult result, {
|
||||
String? displayName,
|
||||
}) async {
|
||||
await tokenStore.saveToken(result.token, result.expiresAt);
|
||||
final now = clock.now();
|
||||
final existing = await accountRepository.currentSession();
|
||||
final session = UserAccountSession(
|
||||
id: existing?.id ?? ids.newId(),
|
||||
serverUserId:
|
||||
existing != null &&
|
||||
existing.email.trim().toLowerCase() ==
|
||||
result.email.trim().toLowerCase()
|
||||
? existing.serverUserId
|
||||
: result.userId,
|
||||
email: result.email,
|
||||
displayName: displayName ?? existing?.displayName,
|
||||
isLoggedIn: true,
|
||||
createdAt: existing?.createdAt ?? now,
|
||||
updatedAt: now,
|
||||
lastAuthenticatedAt: now,
|
||||
);
|
||||
await accountRepository.saveSession(session);
|
||||
return session;
|
||||
}
|
||||
}
|
||||
|
||||
final class ExerciseUseCases {
|
||||
const ExerciseUseCases({
|
||||
required this.repository,
|
||||
@ -2264,6 +2365,19 @@ void _validateExerciseSteps(List<ExerciseStep> steps) {
|
||||
}
|
||||
}
|
||||
|
||||
void _validateAuthInput({required String email, required String password}) {
|
||||
final normalizedEmail = email.trim();
|
||||
final hasBasicEmailShape = RegExp(
|
||||
r'^[^@\s]+@[^@\s]+\.[^@\s]+$',
|
||||
).hasMatch(normalizedEmail);
|
||||
if (!hasBasicEmailShape) {
|
||||
throw const DomainException('Email format is invalid.');
|
||||
}
|
||||
if (password.length < 8) {
|
||||
throw const DomainException('Password must contain at least 8 characters.');
|
||||
}
|
||||
}
|
||||
|
||||
void _validateExerciseDefaultTargets({
|
||||
required bool hasTimeMeasure,
|
||||
required bool hasRepsMeasure,
|
||||
|
||||
@ -102,6 +102,58 @@ final class EntityMetadata {
|
||||
}
|
||||
}
|
||||
|
||||
final class UserAccountSession {
|
||||
UserAccountSession({
|
||||
required String id,
|
||||
required String serverUserId,
|
||||
required String email,
|
||||
this.displayName,
|
||||
required this.isLoggedIn,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
this.lastAuthenticatedAt,
|
||||
}) : id = _nonBlank(id, 'User account session id'),
|
||||
serverUserId = _nonBlank(serverUserId, 'Server user id'),
|
||||
email = _nonBlank(email, 'Email') {
|
||||
if (displayName != null && displayName!.trim().isEmpty) {
|
||||
throw const DomainException('Display name must not be blank.');
|
||||
}
|
||||
}
|
||||
|
||||
final String id;
|
||||
final String serverUserId;
|
||||
final String email;
|
||||
final String? displayName;
|
||||
final bool isLoggedIn;
|
||||
final DateTime createdAt;
|
||||
final DateTime updatedAt;
|
||||
final DateTime? lastAuthenticatedAt;
|
||||
|
||||
UserAccountSession copyWith({
|
||||
String? serverUserId,
|
||||
String? email,
|
||||
Object? displayName = _unchanged,
|
||||
bool? isLoggedIn,
|
||||
DateTime? updatedAt,
|
||||
Object? lastAuthenticatedAt = _unchanged,
|
||||
}) {
|
||||
return UserAccountSession(
|
||||
id: id,
|
||||
serverUserId: serverUserId ?? this.serverUserId,
|
||||
email: email ?? this.email,
|
||||
displayName: displayName == _unchanged
|
||||
? this.displayName
|
||||
: displayName as String?,
|
||||
isLoggedIn: isLoggedIn ?? this.isLoggedIn,
|
||||
createdAt: createdAt,
|
||||
updatedAt: updatedAt ?? this.updatedAt,
|
||||
lastAuthenticatedAt: lastAuthenticatedAt == _unchanged
|
||||
? this.lastAuthenticatedAt
|
||||
: lastAuthenticatedAt as DateTime?,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final class MediaAsset {
|
||||
const MediaAsset({
|
||||
required this.metadata,
|
||||
@ -997,7 +1049,9 @@ final class ActiveExerciseStepProgressState {
|
||||
currentStepSnapshotId:
|
||||
currentStepSnapshotId ?? this.currentStepSnapshotId,
|
||||
status: status ?? this.status,
|
||||
startedAt: startedAt == _unchanged ? this.startedAt : startedAt as DateTime?,
|
||||
startedAt: startedAt == _unchanged
|
||||
? this.startedAt
|
||||
: startedAt as DateTime?,
|
||||
accumulatedMs: accumulatedMs ?? this.accumulatedMs,
|
||||
lastTransitionAt: lastTransitionAt ?? this.lastTransitionAt,
|
||||
);
|
||||
|
||||
@ -3,3 +3,7 @@
|
||||
/// Concrete adapters live here and depend inward on application/domain
|
||||
/// contracts.
|
||||
library;
|
||||
|
||||
export 'local/local.dart';
|
||||
export 'remote/remote.dart';
|
||||
export 'security/security.dart';
|
||||
|
||||
@ -18,6 +18,7 @@ part 'app_database.g.dart';
|
||||
ExerciseImages,
|
||||
ExerciseSteps,
|
||||
MediaAssets,
|
||||
OnlineAccountSessions,
|
||||
ProgramExercises,
|
||||
Programs,
|
||||
WorkoutHistories,
|
||||
@ -41,7 +42,7 @@ final class AppDatabase extends _$AppDatabase {
|
||||
}
|
||||
|
||||
@override
|
||||
int get schemaVersion => 9;
|
||||
int get schemaVersion => 10;
|
||||
|
||||
@override
|
||||
MigrationStrategy get migration {
|
||||
@ -83,6 +84,9 @@ final class AppDatabase extends _$AppDatabase {
|
||||
if (from < 9) {
|
||||
await _migrateToSchema9(migrator);
|
||||
}
|
||||
if (from < 10) {
|
||||
await _migrateToSchema10(migrator);
|
||||
}
|
||||
await _createIndexes();
|
||||
},
|
||||
beforeOpen: (details) async {
|
||||
@ -123,6 +127,10 @@ final class AppDatabase extends _$AppDatabase {
|
||||
'CREATE INDEX IF NOT EXISTS idx_exercise_steps_exercise_id_position '
|
||||
'ON exercise_steps (exercise_id, position)',
|
||||
);
|
||||
await customStatement(
|
||||
'CREATE INDEX IF NOT EXISTS idx_online_account_sessions_logged_in '
|
||||
'ON online_account_sessions (is_logged_in, updated_at)',
|
||||
);
|
||||
await customStatement(
|
||||
'CREATE INDEX IF NOT EXISTS idx_workout_template_programs_template_id '
|
||||
'ON workout_template_programs (workout_template_id)',
|
||||
@ -315,4 +323,8 @@ extension on AppDatabase {
|
||||
await migrator.createTable(activeExerciseStepResults);
|
||||
await migrator.createTable(workoutHistoryStepResults);
|
||||
}
|
||||
|
||||
Future<void> _migrateToSchema10(Migrator migrator) async {
|
||||
await migrator.createTable(onlineAccountSessions);
|
||||
}
|
||||
}
|
||||
|
||||
@ -14914,6 +14914,552 @@ class ExerciseStepsCompanion extends UpdateCompanion<ExerciseStep> {
|
||||
}
|
||||
}
|
||||
|
||||
class $OnlineAccountSessionsTable extends OnlineAccountSessions
|
||||
with TableInfo<$OnlineAccountSessionsTable, OnlineAccountSession> {
|
||||
@override
|
||||
final GeneratedDatabase attachedDatabase;
|
||||
final String? _alias;
|
||||
$OnlineAccountSessionsTable(this.attachedDatabase, [this._alias]);
|
||||
static const VerificationMeta _idMeta = const VerificationMeta('id');
|
||||
@override
|
||||
late final GeneratedColumn<String> id = GeneratedColumn<String>(
|
||||
'id',
|
||||
aliasedName,
|
||||
false,
|
||||
type: DriftSqlType.string,
|
||||
requiredDuringInsert: true,
|
||||
);
|
||||
static const VerificationMeta _serverUserIdMeta = const VerificationMeta(
|
||||
'serverUserId',
|
||||
);
|
||||
@override
|
||||
late final GeneratedColumn<String> serverUserId = GeneratedColumn<String>(
|
||||
'server_user_id',
|
||||
aliasedName,
|
||||
false,
|
||||
additionalChecks: GeneratedColumn.checkTextLength(minTextLength: 1),
|
||||
type: DriftSqlType.string,
|
||||
requiredDuringInsert: true,
|
||||
);
|
||||
static const VerificationMeta _emailMeta = const VerificationMeta('email');
|
||||
@override
|
||||
late final GeneratedColumn<String> email = GeneratedColumn<String>(
|
||||
'email',
|
||||
aliasedName,
|
||||
false,
|
||||
additionalChecks: GeneratedColumn.checkTextLength(minTextLength: 1),
|
||||
type: DriftSqlType.string,
|
||||
requiredDuringInsert: true,
|
||||
);
|
||||
static const VerificationMeta _displayNameMeta = const VerificationMeta(
|
||||
'displayName',
|
||||
);
|
||||
@override
|
||||
late final GeneratedColumn<String> displayName = GeneratedColumn<String>(
|
||||
'display_name',
|
||||
aliasedName,
|
||||
true,
|
||||
type: DriftSqlType.string,
|
||||
requiredDuringInsert: false,
|
||||
);
|
||||
static const VerificationMeta _isLoggedInMeta = const VerificationMeta(
|
||||
'isLoggedIn',
|
||||
);
|
||||
@override
|
||||
late final GeneratedColumn<bool> isLoggedIn = GeneratedColumn<bool>(
|
||||
'is_logged_in',
|
||||
aliasedName,
|
||||
false,
|
||||
type: DriftSqlType.bool,
|
||||
requiredDuringInsert: true,
|
||||
defaultConstraints: GeneratedColumn.constraintIsAlways(
|
||||
'CHECK ("is_logged_in" IN (0, 1))',
|
||||
),
|
||||
);
|
||||
static const VerificationMeta _createdAtMeta = const VerificationMeta(
|
||||
'createdAt',
|
||||
);
|
||||
@override
|
||||
late final GeneratedColumn<DateTime> createdAt = GeneratedColumn<DateTime>(
|
||||
'created_at',
|
||||
aliasedName,
|
||||
false,
|
||||
type: DriftSqlType.dateTime,
|
||||
requiredDuringInsert: true,
|
||||
);
|
||||
static const VerificationMeta _updatedAtMeta = const VerificationMeta(
|
||||
'updatedAt',
|
||||
);
|
||||
@override
|
||||
late final GeneratedColumn<DateTime> updatedAt = GeneratedColumn<DateTime>(
|
||||
'updated_at',
|
||||
aliasedName,
|
||||
false,
|
||||
type: DriftSqlType.dateTime,
|
||||
requiredDuringInsert: true,
|
||||
);
|
||||
static const VerificationMeta _lastAuthenticatedAtMeta =
|
||||
const VerificationMeta('lastAuthenticatedAt');
|
||||
@override
|
||||
late final GeneratedColumn<DateTime> lastAuthenticatedAt =
|
||||
GeneratedColumn<DateTime>(
|
||||
'last_authenticated_at',
|
||||
aliasedName,
|
||||
true,
|
||||
type: DriftSqlType.dateTime,
|
||||
requiredDuringInsert: false,
|
||||
);
|
||||
@override
|
||||
List<GeneratedColumn> get $columns => [
|
||||
id,
|
||||
serverUserId,
|
||||
email,
|
||||
displayName,
|
||||
isLoggedIn,
|
||||
createdAt,
|
||||
updatedAt,
|
||||
lastAuthenticatedAt,
|
||||
];
|
||||
@override
|
||||
String get aliasedName => _alias ?? actualTableName;
|
||||
@override
|
||||
String get actualTableName => $name;
|
||||
static const String $name = 'online_account_sessions';
|
||||
@override
|
||||
VerificationContext validateIntegrity(
|
||||
Insertable<OnlineAccountSession> instance, {
|
||||
bool isInserting = false,
|
||||
}) {
|
||||
final context = VerificationContext();
|
||||
final data = instance.toColumns(true);
|
||||
if (data.containsKey('id')) {
|
||||
context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta));
|
||||
} else if (isInserting) {
|
||||
context.missing(_idMeta);
|
||||
}
|
||||
if (data.containsKey('server_user_id')) {
|
||||
context.handle(
|
||||
_serverUserIdMeta,
|
||||
serverUserId.isAcceptableOrUnknown(
|
||||
data['server_user_id']!,
|
||||
_serverUserIdMeta,
|
||||
),
|
||||
);
|
||||
} else if (isInserting) {
|
||||
context.missing(_serverUserIdMeta);
|
||||
}
|
||||
if (data.containsKey('email')) {
|
||||
context.handle(
|
||||
_emailMeta,
|
||||
email.isAcceptableOrUnknown(data['email']!, _emailMeta),
|
||||
);
|
||||
} else if (isInserting) {
|
||||
context.missing(_emailMeta);
|
||||
}
|
||||
if (data.containsKey('display_name')) {
|
||||
context.handle(
|
||||
_displayNameMeta,
|
||||
displayName.isAcceptableOrUnknown(
|
||||
data['display_name']!,
|
||||
_displayNameMeta,
|
||||
),
|
||||
);
|
||||
}
|
||||
if (data.containsKey('is_logged_in')) {
|
||||
context.handle(
|
||||
_isLoggedInMeta,
|
||||
isLoggedIn.isAcceptableOrUnknown(
|
||||
data['is_logged_in']!,
|
||||
_isLoggedInMeta,
|
||||
),
|
||||
);
|
||||
} else if (isInserting) {
|
||||
context.missing(_isLoggedInMeta);
|
||||
}
|
||||
if (data.containsKey('created_at')) {
|
||||
context.handle(
|
||||
_createdAtMeta,
|
||||
createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta),
|
||||
);
|
||||
} else if (isInserting) {
|
||||
context.missing(_createdAtMeta);
|
||||
}
|
||||
if (data.containsKey('updated_at')) {
|
||||
context.handle(
|
||||
_updatedAtMeta,
|
||||
updatedAt.isAcceptableOrUnknown(data['updated_at']!, _updatedAtMeta),
|
||||
);
|
||||
} else if (isInserting) {
|
||||
context.missing(_updatedAtMeta);
|
||||
}
|
||||
if (data.containsKey('last_authenticated_at')) {
|
||||
context.handle(
|
||||
_lastAuthenticatedAtMeta,
|
||||
lastAuthenticatedAt.isAcceptableOrUnknown(
|
||||
data['last_authenticated_at']!,
|
||||
_lastAuthenticatedAtMeta,
|
||||
),
|
||||
);
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
@override
|
||||
Set<GeneratedColumn> get $primaryKey => {id};
|
||||
@override
|
||||
OnlineAccountSession map(Map<String, dynamic> data, {String? tablePrefix}) {
|
||||
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
|
||||
return OnlineAccountSession(
|
||||
id: attachedDatabase.typeMapping.read(
|
||||
DriftSqlType.string,
|
||||
data['${effectivePrefix}id'],
|
||||
)!,
|
||||
serverUserId: attachedDatabase.typeMapping.read(
|
||||
DriftSqlType.string,
|
||||
data['${effectivePrefix}server_user_id'],
|
||||
)!,
|
||||
email: attachedDatabase.typeMapping.read(
|
||||
DriftSqlType.string,
|
||||
data['${effectivePrefix}email'],
|
||||
)!,
|
||||
displayName: attachedDatabase.typeMapping.read(
|
||||
DriftSqlType.string,
|
||||
data['${effectivePrefix}display_name'],
|
||||
),
|
||||
isLoggedIn: attachedDatabase.typeMapping.read(
|
||||
DriftSqlType.bool,
|
||||
data['${effectivePrefix}is_logged_in'],
|
||||
)!,
|
||||
createdAt: attachedDatabase.typeMapping.read(
|
||||
DriftSqlType.dateTime,
|
||||
data['${effectivePrefix}created_at'],
|
||||
)!,
|
||||
updatedAt: attachedDatabase.typeMapping.read(
|
||||
DriftSqlType.dateTime,
|
||||
data['${effectivePrefix}updated_at'],
|
||||
)!,
|
||||
lastAuthenticatedAt: attachedDatabase.typeMapping.read(
|
||||
DriftSqlType.dateTime,
|
||||
data['${effectivePrefix}last_authenticated_at'],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
$OnlineAccountSessionsTable createAlias(String alias) {
|
||||
return $OnlineAccountSessionsTable(attachedDatabase, alias);
|
||||
}
|
||||
}
|
||||
|
||||
class OnlineAccountSession extends DataClass
|
||||
implements Insertable<OnlineAccountSession> {
|
||||
final String id;
|
||||
final String serverUserId;
|
||||
final String email;
|
||||
final String? displayName;
|
||||
final bool isLoggedIn;
|
||||
final DateTime createdAt;
|
||||
final DateTime updatedAt;
|
||||
final DateTime? lastAuthenticatedAt;
|
||||
const OnlineAccountSession({
|
||||
required this.id,
|
||||
required this.serverUserId,
|
||||
required this.email,
|
||||
this.displayName,
|
||||
required this.isLoggedIn,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
this.lastAuthenticatedAt,
|
||||
});
|
||||
@override
|
||||
Map<String, Expression> toColumns(bool nullToAbsent) {
|
||||
final map = <String, Expression>{};
|
||||
map['id'] = Variable<String>(id);
|
||||
map['server_user_id'] = Variable<String>(serverUserId);
|
||||
map['email'] = Variable<String>(email);
|
||||
if (!nullToAbsent || displayName != null) {
|
||||
map['display_name'] = Variable<String>(displayName);
|
||||
}
|
||||
map['is_logged_in'] = Variable<bool>(isLoggedIn);
|
||||
map['created_at'] = Variable<DateTime>(createdAt);
|
||||
map['updated_at'] = Variable<DateTime>(updatedAt);
|
||||
if (!nullToAbsent || lastAuthenticatedAt != null) {
|
||||
map['last_authenticated_at'] = Variable<DateTime>(lastAuthenticatedAt);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
OnlineAccountSessionsCompanion toCompanion(bool nullToAbsent) {
|
||||
return OnlineAccountSessionsCompanion(
|
||||
id: Value(id),
|
||||
serverUserId: Value(serverUserId),
|
||||
email: Value(email),
|
||||
displayName: displayName == null && nullToAbsent
|
||||
? const Value.absent()
|
||||
: Value(displayName),
|
||||
isLoggedIn: Value(isLoggedIn),
|
||||
createdAt: Value(createdAt),
|
||||
updatedAt: Value(updatedAt),
|
||||
lastAuthenticatedAt: lastAuthenticatedAt == null && nullToAbsent
|
||||
? const Value.absent()
|
||||
: Value(lastAuthenticatedAt),
|
||||
);
|
||||
}
|
||||
|
||||
factory OnlineAccountSession.fromJson(
|
||||
Map<String, dynamic> json, {
|
||||
ValueSerializer? serializer,
|
||||
}) {
|
||||
serializer ??= driftRuntimeOptions.defaultSerializer;
|
||||
return OnlineAccountSession(
|
||||
id: serializer.fromJson<String>(json['id']),
|
||||
serverUserId: serializer.fromJson<String>(json['serverUserId']),
|
||||
email: serializer.fromJson<String>(json['email']),
|
||||
displayName: serializer.fromJson<String?>(json['displayName']),
|
||||
isLoggedIn: serializer.fromJson<bool>(json['isLoggedIn']),
|
||||
createdAt: serializer.fromJson<DateTime>(json['createdAt']),
|
||||
updatedAt: serializer.fromJson<DateTime>(json['updatedAt']),
|
||||
lastAuthenticatedAt: serializer.fromJson<DateTime?>(
|
||||
json['lastAuthenticatedAt'],
|
||||
),
|
||||
);
|
||||
}
|
||||
@override
|
||||
Map<String, dynamic> toJson({ValueSerializer? serializer}) {
|
||||
serializer ??= driftRuntimeOptions.defaultSerializer;
|
||||
return <String, dynamic>{
|
||||
'id': serializer.toJson<String>(id),
|
||||
'serverUserId': serializer.toJson<String>(serverUserId),
|
||||
'email': serializer.toJson<String>(email),
|
||||
'displayName': serializer.toJson<String?>(displayName),
|
||||
'isLoggedIn': serializer.toJson<bool>(isLoggedIn),
|
||||
'createdAt': serializer.toJson<DateTime>(createdAt),
|
||||
'updatedAt': serializer.toJson<DateTime>(updatedAt),
|
||||
'lastAuthenticatedAt': serializer.toJson<DateTime?>(lastAuthenticatedAt),
|
||||
};
|
||||
}
|
||||
|
||||
OnlineAccountSession copyWith({
|
||||
String? id,
|
||||
String? serverUserId,
|
||||
String? email,
|
||||
Value<String?> displayName = const Value.absent(),
|
||||
bool? isLoggedIn,
|
||||
DateTime? createdAt,
|
||||
DateTime? updatedAt,
|
||||
Value<DateTime?> lastAuthenticatedAt = const Value.absent(),
|
||||
}) => OnlineAccountSession(
|
||||
id: id ?? this.id,
|
||||
serverUserId: serverUserId ?? this.serverUserId,
|
||||
email: email ?? this.email,
|
||||
displayName: displayName.present ? displayName.value : this.displayName,
|
||||
isLoggedIn: isLoggedIn ?? this.isLoggedIn,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
updatedAt: updatedAt ?? this.updatedAt,
|
||||
lastAuthenticatedAt: lastAuthenticatedAt.present
|
||||
? lastAuthenticatedAt.value
|
||||
: this.lastAuthenticatedAt,
|
||||
);
|
||||
OnlineAccountSession copyWithCompanion(OnlineAccountSessionsCompanion data) {
|
||||
return OnlineAccountSession(
|
||||
id: data.id.present ? data.id.value : this.id,
|
||||
serverUserId: data.serverUserId.present
|
||||
? data.serverUserId.value
|
||||
: this.serverUserId,
|
||||
email: data.email.present ? data.email.value : this.email,
|
||||
displayName: data.displayName.present
|
||||
? data.displayName.value
|
||||
: this.displayName,
|
||||
isLoggedIn: data.isLoggedIn.present
|
||||
? data.isLoggedIn.value
|
||||
: this.isLoggedIn,
|
||||
createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt,
|
||||
updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt,
|
||||
lastAuthenticatedAt: data.lastAuthenticatedAt.present
|
||||
? data.lastAuthenticatedAt.value
|
||||
: this.lastAuthenticatedAt,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return (StringBuffer('OnlineAccountSession(')
|
||||
..write('id: $id, ')
|
||||
..write('serverUserId: $serverUserId, ')
|
||||
..write('email: $email, ')
|
||||
..write('displayName: $displayName, ')
|
||||
..write('isLoggedIn: $isLoggedIn, ')
|
||||
..write('createdAt: $createdAt, ')
|
||||
..write('updatedAt: $updatedAt, ')
|
||||
..write('lastAuthenticatedAt: $lastAuthenticatedAt')
|
||||
..write(')'))
|
||||
.toString();
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(
|
||||
id,
|
||||
serverUserId,
|
||||
email,
|
||||
displayName,
|
||||
isLoggedIn,
|
||||
createdAt,
|
||||
updatedAt,
|
||||
lastAuthenticatedAt,
|
||||
);
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
(other is OnlineAccountSession &&
|
||||
other.id == this.id &&
|
||||
other.serverUserId == this.serverUserId &&
|
||||
other.email == this.email &&
|
||||
other.displayName == this.displayName &&
|
||||
other.isLoggedIn == this.isLoggedIn &&
|
||||
other.createdAt == this.createdAt &&
|
||||
other.updatedAt == this.updatedAt &&
|
||||
other.lastAuthenticatedAt == this.lastAuthenticatedAt);
|
||||
}
|
||||
|
||||
class OnlineAccountSessionsCompanion
|
||||
extends UpdateCompanion<OnlineAccountSession> {
|
||||
final Value<String> id;
|
||||
final Value<String> serverUserId;
|
||||
final Value<String> email;
|
||||
final Value<String?> displayName;
|
||||
final Value<bool> isLoggedIn;
|
||||
final Value<DateTime> createdAt;
|
||||
final Value<DateTime> updatedAt;
|
||||
final Value<DateTime?> lastAuthenticatedAt;
|
||||
final Value<int> rowid;
|
||||
const OnlineAccountSessionsCompanion({
|
||||
this.id = const Value.absent(),
|
||||
this.serverUserId = const Value.absent(),
|
||||
this.email = const Value.absent(),
|
||||
this.displayName = const Value.absent(),
|
||||
this.isLoggedIn = const Value.absent(),
|
||||
this.createdAt = const Value.absent(),
|
||||
this.updatedAt = const Value.absent(),
|
||||
this.lastAuthenticatedAt = const Value.absent(),
|
||||
this.rowid = const Value.absent(),
|
||||
});
|
||||
OnlineAccountSessionsCompanion.insert({
|
||||
required String id,
|
||||
required String serverUserId,
|
||||
required String email,
|
||||
this.displayName = const Value.absent(),
|
||||
required bool isLoggedIn,
|
||||
required DateTime createdAt,
|
||||
required DateTime updatedAt,
|
||||
this.lastAuthenticatedAt = const Value.absent(),
|
||||
this.rowid = const Value.absent(),
|
||||
}) : id = Value(id),
|
||||
serverUserId = Value(serverUserId),
|
||||
email = Value(email),
|
||||
isLoggedIn = Value(isLoggedIn),
|
||||
createdAt = Value(createdAt),
|
||||
updatedAt = Value(updatedAt);
|
||||
static Insertable<OnlineAccountSession> custom({
|
||||
Expression<String>? id,
|
||||
Expression<String>? serverUserId,
|
||||
Expression<String>? email,
|
||||
Expression<String>? displayName,
|
||||
Expression<bool>? isLoggedIn,
|
||||
Expression<DateTime>? createdAt,
|
||||
Expression<DateTime>? updatedAt,
|
||||
Expression<DateTime>? lastAuthenticatedAt,
|
||||
Expression<int>? rowid,
|
||||
}) {
|
||||
return RawValuesInsertable({
|
||||
if (id != null) 'id': id,
|
||||
if (serverUserId != null) 'server_user_id': serverUserId,
|
||||
if (email != null) 'email': email,
|
||||
if (displayName != null) 'display_name': displayName,
|
||||
if (isLoggedIn != null) 'is_logged_in': isLoggedIn,
|
||||
if (createdAt != null) 'created_at': createdAt,
|
||||
if (updatedAt != null) 'updated_at': updatedAt,
|
||||
if (lastAuthenticatedAt != null)
|
||||
'last_authenticated_at': lastAuthenticatedAt,
|
||||
if (rowid != null) 'rowid': rowid,
|
||||
});
|
||||
}
|
||||
|
||||
OnlineAccountSessionsCompanion copyWith({
|
||||
Value<String>? id,
|
||||
Value<String>? serverUserId,
|
||||
Value<String>? email,
|
||||
Value<String?>? displayName,
|
||||
Value<bool>? isLoggedIn,
|
||||
Value<DateTime>? createdAt,
|
||||
Value<DateTime>? updatedAt,
|
||||
Value<DateTime?>? lastAuthenticatedAt,
|
||||
Value<int>? rowid,
|
||||
}) {
|
||||
return OnlineAccountSessionsCompanion(
|
||||
id: id ?? this.id,
|
||||
serverUserId: serverUserId ?? this.serverUserId,
|
||||
email: email ?? this.email,
|
||||
displayName: displayName ?? this.displayName,
|
||||
isLoggedIn: isLoggedIn ?? this.isLoggedIn,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
updatedAt: updatedAt ?? this.updatedAt,
|
||||
lastAuthenticatedAt: lastAuthenticatedAt ?? this.lastAuthenticatedAt,
|
||||
rowid: rowid ?? this.rowid,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, Expression> toColumns(bool nullToAbsent) {
|
||||
final map = <String, Expression>{};
|
||||
if (id.present) {
|
||||
map['id'] = Variable<String>(id.value);
|
||||
}
|
||||
if (serverUserId.present) {
|
||||
map['server_user_id'] = Variable<String>(serverUserId.value);
|
||||
}
|
||||
if (email.present) {
|
||||
map['email'] = Variable<String>(email.value);
|
||||
}
|
||||
if (displayName.present) {
|
||||
map['display_name'] = Variable<String>(displayName.value);
|
||||
}
|
||||
if (isLoggedIn.present) {
|
||||
map['is_logged_in'] = Variable<bool>(isLoggedIn.value);
|
||||
}
|
||||
if (createdAt.present) {
|
||||
map['created_at'] = Variable<DateTime>(createdAt.value);
|
||||
}
|
||||
if (updatedAt.present) {
|
||||
map['updated_at'] = Variable<DateTime>(updatedAt.value);
|
||||
}
|
||||
if (lastAuthenticatedAt.present) {
|
||||
map['last_authenticated_at'] = Variable<DateTime>(
|
||||
lastAuthenticatedAt.value,
|
||||
);
|
||||
}
|
||||
if (rowid.present) {
|
||||
map['rowid'] = Variable<int>(rowid.value);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return (StringBuffer('OnlineAccountSessionsCompanion(')
|
||||
..write('id: $id, ')
|
||||
..write('serverUserId: $serverUserId, ')
|
||||
..write('email: $email, ')
|
||||
..write('displayName: $displayName, ')
|
||||
..write('isLoggedIn: $isLoggedIn, ')
|
||||
..write('createdAt: $createdAt, ')
|
||||
..write('updatedAt: $updatedAt, ')
|
||||
..write('lastAuthenticatedAt: $lastAuthenticatedAt, ')
|
||||
..write('rowid: $rowid')
|
||||
..write(')'))
|
||||
.toString();
|
||||
}
|
||||
}
|
||||
|
||||
class $ProgramsTable extends Programs with TableInfo<$ProgramsTable, Program> {
|
||||
@override
|
||||
final GeneratedDatabase attachedDatabase;
|
||||
@ -25542,6 +26088,8 @@ abstract class _$AppDatabase extends GeneratedDatabase {
|
||||
late final $ExercisesTable exercises = $ExercisesTable(this);
|
||||
late final $ExerciseImagesTable exerciseImages = $ExerciseImagesTable(this);
|
||||
late final $ExerciseStepsTable exerciseSteps = $ExerciseStepsTable(this);
|
||||
late final $OnlineAccountSessionsTable onlineAccountSessions =
|
||||
$OnlineAccountSessionsTable(this);
|
||||
late final $ProgramsTable programs = $ProgramsTable(this);
|
||||
late final $ProgramExercisesTable programExercises = $ProgramExercisesTable(
|
||||
this,
|
||||
@ -25576,6 +26124,7 @@ abstract class _$AppDatabase extends GeneratedDatabase {
|
||||
exercises,
|
||||
exerciseImages,
|
||||
exerciseSteps,
|
||||
onlineAccountSessions,
|
||||
programs,
|
||||
programExercises,
|
||||
workoutHistories,
|
||||
@ -35150,6 +35699,290 @@ typedef $$ExerciseStepsTableProcessedTableManager =
|
||||
ExerciseStep,
|
||||
PrefetchHooks Function({bool exerciseId})
|
||||
>;
|
||||
typedef $$OnlineAccountSessionsTableCreateCompanionBuilder =
|
||||
OnlineAccountSessionsCompanion Function({
|
||||
required String id,
|
||||
required String serverUserId,
|
||||
required String email,
|
||||
Value<String?> displayName,
|
||||
required bool isLoggedIn,
|
||||
required DateTime createdAt,
|
||||
required DateTime updatedAt,
|
||||
Value<DateTime?> lastAuthenticatedAt,
|
||||
Value<int> rowid,
|
||||
});
|
||||
typedef $$OnlineAccountSessionsTableUpdateCompanionBuilder =
|
||||
OnlineAccountSessionsCompanion Function({
|
||||
Value<String> id,
|
||||
Value<String> serverUserId,
|
||||
Value<String> email,
|
||||
Value<String?> displayName,
|
||||
Value<bool> isLoggedIn,
|
||||
Value<DateTime> createdAt,
|
||||
Value<DateTime> updatedAt,
|
||||
Value<DateTime?> lastAuthenticatedAt,
|
||||
Value<int> rowid,
|
||||
});
|
||||
|
||||
class $$OnlineAccountSessionsTableFilterComposer
|
||||
extends Composer<_$AppDatabase, $OnlineAccountSessionsTable> {
|
||||
$$OnlineAccountSessionsTableFilterComposer({
|
||||
required super.$db,
|
||||
required super.$table,
|
||||
super.joinBuilder,
|
||||
super.$addJoinBuilderToRootComposer,
|
||||
super.$removeJoinBuilderFromRootComposer,
|
||||
});
|
||||
ColumnFilters<String> get id => $composableBuilder(
|
||||
column: $table.id,
|
||||
builder: (column) => ColumnFilters(column),
|
||||
);
|
||||
|
||||
ColumnFilters<String> get serverUserId => $composableBuilder(
|
||||
column: $table.serverUserId,
|
||||
builder: (column) => ColumnFilters(column),
|
||||
);
|
||||
|
||||
ColumnFilters<String> get email => $composableBuilder(
|
||||
column: $table.email,
|
||||
builder: (column) => ColumnFilters(column),
|
||||
);
|
||||
|
||||
ColumnFilters<String> get displayName => $composableBuilder(
|
||||
column: $table.displayName,
|
||||
builder: (column) => ColumnFilters(column),
|
||||
);
|
||||
|
||||
ColumnFilters<bool> get isLoggedIn => $composableBuilder(
|
||||
column: $table.isLoggedIn,
|
||||
builder: (column) => ColumnFilters(column),
|
||||
);
|
||||
|
||||
ColumnFilters<DateTime> get createdAt => $composableBuilder(
|
||||
column: $table.createdAt,
|
||||
builder: (column) => ColumnFilters(column),
|
||||
);
|
||||
|
||||
ColumnFilters<DateTime> get updatedAt => $composableBuilder(
|
||||
column: $table.updatedAt,
|
||||
builder: (column) => ColumnFilters(column),
|
||||
);
|
||||
|
||||
ColumnFilters<DateTime> get lastAuthenticatedAt => $composableBuilder(
|
||||
column: $table.lastAuthenticatedAt,
|
||||
builder: (column) => ColumnFilters(column),
|
||||
);
|
||||
}
|
||||
|
||||
class $$OnlineAccountSessionsTableOrderingComposer
|
||||
extends Composer<_$AppDatabase, $OnlineAccountSessionsTable> {
|
||||
$$OnlineAccountSessionsTableOrderingComposer({
|
||||
required super.$db,
|
||||
required super.$table,
|
||||
super.joinBuilder,
|
||||
super.$addJoinBuilderToRootComposer,
|
||||
super.$removeJoinBuilderFromRootComposer,
|
||||
});
|
||||
ColumnOrderings<String> get id => $composableBuilder(
|
||||
column: $table.id,
|
||||
builder: (column) => ColumnOrderings(column),
|
||||
);
|
||||
|
||||
ColumnOrderings<String> get serverUserId => $composableBuilder(
|
||||
column: $table.serverUserId,
|
||||
builder: (column) => ColumnOrderings(column),
|
||||
);
|
||||
|
||||
ColumnOrderings<String> get email => $composableBuilder(
|
||||
column: $table.email,
|
||||
builder: (column) => ColumnOrderings(column),
|
||||
);
|
||||
|
||||
ColumnOrderings<String> get displayName => $composableBuilder(
|
||||
column: $table.displayName,
|
||||
builder: (column) => ColumnOrderings(column),
|
||||
);
|
||||
|
||||
ColumnOrderings<bool> get isLoggedIn => $composableBuilder(
|
||||
column: $table.isLoggedIn,
|
||||
builder: (column) => ColumnOrderings(column),
|
||||
);
|
||||
|
||||
ColumnOrderings<DateTime> get createdAt => $composableBuilder(
|
||||
column: $table.createdAt,
|
||||
builder: (column) => ColumnOrderings(column),
|
||||
);
|
||||
|
||||
ColumnOrderings<DateTime> get updatedAt => $composableBuilder(
|
||||
column: $table.updatedAt,
|
||||
builder: (column) => ColumnOrderings(column),
|
||||
);
|
||||
|
||||
ColumnOrderings<DateTime> get lastAuthenticatedAt => $composableBuilder(
|
||||
column: $table.lastAuthenticatedAt,
|
||||
builder: (column) => ColumnOrderings(column),
|
||||
);
|
||||
}
|
||||
|
||||
class $$OnlineAccountSessionsTableAnnotationComposer
|
||||
extends Composer<_$AppDatabase, $OnlineAccountSessionsTable> {
|
||||
$$OnlineAccountSessionsTableAnnotationComposer({
|
||||
required super.$db,
|
||||
required super.$table,
|
||||
super.joinBuilder,
|
||||
super.$addJoinBuilderToRootComposer,
|
||||
super.$removeJoinBuilderFromRootComposer,
|
||||
});
|
||||
GeneratedColumn<String> get id =>
|
||||
$composableBuilder(column: $table.id, builder: (column) => column);
|
||||
|
||||
GeneratedColumn<String> get serverUserId => $composableBuilder(
|
||||
column: $table.serverUserId,
|
||||
builder: (column) => column,
|
||||
);
|
||||
|
||||
GeneratedColumn<String> get email =>
|
||||
$composableBuilder(column: $table.email, builder: (column) => column);
|
||||
|
||||
GeneratedColumn<String> get displayName => $composableBuilder(
|
||||
column: $table.displayName,
|
||||
builder: (column) => column,
|
||||
);
|
||||
|
||||
GeneratedColumn<bool> get isLoggedIn => $composableBuilder(
|
||||
column: $table.isLoggedIn,
|
||||
builder: (column) => column,
|
||||
);
|
||||
|
||||
GeneratedColumn<DateTime> get createdAt =>
|
||||
$composableBuilder(column: $table.createdAt, builder: (column) => column);
|
||||
|
||||
GeneratedColumn<DateTime> get updatedAt =>
|
||||
$composableBuilder(column: $table.updatedAt, builder: (column) => column);
|
||||
|
||||
GeneratedColumn<DateTime> get lastAuthenticatedAt => $composableBuilder(
|
||||
column: $table.lastAuthenticatedAt,
|
||||
builder: (column) => column,
|
||||
);
|
||||
}
|
||||
|
||||
class $$OnlineAccountSessionsTableTableManager
|
||||
extends
|
||||
RootTableManager<
|
||||
_$AppDatabase,
|
||||
$OnlineAccountSessionsTable,
|
||||
OnlineAccountSession,
|
||||
$$OnlineAccountSessionsTableFilterComposer,
|
||||
$$OnlineAccountSessionsTableOrderingComposer,
|
||||
$$OnlineAccountSessionsTableAnnotationComposer,
|
||||
$$OnlineAccountSessionsTableCreateCompanionBuilder,
|
||||
$$OnlineAccountSessionsTableUpdateCompanionBuilder,
|
||||
(
|
||||
OnlineAccountSession,
|
||||
BaseReferences<
|
||||
_$AppDatabase,
|
||||
$OnlineAccountSessionsTable,
|
||||
OnlineAccountSession
|
||||
>,
|
||||
),
|
||||
OnlineAccountSession,
|
||||
PrefetchHooks Function()
|
||||
> {
|
||||
$$OnlineAccountSessionsTableTableManager(
|
||||
_$AppDatabase db,
|
||||
$OnlineAccountSessionsTable table,
|
||||
) : super(
|
||||
TableManagerState(
|
||||
db: db,
|
||||
table: table,
|
||||
createFilteringComposer: () =>
|
||||
$$OnlineAccountSessionsTableFilterComposer(
|
||||
$db: db,
|
||||
$table: table,
|
||||
),
|
||||
createOrderingComposer: () =>
|
||||
$$OnlineAccountSessionsTableOrderingComposer(
|
||||
$db: db,
|
||||
$table: table,
|
||||
),
|
||||
createComputedFieldComposer: () =>
|
||||
$$OnlineAccountSessionsTableAnnotationComposer(
|
||||
$db: db,
|
||||
$table: table,
|
||||
),
|
||||
updateCompanionCallback:
|
||||
({
|
||||
Value<String> id = const Value.absent(),
|
||||
Value<String> serverUserId = const Value.absent(),
|
||||
Value<String> email = const Value.absent(),
|
||||
Value<String?> displayName = const Value.absent(),
|
||||
Value<bool> isLoggedIn = const Value.absent(),
|
||||
Value<DateTime> createdAt = const Value.absent(),
|
||||
Value<DateTime> updatedAt = const Value.absent(),
|
||||
Value<DateTime?> lastAuthenticatedAt = const Value.absent(),
|
||||
Value<int> rowid = const Value.absent(),
|
||||
}) => OnlineAccountSessionsCompanion(
|
||||
id: id,
|
||||
serverUserId: serverUserId,
|
||||
email: email,
|
||||
displayName: displayName,
|
||||
isLoggedIn: isLoggedIn,
|
||||
createdAt: createdAt,
|
||||
updatedAt: updatedAt,
|
||||
lastAuthenticatedAt: lastAuthenticatedAt,
|
||||
rowid: rowid,
|
||||
),
|
||||
createCompanionCallback:
|
||||
({
|
||||
required String id,
|
||||
required String serverUserId,
|
||||
required String email,
|
||||
Value<String?> displayName = const Value.absent(),
|
||||
required bool isLoggedIn,
|
||||
required DateTime createdAt,
|
||||
required DateTime updatedAt,
|
||||
Value<DateTime?> lastAuthenticatedAt = const Value.absent(),
|
||||
Value<int> rowid = const Value.absent(),
|
||||
}) => OnlineAccountSessionsCompanion.insert(
|
||||
id: id,
|
||||
serverUserId: serverUserId,
|
||||
email: email,
|
||||
displayName: displayName,
|
||||
isLoggedIn: isLoggedIn,
|
||||
createdAt: createdAt,
|
||||
updatedAt: updatedAt,
|
||||
lastAuthenticatedAt: lastAuthenticatedAt,
|
||||
rowid: rowid,
|
||||
),
|
||||
withReferenceMapper: (p0) => p0
|
||||
.map((e) => (e.readTable(table), BaseReferences(db, table, e)))
|
||||
.toList(),
|
||||
prefetchHooksCallback: null,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
typedef $$OnlineAccountSessionsTableProcessedTableManager =
|
||||
ProcessedTableManager<
|
||||
_$AppDatabase,
|
||||
$OnlineAccountSessionsTable,
|
||||
OnlineAccountSession,
|
||||
$$OnlineAccountSessionsTableFilterComposer,
|
||||
$$OnlineAccountSessionsTableOrderingComposer,
|
||||
$$OnlineAccountSessionsTableAnnotationComposer,
|
||||
$$OnlineAccountSessionsTableCreateCompanionBuilder,
|
||||
$$OnlineAccountSessionsTableUpdateCompanionBuilder,
|
||||
(
|
||||
OnlineAccountSession,
|
||||
BaseReferences<
|
||||
_$AppDatabase,
|
||||
$OnlineAccountSessionsTable,
|
||||
OnlineAccountSession
|
||||
>,
|
||||
),
|
||||
OnlineAccountSession,
|
||||
PrefetchHooks Function()
|
||||
>;
|
||||
typedef $$ProgramsTableCreateCompanionBuilder =
|
||||
ProgramsCompanion Function({
|
||||
required String id,
|
||||
@ -41434,6 +42267,8 @@ class $AppDatabaseManager {
|
||||
$$ExerciseImagesTableTableManager(_db, _db.exerciseImages);
|
||||
$$ExerciseStepsTableTableManager get exerciseSteps =>
|
||||
$$ExerciseStepsTableTableManager(_db, _db.exerciseSteps);
|
||||
$$OnlineAccountSessionsTableTableManager get onlineAccountSessions =>
|
||||
$$OnlineAccountSessionsTableTableManager(_db, _db.onlineAccountSessions);
|
||||
$$ProgramsTableTableManager get programs =>
|
||||
$$ProgramsTableTableManager(_db, _db.programs);
|
||||
$$ProgramExercisesTableTableManager get programExercises =>
|
||||
|
||||
@ -140,6 +140,34 @@ final class DriftMediaAssetRepository implements MediaAssetRepository {
|
||||
}
|
||||
}
|
||||
|
||||
final class DriftOnlineAccountRepository implements OnlineAccountRepository {
|
||||
const DriftOnlineAccountRepository(this.database);
|
||||
|
||||
final db.AppDatabase database;
|
||||
|
||||
@override
|
||||
Future<domain.UserAccountSession?> currentSession() async {
|
||||
final row =
|
||||
await (database.select(database.onlineAccountSessions)
|
||||
..orderBy([(table) => OrderingTerm.desc(table.updatedAt)])
|
||||
..limit(1))
|
||||
.getSingleOrNull();
|
||||
return row == null ? null : _userAccountSessionFromRow(row);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> saveSession(domain.UserAccountSession session) async {
|
||||
await database
|
||||
.into(database.onlineAccountSessions)
|
||||
.insertOnConflictUpdate(_userAccountSessionCompanion(session));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> clearSession() async {
|
||||
await database.delete(database.onlineAccountSessions).go();
|
||||
}
|
||||
}
|
||||
|
||||
final class DriftProgramRepository implements ProgramRepository {
|
||||
const DriftProgramRepository(this.database);
|
||||
|
||||
@ -1471,6 +1499,38 @@ domain.MediaAsset _mediaAssetFromRow(db.MediaAsset row) {
|
||||
);
|
||||
}
|
||||
|
||||
db.OnlineAccountSessionsCompanion _userAccountSessionCompanion(
|
||||
domain.UserAccountSession session,
|
||||
) {
|
||||
return db.OnlineAccountSessionsCompanion(
|
||||
id: Value(session.id),
|
||||
serverUserId: Value(session.serverUserId),
|
||||
email: Value(session.email),
|
||||
displayName: Value<String?>(session.displayName),
|
||||
isLoggedIn: Value(session.isLoggedIn),
|
||||
createdAt: Value(session.createdAt.toUtc()),
|
||||
updatedAt: Value(session.updatedAt.toUtc()),
|
||||
lastAuthenticatedAt: Value<DateTime?>(
|
||||
_utcOrNull(session.lastAuthenticatedAt),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
domain.UserAccountSession _userAccountSessionFromRow(
|
||||
db.OnlineAccountSession row,
|
||||
) {
|
||||
return domain.UserAccountSession(
|
||||
id: row.id,
|
||||
serverUserId: row.serverUserId,
|
||||
email: row.email,
|
||||
displayName: row.displayName,
|
||||
isLoggedIn: row.isLoggedIn,
|
||||
createdAt: _utc(row.createdAt),
|
||||
updatedAt: _utc(row.updatedAt),
|
||||
lastAuthenticatedAt: _utcOrNull(row.lastAuthenticatedAt),
|
||||
);
|
||||
}
|
||||
|
||||
db.ProgramsCompanion _programCompanion(domain.Program program) {
|
||||
final values = _metadataValues(program.metadata);
|
||||
return db.ProgramsCompanion(
|
||||
@ -1931,8 +1991,7 @@ _activeExerciseStepProgressStateCompanion(
|
||||
);
|
||||
}
|
||||
|
||||
domain.ActiveExerciseStepProgressState
|
||||
_activeExerciseStepProgressStateFromRow(
|
||||
domain.ActiveExerciseStepProgressState _activeExerciseStepProgressStateFromRow(
|
||||
db.ActiveExerciseStepProgressState row,
|
||||
) {
|
||||
return domain.ActiveExerciseStepProgressState(
|
||||
|
||||
@ -24,6 +24,30 @@ abstract class SyncableTable extends Table {
|
||||
List<String> get customConstraints => const [];
|
||||
}
|
||||
|
||||
class OnlineAccountSessions extends Table {
|
||||
@override
|
||||
String get tableName => 'online_account_sessions';
|
||||
|
||||
TextColumn get id => text()();
|
||||
TextColumn get serverUserId => text().withLength(min: 1)();
|
||||
TextColumn get email => text().withLength(min: 1)();
|
||||
TextColumn get displayName => text().nullable()();
|
||||
BoolColumn get isLoggedIn => boolean()();
|
||||
DateTimeColumn get createdAt => dateTime()();
|
||||
DateTimeColumn get updatedAt => dateTime()();
|
||||
DateTimeColumn get lastAuthenticatedAt => dateTime().nullable()();
|
||||
|
||||
@override
|
||||
Set<Column> get primaryKey => {id};
|
||||
|
||||
@override
|
||||
List<String> get customConstraints => [
|
||||
'UNIQUE (server_user_id)',
|
||||
'CHECK (length(trim(email)) > 0)',
|
||||
'CHECK (display_name IS NULL OR length(trim(display_name)) > 0)',
|
||||
];
|
||||
}
|
||||
|
||||
class MediaAssets extends SyncableTable {
|
||||
@override
|
||||
String get tableName => 'media_assets';
|
||||
|
||||
71
lib/infrastructure/remote/auth_api.dart
Normal file
71
lib/infrastructure/remote/auth_api.dart
Normal file
@ -0,0 +1,71 @@
|
||||
import '../../application/application.dart';
|
||||
import 'http_api_client.dart';
|
||||
|
||||
final class HttpRemoteAuthApi implements RemoteAuthApi {
|
||||
const HttpRemoteAuthApi(this.client);
|
||||
|
||||
final HttpApiClient client;
|
||||
|
||||
@override
|
||||
Future<RemoteAuthResult> register({
|
||||
required String email,
|
||||
required String password,
|
||||
String? displayName,
|
||||
}) async {
|
||||
final registered = await client.postJson(
|
||||
'/auth/register',
|
||||
body: {
|
||||
'email': email,
|
||||
'password': password,
|
||||
if (displayName != null) 'displayName': displayName,
|
||||
},
|
||||
expectedStatuses: const {201},
|
||||
);
|
||||
final userId = _requiredString(registered, 'userId');
|
||||
final registeredEmail = _requiredString(registered, 'email');
|
||||
final loggedIn = await login(email: email, password: password);
|
||||
return RemoteAuthResult(
|
||||
userId: userId,
|
||||
email: registeredEmail,
|
||||
token: loggedIn.token,
|
||||
expiresAt: loggedIn.expiresAt,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<RemoteAuthResult> login({
|
||||
required String email,
|
||||
required String password,
|
||||
}) async {
|
||||
final response = await client.postJson(
|
||||
'/auth/login',
|
||||
body: {'email': email, 'password': password},
|
||||
);
|
||||
return RemoteAuthResult(
|
||||
userId: response['userId'] is String
|
||||
? response['userId'] as String
|
||||
: email.trim().toLowerCase(),
|
||||
email: response['email'] is String
|
||||
? response['email'] as String
|
||||
: email.trim().toLowerCase(),
|
||||
token: _requiredString(response, 'token'),
|
||||
expiresAt: DateTime.parse(_requiredString(response, 'expiresAt')).toUtc(),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> logout(String token) {
|
||||
return client.postEmpty('/auth/logout', bearerToken: token);
|
||||
}
|
||||
|
||||
String _requiredString(Map<String, Object?> json, String key) {
|
||||
final value = json[key];
|
||||
if (value is String && value.trim().isNotEmpty) {
|
||||
return value;
|
||||
}
|
||||
throw RemoteAuthException(
|
||||
RemoteAuthFailure.unknown,
|
||||
'$key is missing from auth response.',
|
||||
);
|
||||
}
|
||||
}
|
||||
124
lib/infrastructure/remote/http_api_client.dart
Normal file
124
lib/infrastructure/remote/http_api_client.dart
Normal file
@ -0,0 +1,124 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
import '../../application/application.dart';
|
||||
|
||||
final class HttpApiClient {
|
||||
HttpApiClient({
|
||||
required this.baseUrl,
|
||||
http.Client? client,
|
||||
this.timeout = const Duration(seconds: 10),
|
||||
}) : client = client ?? http.Client();
|
||||
|
||||
static const defaultBaseUrl = String.fromEnvironment(
|
||||
'GAMETIME_API_BASE_URL',
|
||||
defaultValue: 'http://localhost:8080',
|
||||
);
|
||||
|
||||
final Uri baseUrl;
|
||||
final http.Client client;
|
||||
final Duration timeout;
|
||||
|
||||
Future<Map<String, Object?>> postJson(
|
||||
String path, {
|
||||
Map<String, Object?>? body,
|
||||
String? bearerToken,
|
||||
Set<int> expectedStatuses = const {200, 201},
|
||||
}) async {
|
||||
final response = await _send(
|
||||
() => client.post(
|
||||
_resolve(path),
|
||||
headers: _headers(bearerToken),
|
||||
body: body == null ? null : jsonEncode(body),
|
||||
),
|
||||
);
|
||||
if (!expectedStatuses.contains(response.statusCode)) {
|
||||
throw _exceptionForStatus(response.statusCode, response.body);
|
||||
}
|
||||
if (response.body.trim().isEmpty) {
|
||||
return const {};
|
||||
}
|
||||
final Object? decoded;
|
||||
try {
|
||||
decoded = jsonDecode(response.body);
|
||||
} on FormatException catch (error) {
|
||||
throw RemoteAuthException(RemoteAuthFailure.unknown, error.message);
|
||||
}
|
||||
if (decoded is Map) {
|
||||
return Map<String, Object?>.from(decoded);
|
||||
}
|
||||
throw const RemoteAuthException(
|
||||
RemoteAuthFailure.unknown,
|
||||
'Unexpected JSON response.',
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> postEmpty(
|
||||
String path, {
|
||||
String? bearerToken,
|
||||
Set<int> expectedStatuses = const {204},
|
||||
}) async {
|
||||
final response = await _send(
|
||||
() => client.post(_resolve(path), headers: _headers(bearerToken)),
|
||||
);
|
||||
if (!expectedStatuses.contains(response.statusCode)) {
|
||||
throw _exceptionForStatus(response.statusCode, response.body);
|
||||
}
|
||||
}
|
||||
|
||||
Uri _resolve(String path) {
|
||||
final normalized = path.startsWith('/') ? path.substring(1) : path;
|
||||
final base = baseUrl.toString().endsWith('/')
|
||||
? baseUrl
|
||||
: Uri.parse('${baseUrl.toString()}/');
|
||||
return base.resolve(normalized);
|
||||
}
|
||||
|
||||
Map<String, String> _headers(String? bearerToken) => {
|
||||
'accept': 'application/json',
|
||||
'content-type': 'application/json',
|
||||
if (bearerToken != null) 'authorization': 'Bearer $bearerToken',
|
||||
};
|
||||
|
||||
Future<http.Response> _send(Future<http.Response> Function() send) async {
|
||||
try {
|
||||
return await send().timeout(timeout);
|
||||
} on TimeoutException {
|
||||
throw const RemoteAuthException(RemoteAuthFailure.network, 'Timeout.');
|
||||
} on http.ClientException catch (error) {
|
||||
throw RemoteAuthException(RemoteAuthFailure.network, error.message);
|
||||
} on FormatException catch (error) {
|
||||
throw RemoteAuthException(RemoteAuthFailure.unknown, error.message);
|
||||
}
|
||||
}
|
||||
|
||||
RemoteAuthException _exceptionForStatus(int statusCode, String body) {
|
||||
final message = _messageFromBody(body);
|
||||
return switch (statusCode) {
|
||||
401 => RemoteAuthException(RemoteAuthFailure.invalidCredentials, message),
|
||||
409 => RemoteAuthException(RemoteAuthFailure.emailAlreadyUsed, message),
|
||||
>= 500 => RemoteAuthException(RemoteAuthFailure.network, message),
|
||||
_ => RemoteAuthException(RemoteAuthFailure.unknown, message),
|
||||
};
|
||||
}
|
||||
|
||||
String? _messageFromBody(String body) {
|
||||
if (body.trim().isEmpty) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
final decoded = jsonDecode(body);
|
||||
if (decoded is Map && decoded['error'] is String) {
|
||||
return decoded['error'] as String;
|
||||
}
|
||||
if (decoded is Map && decoded['message'] is String) {
|
||||
return decoded['message'] as String;
|
||||
}
|
||||
} on FormatException {
|
||||
return body;
|
||||
}
|
||||
return body;
|
||||
}
|
||||
}
|
||||
2
lib/infrastructure/remote/remote.dart
Normal file
2
lib/infrastructure/remote/remote.dart
Normal file
@ -0,0 +1,2 @@
|
||||
export 'auth_api.dart';
|
||||
export 'http_api_client.dart';
|
||||
@ -0,0 +1,44 @@
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
|
||||
import '../../application/application.dart';
|
||||
|
||||
final class SecureStorageAuthTokenStore implements AuthTokenStore {
|
||||
const SecureStorageAuthTokenStore({
|
||||
FlutterSecureStorage storage = const FlutterSecureStorage(),
|
||||
}) : _storage = storage;
|
||||
|
||||
static const _tokenKey = 'gametime.auth.token';
|
||||
static const _expiresAtKey = 'gametime.auth.expiresAt';
|
||||
|
||||
final FlutterSecureStorage _storage;
|
||||
|
||||
@override
|
||||
Future<void> saveToken(String token, DateTime expiresAt) async {
|
||||
await _storage.write(key: _tokenKey, value: token);
|
||||
await _storage.write(
|
||||
key: _expiresAtKey,
|
||||
value: expiresAt.toUtc().toIso8601String(),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String?> readToken() async {
|
||||
final token = await _storage.read(key: _tokenKey);
|
||||
final expiresAtValue = await _storage.read(key: _expiresAtKey);
|
||||
if (token == null || expiresAtValue == null) {
|
||||
return token;
|
||||
}
|
||||
final expiresAt = DateTime.tryParse(expiresAtValue)?.toUtc();
|
||||
if (expiresAt != null && !expiresAt.isAfter(DateTime.now().toUtc())) {
|
||||
await clearToken();
|
||||
return null;
|
||||
}
|
||||
return token;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> clearToken() async {
|
||||
await _storage.delete(key: _tokenKey);
|
||||
await _storage.delete(key: _expiresAtKey);
|
||||
}
|
||||
}
|
||||
1
lib/infrastructure/security/security.dart
Normal file
1
lib/infrastructure/security/security.dart
Normal file
@ -0,0 +1 @@
|
||||
export 'secure_storage_auth_token_store.dart';
|
||||
@ -35,7 +35,8 @@ final class AudioplayersExerciseStepAudioCuePlayer
|
||||
}
|
||||
}
|
||||
|
||||
final class NoOpExerciseStepAudioCuePlayer implements ExerciseStepAudioCuePlayer {
|
||||
final class NoOpExerciseStepAudioCuePlayer
|
||||
implements ExerciseStepAudioCuePlayer {
|
||||
const NoOpExerciseStepAudioCuePlayer();
|
||||
|
||||
@override
|
||||
|
||||
Reference in New Issue
Block a user