fix: handle login

This commit is contained in:
shenlong-tanwen 2024-08-25 10:38:24 +05:30
parent 7f83740b35
commit 877c3b028b
27 changed files with 430 additions and 355 deletions

View File

@ -1,11 +1,11 @@
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:immich_mobile/domain/models/asset.model.dart'; import 'package:immich_mobile/domain/models/asset.model.dart';
class LocalAsset extends Table { class Asset extends Table {
const LocalAsset(); const Asset();
IntColumn get id => integer().autoIncrement()(); IntColumn get id => integer().autoIncrement()();
TextColumn get localId => text().unique()();
TextColumn get name => text()(); TextColumn get name => text()();
TextColumn get checksum => text().unique()(); TextColumn get checksum => text().unique()();
IntColumn get height => integer()(); IntColumn get height => integer()();

View File

@ -0,0 +1,8 @@
import 'package:drift/drift.dart';
import 'package:immich_mobile/domain/entities/asset.entity.dart';
class LocalAsset extends Asset {
const LocalAsset();
TextColumn get localId => text().unique()();
}

View File

@ -0,0 +1,8 @@
import 'package:drift/drift.dart';
import 'package:immich_mobile/domain/entities/asset.entity.dart';
class RemoteAsset extends Asset {
const RemoteAsset();
TextColumn get remoteId => text().unique()();
}

View File

@ -1,5 +1,3 @@
import 'package:flutter/foundation.dart';
enum AssetType { enum AssetType {
// do not change this order! // do not change this order!
other, other,
@ -8,10 +6,8 @@ enum AssetType {
audio, audio,
} }
@immutable class Asset {
class LocalAsset {
final int id; final int id;
final String localId;
final String name; final String name;
final String checksum; final String checksum;
final int height; final int height;
@ -22,9 +18,8 @@ class LocalAsset {
final int duration; final int duration;
final bool isLivePhoto; final bool isLivePhoto;
const LocalAsset({ const Asset({
required this.id, required this.id,
required this.localId,
required this.name, required this.name,
required this.checksum, required this.checksum,
required this.height, required this.height,
@ -36,16 +31,36 @@ class LocalAsset {
required this.isLivePhoto, required this.isLivePhoto,
}); });
@override Asset copyWith({
String toString() { int? id,
return 'LocalAsset(id: $id, localId: $localId, name: $name, checksum: $checksum, height: $height, width: $width, type: $type, createdTime: $createdTime, modifiedTime: $modifiedTime, duration: $duration, isLivePhoto: $isLivePhoto)'; String? name,
String? checksum,
int? height,
int? width,
AssetType? type,
DateTime? createdTime,
DateTime? modifiedTime,
int? duration,
bool? isLivePhoto,
}) {
return Asset(
id: id ?? this.id,
name: name ?? this.name,
checksum: checksum ?? this.checksum,
height: height ?? this.height,
width: width ?? this.width,
type: type ?? this.type,
createdTime: createdTime ?? this.createdTime,
modifiedTime: modifiedTime ?? this.modifiedTime,
duration: duration ?? this.duration,
isLivePhoto: isLivePhoto ?? this.isLivePhoto,
);
} }
String toJSON() { @override
return """ String toString() => """
{ {
"id": $id, "id": $id,
"localId": "$localId",
"name": "$name", "name": "$name",
"checksum": "$checksum", "checksum": "$checksum",
"height": $height, "height": $height,
@ -56,19 +71,26 @@ class LocalAsset {
"duration": "$duration", "duration": "$duration",
"isLivePhoto": "$isLivePhoto", "isLivePhoto": "$isLivePhoto",
}"""; }""";
}
@override @override
bool operator ==(covariant LocalAsset other) { bool operator ==(covariant Asset other) {
if (identical(this, other)) return true; if (identical(this, other)) return true;
return other.hashCode == hashCode; return other.id == id &&
other.name == name &&
other.checksum == checksum &&
other.height == height &&
other.width == width &&
other.type == type &&
other.createdTime == createdTime &&
other.modifiedTime == modifiedTime &&
other.duration == duration &&
other.isLivePhoto == isLivePhoto;
} }
@override @override
int get hashCode { int get hashCode {
return id.hashCode ^ return id.hashCode ^
localId.hashCode ^
name.hashCode ^ name.hashCode ^
checksum.hashCode ^ checksum.hashCode ^
height.hashCode ^ height.hashCode ^

View File

@ -0,0 +1,76 @@
import 'package:flutter/foundation.dart';
import 'package:immich_mobile/domain/models/asset.model.dart';
@immutable
class LocalAsset extends Asset {
final String localId;
const LocalAsset({
required this.localId,
required super.id,
required super.name,
required super.checksum,
required super.height,
required super.width,
required super.type,
required super.createdTime,
required super.modifiedTime,
required super.duration,
required super.isLivePhoto,
});
@override
String toString() => """
{
"id": $id,
"localId": "$localId",
"name": "$name",
"checksum": "$checksum",
"height": $height,
"width": $width,
"type": "$type",
"createdTime": "$createdTime",
"modifiedTime": "$modifiedTime",
"duration": "$duration",
"isLivePhoto": "$isLivePhoto",
}""";
@override
bool operator ==(covariant LocalAsset other) {
if (identical(this, other)) return true;
return super == (other) && other.localId == localId;
}
@override
int get hashCode => super.hashCode ^ localId.hashCode;
@override
LocalAsset copyWith({
int? id,
String? localId,
String? name,
String? checksum,
int? height,
int? width,
AssetType? type,
DateTime? createdTime,
DateTime? modifiedTime,
int? duration,
bool? isLivePhoto,
}) {
return LocalAsset(
id: id ?? this.id,
localId: localId ?? this.localId,
name: name ?? this.name,
checksum: checksum ?? this.checksum,
height: height ?? this.height,
width: width ?? this.width,
type: type ?? this.type,
createdTime: createdTime ?? this.createdTime,
modifiedTime: modifiedTime ?? this.modifiedTime,
duration: duration ?? this.duration,
isLivePhoto: isLivePhoto ?? this.isLivePhoto,
);
}
}

View File

@ -0,0 +1,76 @@
import 'package:flutter/foundation.dart';
import 'package:immich_mobile/domain/models/asset.model.dart';
@immutable
class RemoteAsset extends Asset {
final String remoteId;
const RemoteAsset({
required this.remoteId,
required super.id,
required super.name,
required super.checksum,
required super.height,
required super.width,
required super.type,
required super.createdTime,
required super.modifiedTime,
required super.duration,
required super.isLivePhoto,
});
@override
String toString() => """
{
"id": $id,
"remoteId": "$remoteId",
"name": "$name",
"checksum": "$checksum",
"height": $height,
"width": $width,
"type": "$type",
"createdTime": "$createdTime",
"modifiedTime": "$modifiedTime",
"duration": "$duration",
"isLivePhoto": "$isLivePhoto",
}""";
@override
bool operator ==(covariant RemoteAsset other) {
if (identical(this, other)) return true;
return super == (other) && other.remoteId == remoteId;
}
@override
int get hashCode => super.hashCode ^ remoteId.hashCode;
@override
RemoteAsset copyWith({
int? id,
String? remoteId,
String? name,
String? checksum,
int? height,
int? width,
AssetType? type,
DateTime? createdTime,
DateTime? modifiedTime,
int? duration,
bool? isLivePhoto,
}) {
return RemoteAsset(
id: id ?? this.id,
remoteId: remoteId ?? this.remoteId,
name: name ?? this.name,
checksum: checksum ?? this.checksum,
height: height ?? this.height,
width: width ?? this.width,
type: type ?? this.type,
createdTime: createdTime ?? this.createdTime,
modifiedTime: modifiedTime ?? this.modifiedTime,
duration: duration ?? this.duration,
isLivePhoto: isLivePhoto ?? this.isLivePhoto,
);
}
}

View File

@ -1,4 +1,4 @@
import 'package:openapi/openapi.dart'; import 'package:openapi/api.dart';
class ServerConfig { class ServerConfig {
final String? oauthButtonText; final String? oauthButtonText;

View File

@ -1,4 +1,4 @@
import 'package:openapi/openapi.dart'; import 'package:openapi/api.dart';
class ServerFeatures { class ServerFeatures {
final bool hasPasswordLogin; final bool hasPasswordLogin;

View File

@ -1,6 +1,6 @@
import 'dart:ui'; import 'dart:ui';
import 'package:openapi/openapi.dart' as api; import 'package:openapi/api.dart' as api;
class User { class User {
const User({ const User({

View File

@ -6,8 +6,9 @@ import 'package:drift/native.dart';
import 'package:drift_dev/api/migrations.dart'; import 'package:drift_dev/api/migrations.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:immich_mobile/domain/entities/album.entity.dart'; import 'package:immich_mobile/domain/entities/album.entity.dart';
import 'package:immich_mobile/domain/entities/asset.entity.dart'; import 'package:immich_mobile/domain/entities/local_asset.entity.dart';
import 'package:immich_mobile/domain/entities/log.entity.dart'; import 'package:immich_mobile/domain/entities/log.entity.dart';
import 'package:immich_mobile/domain/entities/remote_asset.entity.dart';
import 'package:immich_mobile/domain/entities/store.entity.dart'; import 'package:immich_mobile/domain/entities/store.entity.dart';
import 'package:immich_mobile/domain/entities/user.entity.dart'; import 'package:immich_mobile/domain/entities/user.entity.dart';
import 'package:immich_mobile/domain/interfaces/database.interface.dart'; import 'package:immich_mobile/domain/interfaces/database.interface.dart';
@ -18,7 +19,7 @@ import 'package:sqlite3_flutter_libs/sqlite3_flutter_libs.dart';
import 'database.repository.drift.dart'; import 'database.repository.drift.dart';
@DriftDatabase(tables: [Logs, Store, LocalAlbum, LocalAsset, User]) @DriftDatabase(tables: [Logs, Store, LocalAlbum, LocalAsset, User, RemoteAsset])
class DriftDatabaseRepository extends $DriftDatabaseRepository class DriftDatabaseRepository extends $DriftDatabaseRepository
implements IDatabaseRepository<GeneratedDatabase> { implements IDatabaseRepository<GeneratedDatabase> {
DriftDatabaseRepository() : super(_openConnection()); DriftDatabaseRepository() : super(_openConnection());

View File

@ -18,32 +18,28 @@ class LogDriftRepository implements ILogRepository {
} }
@override @override
Future<void> truncateLogs({int limit = 250}) { Future<void> truncateLogs({int limit = 250}) async {
return db.transaction(() async { final totalCount = await db.managers.logs.count();
final totalCount = await db.managers.logs.count(); if (totalCount > limit) {
if (totalCount > limit) { final rowsToDelete = totalCount - limit;
final rowsToDelete = totalCount - limit; await db.managers.logs
await db.managers.logs .orderBy((o) => o.createdAt.desc())
.orderBy((o) => o.createdAt.desc()) .limit(rowsToDelete)
.limit(rowsToDelete) .delete();
.delete(); }
}
});
} }
@override @override
FutureOr<bool> add(LogMessage log) async { FutureOr<bool> add(LogMessage log) async {
try { try {
await db.transaction(() async { await db.into(db.logs).insert(LogsCompanion.insert(
await db.into(db.logs).insert(LogsCompanion.insert( content: log.content,
content: log.content, level: log.level,
level: log.level, createdAt: Value(log.createdAt),
createdAt: Value(log.createdAt), error: Value(log.error),
error: Value(log.error), logger: Value(log.logger),
logger: Value(log.logger), stack: Value(log.stack),
stack: Value(log.stack), ));
));
});
return true; return true;
} catch (e) { } catch (e) {
debugPrint("Error while adding a log to the DB - $e"); debugPrint("Error while adding a log to the DB - $e");

View File

@ -32,16 +32,14 @@ class StoreDriftRepository with LogContext implements IStoreRepository {
@override @override
FutureOr<bool> set<T, U>(StoreKey<T, U> key, T value) async { FutureOr<bool> set<T, U>(StoreKey<T, U> key, T value) async {
try { try {
await db.transaction(() async { final storeValue = key.converter.toPrimitive(value);
final storeValue = key.converter.toPrimitive(value); final intValue = (key.type == int) ? storeValue as int : null;
final intValue = (key.type == int) ? storeValue as int : null; final stringValue = (key.type == String) ? storeValue as String : null;
final stringValue = (key.type == String) ? storeValue as String : null; await db.into(db.store).insertOnConflictUpdate(StoreCompanion.insert(
await db.into(db.store).insertOnConflictUpdate(StoreCompanion.insert( id: Value(key.id),
id: Value(key.id), intValue: Value(intValue),
intValue: Value(intValue), stringValue: Value(stringValue),
stringValue: Value(stringValue), ));
));
});
return true; return true;
} catch (e, s) { } catch (e, s) {
log.severe("Cannot set store value - ${key.name}; id - ${key.id}", e, s); log.severe("Cannot set store value - ${key.name}; id - ${key.id}", e, s);
@ -51,9 +49,7 @@ class StoreDriftRepository with LogContext implements IStoreRepository {
@override @override
FutureOr<void> delete(StoreKey key) async { FutureOr<void> delete(StoreKey key) async {
return await db.transaction(() async { await db.managers.store.filter((s) => s.id.equals(key.id)).delete();
await db.managers.store.filter((s) => s.id.equals(key.id)).delete();
});
} }
@override @override
@ -66,9 +62,8 @@ class StoreDriftRepository with LogContext implements IStoreRepository {
@override @override
FutureOr<void> clearStore() async { FutureOr<void> clearStore() async {
return await db.transaction(() async { await db.managers.store.delete();
await db.managers.store.delete(); ;
});
} }
FutureOr<T?> _getValueFromStoreData<T, U>( FutureOr<T?> _getValueFromStoreData<T, U>(

View File

@ -23,8 +23,8 @@ class UserDriftRepository with LogContext implements IUserRepository {
@override @override
FutureOr<bool> insertUser(User user) async { FutureOr<bool> insertUser(User user) async {
try { try {
return await db.transaction(() async { await db.into(db.user).insertOnConflictUpdate(
await db.into(db.user).insertOnConflictUpdate(UserCompanion.insert( UserCompanion.insert(
id: user.id, id: user.id,
name: user.name, name: user.name,
email: user.email, email: user.email,
@ -36,9 +36,9 @@ class UserDriftRepository with LogContext implements IUserRepository {
quotaSizeInBytes: Value(user.quotaSizeInBytes), quotaSizeInBytes: Value(user.quotaSizeInBytes),
quotaUsageInBytes: Value(user.quotaSizeInBytes), quotaUsageInBytes: Value(user.quotaSizeInBytes),
updatedAt: Value(user.updatedAt), updatedAt: Value(user.updatedAt),
)); ),
return true; );
}); return true;
} catch (e, s) { } catch (e, s) {
log.severe("Cannot insert User into table - $user", e, s); log.severe("Cannot insert User into table - $user", e, s);
return false; return false;

View File

@ -1,26 +1,28 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:io';
import 'package:dio/dio.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter_web_auth_2/flutter_web_auth_2.dart'; import 'package:flutter_web_auth_2/flutter_web_auth_2.dart';
import 'package:http/http.dart';
import 'package:immich_mobile/service_locator.dart'; import 'package:immich_mobile/service_locator.dart';
import 'package:immich_mobile/utils/immich_api_client.dart';
import 'package:immich_mobile/utils/mixins/log_context.mixin.dart'; import 'package:immich_mobile/utils/mixins/log_context.mixin.dart';
import 'package:openapi/openapi.dart'; import 'package:openapi/api.dart';
class LoginService with LogContext { class LoginService with LogContext {
const LoginService(); const LoginService();
Future<bool> isEndpointAvailable(Uri uri, {Dio? dio}) async { Future<bool> isEndpointAvailable(Uri uri, {ImmichApiClient? client}) async {
String baseUrl = uri.toString(); String baseUrl = uri.toString();
if (!baseUrl.endsWith('/api')) { if (!baseUrl.endsWith('/api')) {
baseUrl += '/api'; baseUrl += '/api';
} }
final serverAPI = final serverAPI = client?.getServerApi() ??
Openapi(dio: dio, basePathOverride: baseUrl, interceptors: []) ImmichApiClient(endpoint: baseUrl).getServerApi();
.getServerApi();
try { try {
await serverAPI.pingServer(validateStatus: (status) => status == 200); await serverAPI.pingServer();
} catch (e) { } catch (e) {
log.severe("Exception occured while validating endpoint", e); log.severe("Exception occured while validating endpoint", e);
return false; return false;
@ -28,25 +30,24 @@ class LoginService with LogContext {
return true; return true;
} }
Future<String> resolveEndpoint(Uri uri, {Dio? dio}) async { Future<String> resolveEndpoint(Uri uri, {Client? client}) async {
final d = dio ?? Dio();
String baseUrl = uri.toString(); String baseUrl = uri.toString();
final d = client ?? ImmichApiClient(endpoint: baseUrl).client;
try { try {
// Check for well-known endpoint // Check for well-known endpoint
final res = await d.get( final res = await d.get(
"$baseUrl/.well-known/immich", Uri.parse("$baseUrl/.well-known/immich"),
options: Options( headers: {"Accept": "application/json"},
headers: {"Accept": "application/json"},
validateStatus: (status) => status == 200,
),
); );
final data = jsonDecode(res.data); if (res.statusCode == HttpStatus.ok) {
final endpoint = data['api']['endpoint'].toString(); final data = await compute(jsonDecode, res.body);
final endpoint = data['api']['endpoint'].toString();
// Full URL is relative to base // Full URL is relative to base
return endpoint.startsWith('/') ? "$baseUrl$endpoint" : endpoint; return endpoint.startsWith('/') ? "$baseUrl$endpoint" : endpoint;
}
} catch (e) { } catch (e) {
log.fine("Could not locate /.well-known/immich at $baseUrl", e); log.fine("Could not locate /.well-known/immich at $baseUrl", e);
} }
@ -57,14 +58,12 @@ class LoginService with LogContext {
Future<String?> passwordLogin(String email, String password) async { Future<String?> passwordLogin(String email, String password) async {
try { try {
final loginResponse = await di<Openapi>().getAuthenticationApi().login( final loginResponse =
loginCredentialDto: LoginCredentialDto((builder) { await di<ImmichApiClient>().getAuthenticationApi().login(
builder.email = email; LoginCredentialDto(email: email, password: password),
builder.password = password; );
}),
);
return loginResponse.data?.accessToken; return loginResponse?.accessToken;
} catch (e, s) { } catch (e, s) {
log.severe("Exception occured while performing password login", e, s); log.severe("Exception occured while performing password login", e, s);
} }
@ -74,16 +73,14 @@ class LoginService with LogContext {
Future<String?> oAuthLogin() async { Future<String?> oAuthLogin() async {
const String oAuthCallbackSchema = 'app.immich'; const String oAuthCallbackSchema = 'app.immich';
final oAuthApi = di<Openapi>().getOAuthApi(); final oAuthApi = di<ImmichApiClient>().getOAuthApi();
try { try {
final oAuthUrl = await oAuthApi.startOAuth( final oAuthUrl = await oAuthApi.startOAuth(
oAuthConfigDto: OAuthConfigDto((builder) { OAuthConfigDto(redirectUri: "$oAuthCallbackSchema:/"),
builder.redirectUri = "$oAuthCallbackSchema:/";
}),
); );
final oAuthUrlRes = oAuthUrl.data?.url; final oAuthUrlRes = oAuthUrl?.url;
if (oAuthUrlRes == null) { if (oAuthUrlRes == null) {
log.severe( log.severe(
"oAuth Server URL not available. Kindly ensure oAuth login is enabled in the server", "oAuth Server URL not available. Kindly ensure oAuth login is enabled in the server",
@ -97,12 +94,10 @@ class LoginService with LogContext {
); );
final loginResponse = await oAuthApi.finishOAuth( final loginResponse = await oAuthApi.finishOAuth(
oAuthCallbackDto: OAuthCallbackDto((builder) { OAuthCallbackDto(url: oAuthCallbackUrl),
builder.url = oAuthCallbackUrl;
}),
); );
return loginResponse.data?.accessToken; return loginResponse?.accessToken;
} catch (e) { } catch (e) {
log.severe("Exception occured while performing oauth login", e); log.severe("Exception occured while performing oauth login", e);
} }

View File

@ -1,10 +1,11 @@
import 'package:immich_mobile/domain/models/server-info/server_config.model.dart'; import 'package:immich_mobile/domain/models/server-info/server_config.model.dart';
import 'package:immich_mobile/domain/models/server-info/server_features.model.dart'; import 'package:immich_mobile/domain/models/server-info/server_features.model.dart';
import 'package:immich_mobile/utils/immich_api_client.dart';
import 'package:immich_mobile/utils/mixins/log_context.mixin.dart'; import 'package:immich_mobile/utils/mixins/log_context.mixin.dart';
import 'package:openapi/openapi.dart'; import 'package:openapi/api.dart';
class ServerInfoService with LogContext { class ServerInfoService with LogContext {
final Openapi _api; final ImmichApiClient _api;
ServerApi get _serverInfo => _api.getServerApi(); ServerApi get _serverInfo => _api.getServerApi();
@ -12,8 +13,7 @@ class ServerInfoService with LogContext {
Future<ServerFeatures?> getServerFeatures() async { Future<ServerFeatures?> getServerFeatures() async {
try { try {
final response = await _serverInfo.getServerFeatures(); final dto = await _serverInfo.getServerFeatures();
final dto = response.data;
if (dto != null) { if (dto != null) {
return ServerFeatures.fromDto(dto); return ServerFeatures.fromDto(dto);
} }
@ -25,8 +25,7 @@ class ServerInfoService with LogContext {
Future<ServerConfig?> getServerConfig() async { Future<ServerConfig?> getServerConfig() async {
try { try {
final response = await _serverInfo.getServerConfig(); final dto = await _serverInfo.getServerConfig();
final dto = response.data;
if (dto != null) { if (dto != null) {
return ServerConfig.fromDto(dto); return ServerConfig.fromDto(dto);
} }

View File

@ -1,9 +1,10 @@
import 'package:immich_mobile/domain/models/user.model.dart'; import 'package:immich_mobile/domain/models/user.model.dart';
import 'package:immich_mobile/utils/immich_api_client.dart';
import 'package:immich_mobile/utils/mixins/log_context.mixin.dart'; import 'package:immich_mobile/utils/mixins/log_context.mixin.dart';
import 'package:openapi/openapi.dart'; import 'package:openapi/api.dart';
class UserService with LogContext { class UserService with LogContext {
final Openapi _api; final ImmichApiClient _api;
UsersApi get _userApi => _api.getUsersApi(); UsersApi get _userApi => _api.getUsersApi();
@ -11,15 +12,14 @@ class UserService with LogContext {
Future<User?> getMyUser() async { Future<User?> getMyUser() async {
try { try {
final response = await _userApi.getMyUser(); final userDto = await _userApi.getMyUser();
final dto = response.data; if (userDto == null) {
if (dto == null) {
log.severe("Cannot fetch my user."); log.severe("Cannot fetch my user.");
return null; return null;
} }
final preferences = await _userApi.getMyPreferences(); final preferencesDto = await _userApi.getMyPreferences();
return User.fromAdminDto(dto, preferences.data); return User.fromAdminDto(userDto, preferencesDto);
} catch (e, s) { } catch (e, s) {
log.severe("Error while fetching server features", e, s); log.severe("Error while fetching server features", e, s);
} }

View File

@ -3,4 +3,6 @@ import 'package:immich_mobile/domain/models/user.model.dart';
class CurrentUserCubit extends Cubit<User> { class CurrentUserCubit extends Cubit<User> {
CurrentUserCubit(super.initialState); CurrentUserCubit(super.initialState);
void updateUser(User user) => emit(user);
} }

View File

@ -1,6 +1,5 @@
import 'dart:async'; import 'dart:async';
import 'package:collection/collection.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:immich_mobile/domain/interfaces/store.interface.dart'; import 'package:immich_mobile/domain/interfaces/store.interface.dart';
import 'package:immich_mobile/domain/interfaces/user.interface.dart'; import 'package:immich_mobile/domain/interfaces/user.interface.dart';
@ -11,10 +10,9 @@ import 'package:immich_mobile/i18n/strings.g.dart';
import 'package:immich_mobile/presentation/modules/common/states/server_info/server_feature_config.state.dart'; import 'package:immich_mobile/presentation/modules/common/states/server_info/server_feature_config.state.dart';
import 'package:immich_mobile/presentation/modules/login/models/login_page.model.dart'; import 'package:immich_mobile/presentation/modules/login/models/login_page.model.dart';
import 'package:immich_mobile/service_locator.dart'; import 'package:immich_mobile/service_locator.dart';
import 'package:immich_mobile/utils/immich_auth_interceptor.dart'; import 'package:immich_mobile/utils/immich_api_client.dart';
import 'package:immich_mobile/utils/mixins/log_context.mixin.dart'; import 'package:immich_mobile/utils/mixins/log_context.mixin.dart';
import 'package:immich_mobile/utils/snackbar_manager.dart'; import 'package:immich_mobile/utils/snackbar_manager.dart';
import 'package:openapi/openapi.dart';
class LoginPageCubit extends Cubit<LoginPageState> with LogContext { class LoginPageCubit extends Cubit<LoginPageState> with LogContext {
LoginPageCubit() : super(LoginPageState.reset()); LoginPageCubit() : super(LoginPageState.reset());
@ -66,7 +64,7 @@ class LoginPageCubit extends Cubit<LoginPageState> with LogContext {
url = await loginService.resolveEndpoint(uri); url = await loginService.resolveEndpoint(uri);
di<IStoreRepository>().set(StoreKey.serverEndpoint, url); di<IStoreRepository>().set(StoreKey.serverEndpoint, url);
await ServiceLocator.registerPostValidationServices(url); ServiceLocator.registerPostValidationServices(url);
// Fetch server features // Fetch server features
await di<ServerFeatureConfigCubit>().getFeatures(); await di<ServerFeatureConfigCubit>().getFeatures();
@ -92,6 +90,9 @@ class LoginPageCubit extends Cubit<LoginPageState> with LogContext {
} }
await _postLogin(accessToken); await _postLogin(accessToken);
} catch (e, s) {
SnackbarManager.showError(t.login.error.error_login);
log.severe("Cannot perform password login", e, s);
} finally { } finally {
emit(state.copyWith(isValidationInProgress: false)); emit(state.copyWith(isValidationInProgress: false));
} }
@ -109,6 +110,9 @@ class LoginPageCubit extends Cubit<LoginPageState> with LogContext {
} }
await _postLogin(accessToken); await _postLogin(accessToken);
} catch (e, s) {
SnackbarManager.showError(t.login.error.error_login_oauth);
log.severe("Cannot perform oauth login", e, s);
} finally { } finally {
emit(state.copyWith(isValidationInProgress: false)); emit(state.copyWith(isValidationInProgress: false));
} }
@ -118,12 +122,7 @@ class LoginPageCubit extends Cubit<LoginPageState> with LogContext {
await di<IStoreRepository>().set(StoreKey.accessToken, accessToken); await di<IStoreRepository>().set(StoreKey.accessToken, accessToken);
/// Set token to interceptor /// Set token to interceptor
final interceptor = di<Openapi>() await di<ImmichApiClient>().init(accessToken: accessToken);
.dio
.interceptors
.firstWhereOrNull((i) => i is ImmichAuthInterceptor)
as ImmichAuthInterceptor?;
interceptor?.setAccessToken(accessToken);
final user = await di<UserService>().getMyUser(); final user = await di<UserService>().getMyUser();
if (user == null) { if (user == null) {
@ -137,7 +136,7 @@ class LoginPageCubit extends Cubit<LoginPageState> with LogContext {
emit(state.copyWith( emit(state.copyWith(
isValidationInProgress: false, isValidationInProgress: false,
isServerValidated: true, isLoginSuccessful: true,
)); ));
} }

View File

@ -1,8 +1,3 @@
import 'dart:async';
import 'dart:io';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:dio/dio.dart';
import 'package:get_it/get_it.dart'; import 'package:get_it/get_it.dart';
import 'package:immich_mobile/domain/interfaces/log.interface.dart'; import 'package:immich_mobile/domain/interfaces/log.interface.dart';
import 'package:immich_mobile/domain/interfaces/store.interface.dart'; import 'package:immich_mobile/domain/interfaces/store.interface.dart';
@ -15,12 +10,12 @@ import 'package:immich_mobile/domain/repositories/user.repository.dart';
import 'package:immich_mobile/domain/services/app_setting.service.dart'; import 'package:immich_mobile/domain/services/app_setting.service.dart';
import 'package:immich_mobile/domain/services/login.service.dart'; import 'package:immich_mobile/domain/services/login.service.dart';
import 'package:immich_mobile/domain/services/server_info.service.dart'; import 'package:immich_mobile/domain/services/server_info.service.dart';
import 'package:immich_mobile/domain/services/user.service.dart';
import 'package:immich_mobile/presentation/modules/common/states/current_user.state.dart'; import 'package:immich_mobile/presentation/modules/common/states/current_user.state.dart';
import 'package:immich_mobile/presentation/modules/common/states/server_info/server_feature_config.state.dart'; import 'package:immich_mobile/presentation/modules/common/states/server_info/server_feature_config.state.dart';
import 'package:immich_mobile/presentation/modules/theme/states/app_theme.state.dart'; import 'package:immich_mobile/presentation/modules/theme/states/app_theme.state.dart';
import 'package:immich_mobile/presentation/router/router.dart'; import 'package:immich_mobile/presentation/router/router.dart';
import 'package:immich_mobile/utils/immich_auth_interceptor.dart'; import 'package:immich_mobile/utils/immich_api_client.dart';
import 'package:openapi/openapi.dart';
final di = GetIt.I; final di = GetIt.I;
@ -55,47 +50,24 @@ class ServiceLocator {
di.registerLazySingleton<AppThemeCubit>(() => AppThemeCubit(di())); di.registerLazySingleton<AppThemeCubit>(() => AppThemeCubit(di()));
} }
static FutureOr<void> registerPostValidationServices(String endpoint) async { static void registerPostValidationServices(String endpoint) {
if (di.isRegistered<Openapi>()) { di.registerSingleton<ImmichApiClient>(ImmichApiClient(endpoint: endpoint));
return;
}
final deviceInfo = DeviceInfoPlugin();
final String deviceModel;
if (Platform.isIOS) {
deviceModel = (await deviceInfo.iosInfo).utsname.machine;
} else {
deviceModel = (await deviceInfo.androidInfo).model;
}
// ====== DOMAIN // ====== DOMAIN
di.registerFactory<UserService>(() => UserService(di()));
di.registerSingleton<Openapi>(
Openapi(
dio: Dio(
BaseOptions(
baseUrl: endpoint,
connectTimeout: const Duration(milliseconds: 5000),
receiveTimeout: const Duration(milliseconds: 3000),
headers: {
'deviceModel': deviceModel,
'deviceType': Platform.operatingSystem,
},
),
),
interceptors: [ImmichAuthInterceptor()],
),
);
di.registerFactory<ServerInfoService>(() => ServerInfoService(di())); di.registerFactory<ServerInfoService>(() => ServerInfoService(di()));
// ====== PRESENTATION // ====== PRESENTATION
di.registerLazySingleton<ServerFeatureConfigCubit>( di.registerLazySingleton<ServerFeatureConfigCubit>(
() => ServerFeatureConfigCubit(di()), () => ServerFeatureConfigCubit(di()),
); );
} }
static void registerCurrentUser(User user) { static void registerCurrentUser(User user) {
di.registerSingleton<CurrentUserCubit>(CurrentUserCubit(user)); if (di.isRegistered<CurrentUserCubit>()) {
di<CurrentUserCubit>().updateUser(user);
} else {
di.registerSingleton<CurrentUserCubit>(CurrentUserCubit(user));
}
} }
} }

View File

@ -1,117 +0,0 @@
/// GENERATED CODE - DO NOT MODIFY BY HAND
/// *****************************************************
/// FlutterGen
/// *****************************************************
// coverage:ignore-file
// ignore_for_file: type=lint
// ignore_for_file: directives_ordering,unnecessary_import,implicit_dynamic_list_literal,deprecated_member_use
import 'package:flutter/widgets.dart';
class $AssetsImagesGen {
const $AssetsImagesGen();
/// File path: assets/images/immich-logo.png
AssetGenImage get immichLogo =>
const AssetGenImage('assets/images/immich-logo.png');
/// File path: assets/images/immich-text-dark.png
AssetGenImage get immichTextDark =>
const AssetGenImage('assets/images/immich-text-dark.png');
/// File path: assets/images/immich-text-light.png
AssetGenImage get immichTextLight =>
const AssetGenImage('assets/images/immich-text-light.png');
/// List of all assets
List<AssetGenImage> get values =>
[immichLogo, immichTextDark, immichTextLight];
}
class Assets {
Assets._();
static const $AssetsImagesGen images = $AssetsImagesGen();
}
class AssetGenImage {
const AssetGenImage(
this._assetName, {
this.size,
this.flavors = const {},
});
final String _assetName;
final Size? size;
final Set<String> flavors;
Image image({
Key? key,
AssetBundle? bundle,
ImageFrameBuilder? frameBuilder,
ImageErrorWidgetBuilder? errorBuilder,
String? semanticLabel,
bool excludeFromSemantics = false,
double? scale,
double? width,
double? height,
Color? color,
Animation<double>? opacity,
BlendMode? colorBlendMode,
BoxFit? fit,
AlignmentGeometry alignment = Alignment.center,
ImageRepeat repeat = ImageRepeat.noRepeat,
Rect? centerSlice,
bool matchTextDirection = false,
bool gaplessPlayback = false,
bool isAntiAlias = false,
String? package,
FilterQuality filterQuality = FilterQuality.low,
int? cacheWidth,
int? cacheHeight,
}) {
return Image.asset(
_assetName,
key: key,
bundle: bundle,
frameBuilder: frameBuilder,
errorBuilder: errorBuilder,
semanticLabel: semanticLabel,
excludeFromSemantics: excludeFromSemantics,
scale: scale,
width: width,
height: height,
color: color,
opacity: opacity,
colorBlendMode: colorBlendMode,
fit: fit,
alignment: alignment,
repeat: repeat,
centerSlice: centerSlice,
matchTextDirection: matchTextDirection,
gaplessPlayback: gaplessPlayback,
isAntiAlias: isAntiAlias,
package: package,
filterQuality: filterQuality,
cacheWidth: cacheWidth,
cacheHeight: cacheHeight,
);
}
ImageProvider provider({
AssetBundle? bundle,
String? package,
}) {
return AssetImage(
_assetName,
bundle: bundle,
package: package,
);
}
String get path => _assetName;
String get keyName => _assetName;
}

View File

@ -3,5 +3,11 @@ import 'package:flutter/material.dart';
/// Log messages stored in the DB /// Log messages stored in the DB
const int kLogMessageLimit = 500; const int kLogMessageLimit = 500;
/// Headers
// Auth header
const String kImmichHeaderAuthKey = "x-immich-user-token";
const String kImmichHeaderDeviceModel = "deviceModel";
const String kImmichHeaderDeviceType = "deviceType";
/// Global ScaffoldMessengerKey to show snackbars /// Global ScaffoldMessengerKey to show snackbars
final GlobalKey<ScaffoldMessengerState> kScafMessengerKey = GlobalKey(); final GlobalKey<ScaffoldMessengerState> kScafMessengerKey = GlobalKey();

View File

@ -0,0 +1,92 @@
import 'dart:io';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:http/http.dart';
import 'package:immich_mobile/domain/interfaces/store.interface.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/presentation/router/router.dart';
import 'package:immich_mobile/service_locator.dart';
import 'package:immich_mobile/utils/constants/globals.dart';
import 'package:immich_mobile/utils/mixins/log_context.mixin.dart';
import 'package:openapi/api.dart';
class ImmichApiClient extends ApiClient with LogContext {
ImmichApiClient({required String endpoint}) : super(basePath: endpoint);
Future<void> init({String? accessToken}) async {
final token =
accessToken ?? (await di<IStoreRepository>().get(StoreKey.accessToken));
if (token != null) {
addDefaultHeader(kImmichHeaderAuthKey, token);
}
final deviceInfo = DeviceInfoPlugin();
final String deviceModel;
if (Platform.isIOS) {
deviceModel = (await deviceInfo.iosInfo).utsname.machine;
} else {
deviceModel = (await deviceInfo.androidInfo).model;
}
addDefaultHeader(kImmichHeaderDeviceModel, deviceModel);
addDefaultHeader(kImmichHeaderDeviceType, Platform.operatingSystem);
}
@override
Future<Response> invokeAPI(
String path,
String method,
List<QueryParam> queryParams,
Object? body,
Map<String, String> headerParams,
Map<String, String> formParams,
String? contentType,
) async {
final res = await super.invokeAPI(
path,
method,
queryParams,
body,
headerParams,
formParams,
contentType,
);
if (res.statusCode == HttpStatus.unauthorized) {
log.severe("Token invalid. Redirecting to login route");
await di<AppRouter>().replaceAll([const LoginRoute()]);
throw ApiException(res.statusCode, "Unauthorized");
}
return res;
}
// ignore: avoid-dynamic
static dynamic _patchDto(dynamic value, String targetType) {
switch (targetType) {
case 'UserPreferencesResponseDto':
if (value is Map) {
if (value['rating'] == null) {
value['rating'] = RatingResponse().toJson();
}
}
}
}
// ignore: avoid-dynamic
static dynamic fromJson(
// ignore: avoid-dynamic
dynamic value,
String targetType, {
bool growable = false,
}) {
_patchDto(value, targetType);
return ApiClient.fromJson(value, targetType, growable: growable);
}
UsersApi getUsersApi() => UsersApi(this);
ServerApi getServerApi() => ServerApi(this);
AuthenticationApi getAuthenticationApi() => AuthenticationApi(this);
OAuthApi getOAuthApi() => OAuthApi(this);
}

View File

@ -1,29 +0,0 @@
import 'package:dio/dio.dart';
import 'package:immich_mobile/presentation/router/router.dart';
import 'package:immich_mobile/service_locator.dart';
import 'package:immich_mobile/utils/mixins/log_context.mixin.dart';
class ImmichAuthInterceptor extends Interceptor with LogContext {
String? _accessToken;
void setAccessToken(String token) => _accessToken = token;
@override
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
if (_accessToken != null) {
options.headers["x-immich-user-token"] = _accessToken;
}
handler.next(options);
}
@override
void onResponse(Response response, ResponseInterceptorHandler handler) {
if (response.statusCode == 401) {
log.severe("Token expired. Logging user out");
di<AppRouter>().replaceAll([const LoginRoute()]);
return;
}
handler.next(response);
}
}

View File

@ -14,7 +14,6 @@ import 'package:logging/logging.dart';
class LogManager { class LogManager {
LogManager._(); LogManager._();
static final LogManager _instance = LogManager._(); static final LogManager _instance = LogManager._();
// ignore: match-getter-setter-field-names // ignore: match-getter-setter-field-names
static LogManager get I => _instance; static LogManager get I => _instance;

View File

@ -262,22 +262,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "7.0.1" version: "7.0.1"
dio:
dependency: "direct main"
description:
name: dio
sha256: "0dfb6b6a1979dac1c1245e17cef824d7b452ea29bd33d3467269f9bef3715fb0"
url: "https://pub.dev"
source: hosted
version: "5.6.0"
dio_web_adapter:
dependency: transitive
description:
name: dio_web_adapter
sha256: "33259a9276d6cea88774a0000cfae0d861003497755969c92faa223108620dc8"
url: "https://pub.dev"
source: hosted
version: "2.0.0"
drift: drift:
dependency: "direct main" dependency: "direct main"
description: description:
@ -451,7 +435,7 @@ packages:
source: hosted source: hosted
version: "2.0.0" version: "2.0.0"
http: http:
dependency: transitive dependency: "direct main"
description: description:
name: http name: http
sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010 sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010
@ -618,22 +602,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.0" version: "1.0.0"
one_of:
dependency: transitive
description:
name: one_of
sha256: "25fe0fcf181e761c6fcd604caf9d5fdf952321be17584ba81c72c06bdaa511f0"
url: "https://pub.dev"
source: hosted
version: "1.5.0"
one_of_serializer:
dependency: transitive
description:
name: one_of_serializer
sha256: "3f3dfb5c1578ba3afef1cb47fcc49e585e797af3f2b6c2cc7ed90aad0c5e7b83"
url: "https://pub.dev"
source: hosted
version: "1.5.0"
openapi: openapi:
dependency: "direct main" dependency: "direct main"
description: description:
@ -801,14 +769,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.3.0" version: "1.3.0"
quiver:
dependency: transitive
description:
name: quiver
sha256: b1c1ac5ce6688d77f65f3375a9abb9319b3cb32486bdc7a1e0fdf004d7ba4e47
url: "https://pub.dev"
source: hosted
version: "3.2.1"
recase: recase:
dependency: transitive dependency: transitive
description: description:

View File

@ -16,14 +16,14 @@ dependencies:
# OS specific path # OS specific path
path_provider: ^2.1.4 path_provider: ^2.1.4
path: ^1.9.0 path: ^1.9.0
# Bloc # State handling
flutter_bloc: ^8.1.6 flutter_bloc: ^8.1.6
# Database # Database
drift: ^2.20.0 drift: ^2.20.0
sqlite3: ^2.4.6 sqlite3: ^2.4.6
sqlite3_flutter_libs: ^0.5.24 sqlite3_flutter_libs: ^0.5.24
# Network # Network
dio: ^5.6.0 http: ^1.2.2
# Route handling # Route handling
auto_route: ^9.2.2 auto_route: ^9.2.2
# Logging # Logging
@ -55,6 +55,11 @@ dependencies:
openapi: openapi:
path: openapi path: openapi
dependency_overrides:
# openapi uses an older version of http for backward compatibility. New versions do not have
# a breaking change so it is safer to override it and use the latest version for the app
http: ^1.2.1
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
sdk: flutter sdk: flutter

View File

@ -3,6 +3,7 @@ OPENAPI_GENERATOR_VERSION=v7.8.0
# usage: ./bin/generate-open-api.sh # usage: ./bin/generate-open-api.sh
function dart { function dart {
rm -rf ../mobile/openapi rm -rf ../mobile/openapi
cd ./templates/mobile/serialization/native cd ./templates/mobile/serialization/native
@ -23,11 +24,20 @@ function dart {
function dartDio { function dartDio {
rm -rf ../mobile-v2/openapi rm -rf ../mobile-v2/openapi
npx --yes @openapitools/openapi-generator-cli generate -g dart-dio -i ./immich-openapi-specs.json -o ../mobile-v2/openapi --global-property skipFormModel=false --global-property models,apis,supportingFiles,apiTests=false,apiDocs=false,modelTests=false,modelDocs=false cd ./templates/mobile/serialization/native
wget -O native_class.mustache https://raw.githubusercontent.com/OpenAPITools/openapi-generator/$OPENAPI_GENERATOR_VERSION/modules/openapi-generator/src/main/resources/dart2/serialization/native/native_class.mustache
patch --no-backup-if-mismatch -u native_class.mustache <native_class.mustache.patch
cd ../../../../
npx --yes @openapitools/openapi-generator-cli generate -g dart -i ./immich-openapi-specs.json -o ../mobile-v2/openapi -t ./templates/mobile
# Post generate patches
patch --no-backup-if-mismatch -u ../mobile-v2/openapi/lib/api_client.dart <./patch/api_client.dart.patch
patch --no-backup-if-mismatch -u ../mobile-v2/openapi/lib/api.dart <./patch/api.dart.patch
patch --no-backup-if-mismatch -u ../mobile-v2/openapi/pubspec.yaml <./patch/pubspec_immich_mobile.yaml.patch
# Don't include analysis_options.yaml for the generated openapi files # Don't include analysis_options.yaml for the generated openapi files
# so that language servers can properly exclude the mobile/openapi directory # so that language servers can properly exclude the mobile/openapi directory
rm ../mobile-v2/openapi/analysis_options.yaml rm ../mobile-v2/openapi/analysis_options.yaml
echo "export 'package:openapi/src/auth/bearer_auth.dart';" >> ../mobile-v2/openapi/lib/openapi.dart
} }
function typescript { function typescript {