mirror of
https://github.com/immich-app/immich.git
synced 2025-01-04 02:46:47 +01:00
feat(mobile): Add support for Basic Authentication (#6840)
This commit is contained in:
parent
b4c211cad1
commit
5061c35c8d
14 changed files with 33 additions and 33 deletions
|
@ -49,7 +49,7 @@ class AlbumThumbnailListTile extends StatelessWidget {
|
||||||
type: ThumbnailFormat.WEBP,
|
type: ThumbnailFormat.WEBP,
|
||||||
),
|
),
|
||||||
httpHeaders: {
|
httpHeaders: {
|
||||||
"Authorization": "Bearer ${Store.get(StoreKey.accessToken)}",
|
"x-immich-user-token": Store.get(StoreKey.accessToken),
|
||||||
},
|
},
|
||||||
cacheKey: getAlbumThumbNailCacheKey(album, type: ThumbnailFormat.WEBP),
|
cacheKey: getAlbumThumbNailCacheKey(album, type: ThumbnailFormat.WEBP),
|
||||||
errorWidget: (context, url, error) =>
|
errorWidget: (context, url, error) =>
|
||||||
|
|
|
@ -78,8 +78,7 @@ class GalleryViewerPage extends HookConsumerWidget {
|
||||||
final isPlayingMotionVideo = useState(false);
|
final isPlayingMotionVideo = useState(false);
|
||||||
final isPlayingVideo = useState(false);
|
final isPlayingVideo = useState(false);
|
||||||
Offset? localPosition;
|
Offset? localPosition;
|
||||||
final authToken = 'Bearer ${Store.get(StoreKey.accessToken)}';
|
final header = {"x-immich-user-token": Store.get(StoreKey.accessToken)};
|
||||||
final header = {"Authorization": authToken};
|
|
||||||
final currentIndex = useState(initialIndex);
|
final currentIndex = useState(initialIndex);
|
||||||
final currentAsset = loadAsset(currentIndex.value);
|
final currentAsset = loadAsset(currentIndex.value);
|
||||||
final isTrashEnabled =
|
final isTrashEnabled =
|
||||||
|
@ -524,8 +523,7 @@ class GalleryViewerPage extends HookConsumerWidget {
|
||||||
imageUrl:
|
imageUrl:
|
||||||
'${Store.get(StoreKey.serverEndpoint)}/asset/thumbnail/$assetId',
|
'${Store.get(StoreKey.serverEndpoint)}/asset/thumbnail/$assetId',
|
||||||
httpHeaders: {
|
httpHeaders: {
|
||||||
"Authorization":
|
"x-immich-user-token": Store.get(StoreKey.accessToken),
|
||||||
"Bearer ${Store.get(StoreKey.accessToken)}",
|
|
||||||
},
|
},
|
||||||
errorWidget: (context, url, error) =>
|
errorWidget: (context, url, error) =>
|
||||||
const Icon(Icons.image_not_supported_outlined),
|
const Icon(Icons.image_not_supported_outlined),
|
||||||
|
|
|
@ -68,7 +68,7 @@ class VideoViewerPage extends HookConsumerWidget {
|
||||||
children: [
|
children: [
|
||||||
VideoPlayer(
|
VideoPlayer(
|
||||||
url: videoUrl,
|
url: videoUrl,
|
||||||
jwtToken: Store.get(StoreKey.accessToken),
|
accessToken: Store.get(StoreKey.accessToken),
|
||||||
isMotionVideo: isMotionVideo,
|
isMotionVideo: isMotionVideo,
|
||||||
onVideoEnded: onVideoEnded,
|
onVideoEnded: onVideoEnded,
|
||||||
onPaused: onPaused,
|
onPaused: onPaused,
|
||||||
|
@ -99,7 +99,7 @@ final _fileFamily =
|
||||||
|
|
||||||
class VideoPlayer extends StatefulWidget {
|
class VideoPlayer extends StatefulWidget {
|
||||||
final String? url;
|
final String? url;
|
||||||
final String? jwtToken;
|
final String? accessToken;
|
||||||
final File? file;
|
final File? file;
|
||||||
final bool isMotionVideo;
|
final bool isMotionVideo;
|
||||||
final VoidCallback onVideoEnded;
|
final VoidCallback onVideoEnded;
|
||||||
|
@ -114,7 +114,7 @@ class VideoPlayer extends StatefulWidget {
|
||||||
const VideoPlayer({
|
const VideoPlayer({
|
||||||
super.key,
|
super.key,
|
||||||
this.url,
|
this.url,
|
||||||
this.jwtToken,
|
this.accessToken,
|
||||||
this.file,
|
this.file,
|
||||||
required this.onVideoEnded,
|
required this.onVideoEnded,
|
||||||
required this.isMotionVideo,
|
required this.isMotionVideo,
|
||||||
|
@ -160,7 +160,7 @@ class _VideoPlayerState extends State<VideoPlayer> {
|
||||||
videoPlayerController = widget.file == null
|
videoPlayerController = widget.file == null
|
||||||
? VideoPlayerController.networkUrl(
|
? VideoPlayerController.networkUrl(
|
||||||
Uri.parse(widget.url!),
|
Uri.parse(widget.url!),
|
||||||
httpHeaders: {"Authorization": "Bearer ${widget.jwtToken}"},
|
httpHeaders: {"x-immich-user-token": widget.accessToken ?? ""},
|
||||||
)
|
)
|
||||||
: VideoPlayerController.file(widget.file!);
|
: VideoPlayerController.file(widget.file!);
|
||||||
|
|
||||||
|
|
|
@ -302,8 +302,7 @@ class BackupService {
|
||||||
onProgress: ((bytes, totalBytes) =>
|
onProgress: ((bytes, totalBytes) =>
|
||||||
uploadProgressCb(bytes, totalBytes)),
|
uploadProgressCb(bytes, totalBytes)),
|
||||||
);
|
);
|
||||||
req.headers["Authorization"] =
|
req.headers["x-immich-user-token"] = Store.get(StoreKey.accessToken);
|
||||||
"Bearer ${Store.get(StoreKey.accessToken)}";
|
|
||||||
req.headers["Transfer-Encoding"] = "chunked";
|
req.headers["Transfer-Encoding"] = "chunked";
|
||||||
|
|
||||||
req.fields['deviceAssetId'] = entity.id;
|
req.fields['deviceAssetId'] = entity.id;
|
||||||
|
|
|
@ -89,8 +89,7 @@ class _AssetMarkerIcon extends StatelessWidget {
|
||||||
imageUrl,
|
imageUrl,
|
||||||
cacheKey: cacheKey,
|
cacheKey: cacheKey,
|
||||||
headers: {
|
headers: {
|
||||||
"Authorization":
|
"x-immich-user-token": Store.get(StoreKey.accessToken),
|
||||||
"Bearer ${Store.get(StoreKey.accessToken)}",
|
|
||||||
},
|
},
|
||||||
errorListener: (_) =>
|
errorListener: (_) =>
|
||||||
const Icon(Icons.image_not_supported_outlined),
|
const Icon(Icons.image_not_supported_outlined),
|
||||||
|
|
|
@ -27,7 +27,7 @@ class MemoryCard extends StatelessWidget {
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
String get authToken => 'Bearer ${Store.get(StoreKey.accessToken)}';
|
String get accessToken => Store.get(StoreKey.accessToken);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
@ -55,7 +55,7 @@ class MemoryCard extends StatelessWidget {
|
||||||
cacheKey: getThumbnailCacheKey(
|
cacheKey: getThumbnailCacheKey(
|
||||||
asset,
|
asset,
|
||||||
),
|
),
|
||||||
headers: {"Authorization": authToken},
|
headers: {"x-immich-user-token": accessToken},
|
||||||
),
|
),
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
),
|
),
|
||||||
|
|
|
@ -51,7 +51,7 @@ class CuratedPeopleRow extends StatelessWidget {
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final person = content[index];
|
final person = content[index];
|
||||||
final headers = {
|
final headers = {
|
||||||
"Authorization": "Bearer ${Store.get(StoreKey.accessToken)}",
|
"x-immich-user-token": Store.get(StoreKey.accessToken),
|
||||||
};
|
};
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.only(right: 18.0),
|
padding: const EdgeInsets.only(right: 18.0),
|
||||||
|
|
|
@ -46,8 +46,7 @@ class ThumbnailWithInfo extends StatelessWidget {
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
imageUrl: imageUrl!,
|
imageUrl: imageUrl!,
|
||||||
httpHeaders: {
|
httpHeaders: {
|
||||||
"Authorization":
|
"x-immich-user-token": Store.get(StoreKey.accessToken),
|
||||||
"Bearer ${Store.get(StoreKey.accessToken)}",
|
|
||||||
},
|
},
|
||||||
errorWidget: (context, url, error) =>
|
errorWidget: (context, url, error) =>
|
||||||
const Icon(Icons.image_not_supported_outlined),
|
const Icon(Icons.image_not_supported_outlined),
|
||||||
|
|
|
@ -123,8 +123,7 @@ class PersonResultPage extends HookConsumerWidget {
|
||||||
backgroundImage: NetworkImage(
|
backgroundImage: NetworkImage(
|
||||||
getFaceThumbnailUrl(personId),
|
getFaceThumbnailUrl(personId),
|
||||||
headers: {
|
headers: {
|
||||||
"Authorization":
|
"x-immich-user-token": Store.get(StoreKey.accessToken),
|
||||||
"Bearer ${Store.get(StoreKey.accessToken)}",
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
|
@ -106,6 +108,10 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> {
|
||||||
final accessToken = Store.get(StoreKey.accessToken);
|
final accessToken = Store.get(StoreKey.accessToken);
|
||||||
try {
|
try {
|
||||||
final endpoint = Uri.parse(Store.get(StoreKey.serverEndpoint));
|
final endpoint = Uri.parse(Store.get(StoreKey.serverEndpoint));
|
||||||
|
final headers = {"x-immich-user-token": accessToken};
|
||||||
|
if (endpoint.userInfo.isNotEmpty) {
|
||||||
|
headers["Authorization"] = "Basic ${base64.encode(utf8.encode(endpoint.userInfo))}";
|
||||||
|
}
|
||||||
|
|
||||||
debugPrint("Attempting to connect to websocket");
|
debugPrint("Attempting to connect to websocket");
|
||||||
// Configure socket transports must be specified
|
// Configure socket transports must be specified
|
||||||
|
@ -118,7 +124,7 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> {
|
||||||
.enableForceNew()
|
.enableForceNew()
|
||||||
.enableForceNewConnection()
|
.enableForceNewConnection()
|
||||||
.enableAutoConnect()
|
.enableAutoConnect()
|
||||||
.setExtraHeaders({"Authorization": "Bearer $accessToken"})
|
.setExtraHeaders(headers)
|
||||||
.build(),
|
.build(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -31,12 +31,12 @@ class ApiService {
|
||||||
setEndpoint(endpoint);
|
setEndpoint(endpoint);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
String? _authToken;
|
String? _accessToken;
|
||||||
|
|
||||||
setEndpoint(String endpoint) {
|
setEndpoint(String endpoint) {
|
||||||
_apiClient = ApiClient(basePath: endpoint);
|
_apiClient = ApiClient(basePath: endpoint);
|
||||||
if (_authToken != null) {
|
if (_accessToken != null) {
|
||||||
setAccessToken(_authToken!);
|
setAccessToken(_accessToken!);
|
||||||
}
|
}
|
||||||
userApi = UserApi(_apiClient);
|
userApi = UserApi(_apiClient);
|
||||||
authenticationApi = AuthenticationApi(_apiClient);
|
authenticationApi = AuthenticationApi(_apiClient);
|
||||||
|
@ -134,8 +134,8 @@ class ApiService {
|
||||||
}
|
}
|
||||||
|
|
||||||
setAccessToken(String accessToken) {
|
setAccessToken(String accessToken) {
|
||||||
_authToken = accessToken;
|
_accessToken = accessToken;
|
||||||
_apiClient.addDefaultHeader('Authorization', 'Bearer $accessToken');
|
_apiClient.addDefaultHeader('x-immich-user-token', accessToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
ApiClient get apiClient => _apiClient;
|
ApiClient get apiClient => _apiClient;
|
||||||
|
|
|
@ -95,11 +95,11 @@ class ImmichImage extends StatelessWidget {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
final String? token = Store.get(StoreKey.accessToken);
|
final String? accessToken = Store.get(StoreKey.accessToken);
|
||||||
final String thumbnailRequestUrl = getThumbnailUrl(asset, type: type);
|
final String thumbnailRequestUrl = getThumbnailUrl(asset, type: type);
|
||||||
return CachedNetworkImage(
|
return CachedNetworkImage(
|
||||||
imageUrl: thumbnailRequestUrl,
|
imageUrl: thumbnailRequestUrl,
|
||||||
httpHeaders: {"Authorization": "Bearer $token"},
|
httpHeaders: {"x-immich-user-token": accessToken ?? ""},
|
||||||
cacheKey: getThumbnailCacheKey(asset, type: type),
|
cacheKey: getThumbnailCacheKey(asset, type: type),
|
||||||
width: width,
|
width: width,
|
||||||
height: height,
|
height: height,
|
||||||
|
@ -177,7 +177,7 @@ class ImmichImage extends StatelessWidget {
|
||||||
getThumbnailUrlForRemoteId(assetId, type: type),
|
getThumbnailUrlForRemoteId(assetId, type: type),
|
||||||
cacheKey: getThumbnailCacheKeyForRemoteId(assetId, type: type),
|
cacheKey: getThumbnailCacheKeyForRemoteId(assetId, type: type),
|
||||||
headers: {
|
headers: {
|
||||||
"Authorization": 'Bearer ${Store.get(StoreKey.accessToken)}',
|
"x-immich-user-token": Store.get(StoreKey.accessToken),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -195,10 +195,10 @@ class ImmichImage extends StatelessWidget {
|
||||||
context,
|
context,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
final authToken = 'Bearer ${Store.get(StoreKey.accessToken)}';
|
final accessToken = Store.get(StoreKey.accessToken);
|
||||||
// Precache the remote image since we are not using local images
|
// Precache the remote image since we are not using local images
|
||||||
return precacheImage(
|
return precacheImage(
|
||||||
remoteThumbnailProvider(asset, type, {"Authorization": authToken}),
|
remoteThumbnailProvider(asset, type, {"x-immich-user-token": accessToken}),
|
||||||
context,
|
context,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,7 +13,7 @@ Widget userAvatar(BuildContext context, User u, {double? radius}) {
|
||||||
backgroundColor: context.primaryColor.withAlpha(50),
|
backgroundColor: context.primaryColor.withAlpha(50),
|
||||||
foregroundImage: CachedNetworkImageProvider(
|
foregroundImage: CachedNetworkImageProvider(
|
||||||
url,
|
url,
|
||||||
headers: {"Authorization": "Bearer ${Store.get(StoreKey.accessToken)}"},
|
headers: {"x-immich-user-token": Store.get(StoreKey.accessToken)},
|
||||||
cacheKey: "user-${u.id}-profile",
|
cacheKey: "user-${u.id}-profile",
|
||||||
),
|
),
|
||||||
// silence errors if user has no profile image, use initials as fallback
|
// silence errors if user has no profile image, use initials as fallback
|
||||||
|
|
|
@ -51,7 +51,7 @@ class UserCircleAvatar extends ConsumerWidget {
|
||||||
placeholder: (_, __) => Image.memory(kTransparentImage),
|
placeholder: (_, __) => Image.memory(kTransparentImage),
|
||||||
imageUrl: profileImageUrl,
|
imageUrl: profileImageUrl,
|
||||||
httpHeaders: {
|
httpHeaders: {
|
||||||
"Authorization": "Bearer ${Store.get(StoreKey.accessToken)}",
|
"x-immich-user-token": Store.get(StoreKey.accessToken),
|
||||||
},
|
},
|
||||||
fadeInDuration: const Duration(milliseconds: 300),
|
fadeInDuration: const Duration(milliseconds: 300),
|
||||||
errorWidget: (context, error, stackTrace) => textIcon,
|
errorWidget: (context, error, stackTrace) => textIcon,
|
||||||
|
|
Loading…
Reference in a new issue