浏览代码

feat(server): require auth for more endpoints (#2092)

* feat(server): require auth for more endpoints

* dev: add authorization header to profile image on mobile

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>
Michel Heusschen 2 年之前
父节点
当前提交
089dbdbd7e

+ 3 - 22
mobile/lib/modules/home/ui/home_page_app_bar.dart

@@ -1,8 +1,7 @@
-import 'dart:math';
-
 import 'package:auto_route/auto_route.dart';
 import 'package:auto_route/auto_route.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/material.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/modules/home/ui/user_circle_avatar.dart';
 import 'package:immich_mobile/modules/login/models/authentication_state.model.dart';
 import 'package:immich_mobile/modules/login/models/authentication_state.model.dart';
 import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
 import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
 
 
@@ -10,9 +9,7 @@ import 'package:immich_mobile/routing/router.dart';
 import 'package:immich_mobile/modules/backup/models/backup_state.model.dart';
 import 'package:immich_mobile/modules/backup/models/backup_state.model.dart';
 import 'package:immich_mobile/shared/models/server_info_state.model.dart';
 import 'package:immich_mobile/shared/models/server_info_state.model.dart';
 import 'package:immich_mobile/modules/backup/providers/backup.provider.dart';
 import 'package:immich_mobile/modules/backup/providers/backup.provider.dart';
-import 'package:immich_mobile/shared/models/store.dart';
 import 'package:immich_mobile/shared/providers/server_info.provider.dart';
 import 'package:immich_mobile/shared/providers/server_info.provider.dart';
-import 'package:immich_mobile/shared/ui/transparent_image.dart';
 
 
 class HomePageAppBar extends ConsumerWidget with PreferredSizeWidget {
 class HomePageAppBar extends ConsumerWidget with PreferredSizeWidget {
   @override
   @override
@@ -46,29 +43,13 @@ class HomePageAppBar extends ConsumerWidget with PreferredSizeWidget {
           },
           },
         );
         );
       } else {
       } else {
-        final String? endpoint = Store.get(StoreKey.serverEndpoint);
-        var dummy = Random().nextInt(1024);
         return InkWell(
         return InkWell(
           onTap: () {
           onTap: () {
             Scaffold.of(context).openDrawer();
             Scaffold.of(context).openDrawer();
           },
           },
-          child: CircleAvatar(
-            backgroundColor: Theme.of(context).primaryColor,
+          child: const UserCircleAvatar(
             radius: 18,
             radius: 18,
-            child: ClipRRect(
-              borderRadius: BorderRadius.circular(50),
-              child: FadeInImage.memoryNetwork(
-                fit: BoxFit.cover,
-                placeholder: kTransparentImage,
-                width: 33,
-                height: 33,
-                image:
-                    '$endpoint/user/profile-image/${authState.userId}?d=${dummy++}',
-                fadeInDuration: const Duration(milliseconds: 200),
-                imageErrorBuilder: (context, error, stackTrace) =>
-                    Image.memory(kTransparentImage),
-              ),
-            ),
+            size: 33,
           ),
           ),
         );
         );
       }
       }

+ 3 - 22
mobile/lib/modules/home/ui/profile_drawer/profile_drawer_header.dart

@@ -1,15 +1,12 @@
-import 'dart:math';
-
 import 'package:flutter/material.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_hooks/flutter_hooks.dart' hide Store;
 import 'package:flutter_hooks/flutter_hooks.dart' hide Store;
 import 'package:hooks_riverpod/hooks_riverpod.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
 import 'package:image_picker/image_picker.dart';
 import 'package:image_picker/image_picker.dart';
 import 'package:immich_mobile/modules/home/providers/upload_profile_image.provider.dart';
 import 'package:immich_mobile/modules/home/providers/upload_profile_image.provider.dart';
+import 'package:immich_mobile/modules/home/ui/user_circle_avatar.dart';
 import 'package:immich_mobile/modules/login/models/authentication_state.model.dart';
 import 'package:immich_mobile/modules/login/models/authentication_state.model.dart';
 import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
 import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
-import 'package:immich_mobile/shared/models/store.dart';
 import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
 import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
-import 'package:immich_mobile/shared/ui/transparent_image.dart';
 
 
 class ProfileDrawerHeader extends HookConsumerWidget {
 class ProfileDrawerHeader extends HookConsumerWidget {
   const ProfileDrawerHeader({
   const ProfileDrawerHeader({
@@ -18,31 +15,15 @@ class ProfileDrawerHeader extends HookConsumerWidget {
 
 
   @override
   @override
   Widget build(BuildContext context, WidgetRef ref) {
   Widget build(BuildContext context, WidgetRef ref) {
-    final String endpoint = Store.get(StoreKey.serverEndpoint);
     AuthenticationState authState = ref.watch(authenticationProvider);
     AuthenticationState authState = ref.watch(authenticationProvider);
     final uploadProfileImageStatus =
     final uploadProfileImageStatus =
         ref.watch(uploadProfileImageProvider).status;
         ref.watch(uploadProfileImageProvider).status;
-    var dummy = Random().nextInt(1024);
     final isDarkMode = Theme.of(context).brightness == Brightness.dark;
     final isDarkMode = Theme.of(context).brightness == Brightness.dark;
 
 
     buildUserProfileImage() {
     buildUserProfileImage() {
-      var userImage = CircleAvatar(
-        backgroundColor: Theme.of(context).primaryColor,
+      var userImage = const UserCircleAvatar(
         radius: 35,
         radius: 35,
-        child: ClipRRect(
-          borderRadius: BorderRadius.circular(50),
-          child: FadeInImage.memoryNetwork(
-            fit: BoxFit.cover,
-            placeholder: kTransparentImage,
-            width: 66,
-            height: 66,
-            image:
-                '$endpoint/user/profile-image/${authState.userId}?d=${dummy++}',
-            fadeInDuration: const Duration(milliseconds: 200),
-            imageErrorBuilder: (context, error, stackTrace) =>
-                Image.memory(kTransparentImage),
-          ),
-        ),
+        size: 66,
       );
       );
 
 
       if (authState.profileImagePath.isEmpty) {
       if (authState.profileImagePath.isEmpty) {

+ 44 - 0
mobile/lib/modules/home/ui/user_circle_avatar.dart

@@ -0,0 +1,44 @@
+import 'dart:math';
+
+import 'package:flutter/material.dart';
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/modules/login/models/authentication_state.model.dart';
+import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
+import 'package:immich_mobile/shared/models/store.dart';
+import 'package:immich_mobile/shared/ui/transparent_image.dart';
+
+class UserCircleAvatar extends ConsumerWidget {
+  final double radius;
+  final double size;
+  const UserCircleAvatar({super.key, required this.radius, required this.size});
+
+  @override
+  Widget build(BuildContext context, WidgetRef ref) {
+    AuthenticationState authState = ref.watch(authenticationProvider);
+
+    var profileImageUrl =
+        '${Store.get(StoreKey.serverEndpoint)}/user/profile-image/${authState.userId}?d=${Random().nextInt(1024)}';
+    return CircleAvatar(
+      backgroundColor: Theme.of(context).primaryColor,
+      radius: radius,
+      child: ClipRRect(
+        borderRadius: BorderRadius.circular(50),
+        child: FadeInImage(
+          fit: BoxFit.cover,
+          placeholder: MemoryImage(kTransparentImage),
+          width: size,
+          height: size,
+          image: NetworkImage(
+            profileImageUrl,
+            headers: {
+              "Authorization": "Bearer ${Store.get(StoreKey.accessToken)}"
+            },
+          ),
+          fadeInDuration: const Duration(milliseconds: 200),
+          imageErrorBuilder: (context, error, stackTrace) =>
+              Image.memory(kTransparentImage),
+        ),
+      ),
+    );
+  }
+}

+ 11 - 1
mobile/openapi/doc/ServerInfoApi.md

@@ -25,6 +25,16 @@ Method | HTTP request | Description
 ### Example
 ### Example
 ```dart
 ```dart
 import 'package:openapi/api.dart';
 import 'package:openapi/api.dart';
+// TODO Configure HTTP Bearer authorization: bearer
+// Case 1. Use String Token
+//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken('YOUR_ACCESS_TOKEN');
+// Case 2. Use Function which generate token.
+// String yourTokenGeneratorFunction() { ... }
+//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
+// TODO Configure API key authorization: cookie
+//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKey = 'YOUR_API_KEY';
+// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
+//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKeyPrefix = 'Bearer';
 
 
 final api_instance = ServerInfoApi();
 final api_instance = ServerInfoApi();
 
 
@@ -45,7 +55,7 @@ This endpoint does not need any parameter.
 
 
 ### Authorization
 ### Authorization
 
 
-No authorization required
+[bearer](../README.md#bearer), [cookie](../README.md#cookie)
 
 
 ### HTTP request headers
 ### HTTP request headers
 
 

+ 22 - 2
mobile/openapi/doc/UserApi.md

@@ -292,6 +292,16 @@ This endpoint does not need any parameter.
 ### Example
 ### Example
 ```dart
 ```dart
 import 'package:openapi/api.dart';
 import 'package:openapi/api.dart';
+// TODO Configure HTTP Bearer authorization: bearer
+// Case 1. Use String Token
+//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken('YOUR_ACCESS_TOKEN');
+// Case 2. Use Function which generate token.
+// String yourTokenGeneratorFunction() { ... }
+//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
+// TODO Configure API key authorization: cookie
+//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKey = 'YOUR_API_KEY';
+// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
+//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKeyPrefix = 'Bearer';
 
 
 final api_instance = UserApi();
 final api_instance = UserApi();
 final userId = userId_example; // String | 
 final userId = userId_example; // String | 
@@ -316,7 +326,7 @@ Name | Type | Description  | Notes
 
 
 ### Authorization
 ### Authorization
 
 
-No authorization required
+[bearer](../README.md#bearer), [cookie](../README.md#cookie)
 
 
 ### HTTP request headers
 ### HTTP request headers
 
 
@@ -335,6 +345,16 @@ No authorization required
 ### Example
 ### Example
 ```dart
 ```dart
 import 'package:openapi/api.dart';
 import 'package:openapi/api.dart';
+// TODO Configure HTTP Bearer authorization: bearer
+// Case 1. Use String Token
+//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken('YOUR_ACCESS_TOKEN');
+// Case 2. Use Function which generate token.
+// String yourTokenGeneratorFunction() { ... }
+//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
+// TODO Configure API key authorization: cookie
+//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKey = 'YOUR_API_KEY';
+// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
+//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKeyPrefix = 'Bearer';
 
 
 final api_instance = UserApi();
 final api_instance = UserApi();
 final userId = userId_example; // String | 
 final userId = userId_example; // String | 
@@ -359,7 +379,7 @@ Name | Type | Description  | Notes
 
 
 ### Authorization
 ### Authorization
 
 
-No authorization required
+[bearer](../README.md#bearer), [cookie](../README.md#cookie)
 
 
 ### HTTP request headers
 ### HTTP request headers
 
 

+ 1 - 0
server/apps/immich/src/controllers/server-info.controller.ts

@@ -14,6 +14,7 @@ import { Authenticated } from '../decorators/authenticated.decorator';
 export class ServerInfoController {
 export class ServerInfoController {
   constructor(private service: ServerInfoService) {}
   constructor(private service: ServerInfoService) {}
 
 
+  @Authenticated()
   @Get()
   @Get()
   getServerInfo(): Promise<ServerInfoResponseDto> {
   getServerInfo(): Promise<ServerInfoResponseDto> {
     return this.service.getInfo();
     return this.service.getInfo();

+ 3 - 1
server/apps/immich/src/controllers/user.controller.ts

@@ -44,6 +44,7 @@ export class UserController {
     return this.service.getAllUsers(authUser, isAll);
     return this.service.getAllUsers(authUser, isAll);
   }
   }
 
 
+  @Authenticated()
   @Get('/info/:userId')
   @Get('/info/:userId')
   getUserById(@Param('userId') userId: string): Promise<UserResponseDto> {
   getUserById(@Param('userId') userId: string): Promise<UserResponseDto> {
     return this.service.getUserById(userId);
     return this.service.getUserById(userId);
@@ -87,8 +88,8 @@ export class UserController {
     return this.service.updateUser(authUser, updateUserDto);
     return this.service.updateUser(authUser, updateUserDto);
   }
   }
 
 
-  @UseInterceptors(FileInterceptor('file', profileImageUploadOption))
   @Authenticated()
   @Authenticated()
+  @UseInterceptors(FileInterceptor('file', profileImageUploadOption))
   @ApiConsumes('multipart/form-data')
   @ApiConsumes('multipart/form-data')
   @ApiBody({
   @ApiBody({
     description: 'A new avatar for the user',
     description: 'A new avatar for the user',
@@ -102,6 +103,7 @@ export class UserController {
     return this.service.createProfileImage(authUser, fileInfo);
     return this.service.createProfileImage(authUser, fileInfo);
   }
   }
 
 
+  @Authenticated()
   @Get('/profile-image/:userId')
   @Get('/profile-image/:userId')
   @Header('Cache-Control', 'max-age=600')
   @Header('Cache-Control', 'max-age=600')
   async getProfileImage(@Param('userId') userId: string, @Response({ passthrough: true }) res: Res): Promise<any> {
   async getProfileImage(@Param('userId') userId: string, @Response({ passthrough: true }) res: Res): Promise<any> {

+ 24 - 0
server/immich-openapi-specs.json

@@ -943,6 +943,14 @@
         },
         },
         "tags": [
         "tags": [
           "Server Info"
           "Server Info"
+        ],
+        "security": [
+          {
+            "bearer": []
+          },
+          {
+            "cookie": []
+          }
         ]
         ]
       }
       }
     },
     },
@@ -1482,6 +1490,14 @@
         },
         },
         "tags": [
         "tags": [
           "User"
           "User"
+        ],
+        "security": [
+          {
+            "bearer": []
+          },
+          {
+            "cookie": []
+          }
         ]
         ]
       }
       }
     },
     },
@@ -1694,6 +1710,14 @@
         },
         },
         "tags": [
         "tags": [
           "User"
           "User"
+        ],
+        "security": [
+          {
+            "bearer": []
+          },
+          {
+            "cookie": []
+          }
         ]
         ]
       }
       }
     },
     },

+ 18 - 0
web/src/api/open-api/api.ts

@@ -7043,6 +7043,12 @@ export const ServerInfoApiAxiosParamCreator = function (configuration?: Configur
             const localVarHeaderParameter = {} as any;
             const localVarHeaderParameter = {} as any;
             const localVarQueryParameter = {} as any;
             const localVarQueryParameter = {} as any;
 
 
+            // authentication bearer required
+            // http bearer authentication required
+            await setBearerAuthToObject(localVarHeaderParameter, configuration)
+
+            // authentication cookie required
+
 
 
     
     
             setSearchParams(localVarUrlObj, localVarQueryParameter);
             setSearchParams(localVarUrlObj, localVarQueryParameter);
@@ -8586,6 +8592,12 @@ export const UserApiAxiosParamCreator = function (configuration?: Configuration)
             const localVarHeaderParameter = {} as any;
             const localVarHeaderParameter = {} as any;
             const localVarQueryParameter = {} as any;
             const localVarQueryParameter = {} as any;
 
 
+            // authentication bearer required
+            // http bearer authentication required
+            await setBearerAuthToObject(localVarHeaderParameter, configuration)
+
+            // authentication cookie required
+
 
 
     
     
             setSearchParams(localVarUrlObj, localVarQueryParameter);
             setSearchParams(localVarUrlObj, localVarQueryParameter);
@@ -8619,6 +8631,12 @@ export const UserApiAxiosParamCreator = function (configuration?: Configuration)
             const localVarHeaderParameter = {} as any;
             const localVarHeaderParameter = {} as any;
             const localVarQueryParameter = {} as any;
             const localVarQueryParameter = {} as any;
 
 
+            // authentication bearer required
+            // http bearer authentication required
+            await setBearerAuthToObject(localVarHeaderParameter, configuration)
+
+            // authentication cookie required
+
 
 
     
     
             setSearchParams(localVarUrlObj, localVarQueryParameter);
             setSearchParams(localVarUrlObj, localVarQueryParameter);