Manage secret

This commit is contained in:
2025-08-24 23:46:14 +08:00
parent 246ac52d0a
commit a03d6015a6
7 changed files with 192 additions and 73 deletions

View File

@@ -922,5 +922,9 @@
"deleteSecret": "Delete Secret", "deleteSecret": "Delete Secret",
"deleteSecretHint": "Are you sure you want to delete this secret? This action cannot be undone.", "deleteSecretHint": "Are you sure you want to delete this secret? This action cannot be undone.",
"generateSecret": "Generate New Secret", "generateSecret": "Generate New Secret",
"created_at": "Created at {}" "createdAt": "Created at {}",
"newSecretGenerated": "New Secret Generated",
"copySecretHint": "Please copy this secret and store it somewhere safe. You will not be able to see it again.",
"expiresIn": "Expires In (seconds)",
"isOidc": "OIDC Compliant"
} }

View File

@@ -851,5 +851,9 @@
"deleteSecret": "删除密钥", "deleteSecret": "删除密钥",
"deleteSecretHint": "您确定要删除此密钥吗?此操作无法撤销。", "deleteSecretHint": "您确定要删除此密钥吗?此操作无法撤销。",
"generateSecret": "生成新密钥", "generateSecret": "生成新密钥",
"created_at": "创建于 {}" "createdAt": "创建于 {}",
"newSecretGenerated": "已生成新密钥",
"copySecretHint": "请复制此密钥并将其存放在安全的地方。您将无法再次看到它。",
"expiresIn": "过期时间(秒)",
"isOidc": "OIDC 兼容"
} }

View File

@@ -819,5 +819,9 @@
"deleteSecret": "刪除密鑰", "deleteSecret": "刪除密鑰",
"deleteSecretHint": "您確定要刪除此密鑰嗎?此操作無法復原。", "deleteSecretHint": "您確定要刪除此密鑰嗎?此操作無法復原。",
"generateSecret": "產生新密鑰", "generateSecret": "產生新密鑰",
"created_at": "建立於 {}" "createdAt": "建立於 {}",
"newSecretGenerated": "已產生新密鑰",
"copySecretHint": "請複製此密鑰並將其存放在安全的地方。您將無法再次看到它。",
"expiresIn": "過期時間(秒)",
"isOidc": "OIDC 相容"
} }

View File

@@ -7,9 +7,11 @@ part 'custom_app_secret.g.dart';
sealed class CustomAppSecret with _$CustomAppSecret { sealed class CustomAppSecret with _$CustomAppSecret {
const factory CustomAppSecret({ const factory CustomAppSecret({
required String id, required String id,
required String secret, required String? secret,
required DateTime createdAt, required DateTime createdAt,
String? description, String? description,
int? expiresIn,
bool? isOidc,
}) = _CustomAppSecret; }) = _CustomAppSecret;
factory CustomAppSecret.fromJson(Map<String, dynamic> json) => factory CustomAppSecret.fromJson(Map<String, dynamic> json) =>

View File

@@ -15,7 +15,7 @@ T _$identity<T>(T value) => value;
/// @nodoc /// @nodoc
mixin _$CustomAppSecret { mixin _$CustomAppSecret {
String get id; String get secret; DateTime get createdAt; String? get description; String get id; String? get secret; DateTime get createdAt; String? get description; int? get expiresIn; bool? get isOidc;
/// Create a copy of CustomAppSecret /// Create a copy of CustomAppSecret
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
@@ -28,16 +28,16 @@ $CustomAppSecretCopyWith<CustomAppSecret> get copyWith => _$CustomAppSecretCopyW
@override @override
bool operator ==(Object other) { bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is CustomAppSecret&&(identical(other.id, id) || other.id == id)&&(identical(other.secret, secret) || other.secret == secret)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.description, description) || other.description == description)); return identical(this, other) || (other.runtimeType == runtimeType&&other is CustomAppSecret&&(identical(other.id, id) || other.id == id)&&(identical(other.secret, secret) || other.secret == secret)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.description, description) || other.description == description)&&(identical(other.expiresIn, expiresIn) || other.expiresIn == expiresIn)&&(identical(other.isOidc, isOidc) || other.isOidc == isOidc));
} }
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
@override @override
int get hashCode => Object.hash(runtimeType,id,secret,createdAt,description); int get hashCode => Object.hash(runtimeType,id,secret,createdAt,description,expiresIn,isOidc);
@override @override
String toString() { String toString() {
return 'CustomAppSecret(id: $id, secret: $secret, createdAt: $createdAt, description: $description)'; return 'CustomAppSecret(id: $id, secret: $secret, createdAt: $createdAt, description: $description, expiresIn: $expiresIn, isOidc: $isOidc)';
} }
@@ -48,7 +48,7 @@ abstract mixin class $CustomAppSecretCopyWith<$Res> {
factory $CustomAppSecretCopyWith(CustomAppSecret value, $Res Function(CustomAppSecret) _then) = _$CustomAppSecretCopyWithImpl; factory $CustomAppSecretCopyWith(CustomAppSecret value, $Res Function(CustomAppSecret) _then) = _$CustomAppSecretCopyWithImpl;
@useResult @useResult
$Res call({ $Res call({
String id, String secret, DateTime createdAt, String? description String id, String? secret, DateTime createdAt, String? description, int? expiresIn, bool? isOidc
}); });
@@ -65,13 +65,15 @@ class _$CustomAppSecretCopyWithImpl<$Res>
/// Create a copy of CustomAppSecret /// Create a copy of CustomAppSecret
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? secret = null,Object? createdAt = null,Object? description = freezed,}) { @pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? secret = freezed,Object? createdAt = null,Object? description = freezed,Object? expiresIn = freezed,Object? isOidc = freezed,}) {
return _then(_self.copyWith( return _then(_self.copyWith(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,secret: null == secret ? _self.secret : secret // ignore: cast_nullable_to_non_nullable as String,secret: freezed == secret ? _self.secret : secret // ignore: cast_nullable_to_non_nullable
as String,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable as String?,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
as DateTime,description: freezed == description ? _self.description : description // ignore: cast_nullable_to_non_nullable as DateTime,description: freezed == description ? _self.description : description // ignore: cast_nullable_to_non_nullable
as String?, as String?,expiresIn: freezed == expiresIn ? _self.expiresIn : expiresIn // ignore: cast_nullable_to_non_nullable
as int?,isOidc: freezed == isOidc ? _self.isOidc : isOidc // ignore: cast_nullable_to_non_nullable
as bool?,
)); ));
} }
@@ -118,10 +120,7 @@ return $default(_that);case _:
final _that = this; final _that = this;
switch (_that) { switch (_that) {
case _CustomAppSecret(): case _CustomAppSecret():
return $default(_that);case _: return $default(_that);}
throw StateError('Unexpected subclass');
}
} }
/// A variant of `map` that fallback to returning `null`. /// A variant of `map` that fallback to returning `null`.
/// ///
@@ -156,10 +155,10 @@ return $default(_that);case _:
/// } /// }
/// ``` /// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id, String secret, DateTime createdAt, String? description)? $default,{required TResult orElse(),}) {final _that = this; @optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id, String? secret, DateTime createdAt, String? description, int? expiresIn, bool? isOidc)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) { switch (_that) {
case _CustomAppSecret() when $default != null: case _CustomAppSecret() when $default != null:
return $default(_that.id,_that.secret,_that.createdAt,_that.description);case _: return $default(_that.id,_that.secret,_that.createdAt,_that.description,_that.expiresIn,_that.isOidc);case _:
return orElse(); return orElse();
} }
@@ -177,13 +176,10 @@ return $default(_that.id,_that.secret,_that.createdAt,_that.description);case _:
/// } /// }
/// ``` /// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, String secret, DateTime createdAt, String? description) $default,) {final _that = this; @optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, String? secret, DateTime createdAt, String? description, int? expiresIn, bool? isOidc) $default,) {final _that = this;
switch (_that) { switch (_that) {
case _CustomAppSecret(): case _CustomAppSecret():
return $default(_that.id,_that.secret,_that.createdAt,_that.description);case _: return $default(_that.id,_that.secret,_that.createdAt,_that.description,_that.expiresIn,_that.isOidc);}
throw StateError('Unexpected subclass');
}
} }
/// A variant of `when` that fallback to returning `null` /// A variant of `when` that fallback to returning `null`
/// ///
@@ -197,10 +193,10 @@ return $default(_that.id,_that.secret,_that.createdAt,_that.description);case _:
/// } /// }
/// ``` /// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, String secret, DateTime createdAt, String? description)? $default,) {final _that = this; @optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, String? secret, DateTime createdAt, String? description, int? expiresIn, bool? isOidc)? $default,) {final _that = this;
switch (_that) { switch (_that) {
case _CustomAppSecret() when $default != null: case _CustomAppSecret() when $default != null:
return $default(_that.id,_that.secret,_that.createdAt,_that.description);case _: return $default(_that.id,_that.secret,_that.createdAt,_that.description,_that.expiresIn,_that.isOidc);case _:
return null; return null;
} }
@@ -212,13 +208,15 @@ return $default(_that.id,_that.secret,_that.createdAt,_that.description);case _:
@JsonSerializable() @JsonSerializable()
class _CustomAppSecret implements CustomAppSecret { class _CustomAppSecret implements CustomAppSecret {
const _CustomAppSecret({required this.id, required this.secret, required this.createdAt, this.description}); const _CustomAppSecret({required this.id, required this.secret, required this.createdAt, this.description, this.expiresIn, this.isOidc});
factory _CustomAppSecret.fromJson(Map<String, dynamic> json) => _$CustomAppSecretFromJson(json); factory _CustomAppSecret.fromJson(Map<String, dynamic> json) => _$CustomAppSecretFromJson(json);
@override final String id; @override final String id;
@override final String secret; @override final String? secret;
@override final DateTime createdAt; @override final DateTime createdAt;
@override final String? description; @override final String? description;
@override final int? expiresIn;
@override final bool? isOidc;
/// Create a copy of CustomAppSecret /// Create a copy of CustomAppSecret
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@@ -233,16 +231,16 @@ Map<String, dynamic> toJson() {
@override @override
bool operator ==(Object other) { bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _CustomAppSecret&&(identical(other.id, id) || other.id == id)&&(identical(other.secret, secret) || other.secret == secret)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.description, description) || other.description == description)); return identical(this, other) || (other.runtimeType == runtimeType&&other is _CustomAppSecret&&(identical(other.id, id) || other.id == id)&&(identical(other.secret, secret) || other.secret == secret)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.description, description) || other.description == description)&&(identical(other.expiresIn, expiresIn) || other.expiresIn == expiresIn)&&(identical(other.isOidc, isOidc) || other.isOidc == isOidc));
} }
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
@override @override
int get hashCode => Object.hash(runtimeType,id,secret,createdAt,description); int get hashCode => Object.hash(runtimeType,id,secret,createdAt,description,expiresIn,isOidc);
@override @override
String toString() { String toString() {
return 'CustomAppSecret(id: $id, secret: $secret, createdAt: $createdAt, description: $description)'; return 'CustomAppSecret(id: $id, secret: $secret, createdAt: $createdAt, description: $description, expiresIn: $expiresIn, isOidc: $isOidc)';
} }
@@ -253,7 +251,7 @@ abstract mixin class _$CustomAppSecretCopyWith<$Res> implements $CustomAppSecret
factory _$CustomAppSecretCopyWith(_CustomAppSecret value, $Res Function(_CustomAppSecret) _then) = __$CustomAppSecretCopyWithImpl; factory _$CustomAppSecretCopyWith(_CustomAppSecret value, $Res Function(_CustomAppSecret) _then) = __$CustomAppSecretCopyWithImpl;
@override @useResult @override @useResult
$Res call({ $Res call({
String id, String secret, DateTime createdAt, String? description String id, String? secret, DateTime createdAt, String? description, int? expiresIn, bool? isOidc
}); });
@@ -270,13 +268,15 @@ class __$CustomAppSecretCopyWithImpl<$Res>
/// Create a copy of CustomAppSecret /// Create a copy of CustomAppSecret
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? secret = null,Object? createdAt = null,Object? description = freezed,}) { @override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? secret = freezed,Object? createdAt = null,Object? description = freezed,Object? expiresIn = freezed,Object? isOidc = freezed,}) {
return _then(_CustomAppSecret( return _then(_CustomAppSecret(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,secret: null == secret ? _self.secret : secret // ignore: cast_nullable_to_non_nullable as String,secret: freezed == secret ? _self.secret : secret // ignore: cast_nullable_to_non_nullable
as String,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable as String?,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
as DateTime,description: freezed == description ? _self.description : description // ignore: cast_nullable_to_non_nullable as DateTime,description: freezed == description ? _self.description : description // ignore: cast_nullable_to_non_nullable
as String?, as String?,expiresIn: freezed == expiresIn ? _self.expiresIn : expiresIn // ignore: cast_nullable_to_non_nullable
as int?,isOidc: freezed == isOidc ? _self.isOidc : isOidc // ignore: cast_nullable_to_non_nullable
as bool?,
)); ));
} }

View File

@@ -9,9 +9,11 @@ part of 'custom_app_secret.dart';
_CustomAppSecret _$CustomAppSecretFromJson(Map<String, dynamic> json) => _CustomAppSecret _$CustomAppSecretFromJson(Map<String, dynamic> json) =>
_CustomAppSecret( _CustomAppSecret(
id: json['id'] as String, id: json['id'] as String,
secret: json['secret'] as String, secret: json['secret'] as String?,
createdAt: DateTime.parse(json['created_at'] as String), createdAt: DateTime.parse(json['created_at'] as String),
description: json['description'] as String?, description: json['description'] as String?,
expiresIn: (json['expires_in'] as num?)?.toInt(),
isOidc: json['is_oidc'] as bool?,
); );
Map<String, dynamic> _$CustomAppSecretToJson(_CustomAppSecret instance) => Map<String, dynamic> _$CustomAppSecretToJson(_CustomAppSecret instance) =>
@@ -20,4 +22,6 @@ Map<String, dynamic> _$CustomAppSecretToJson(_CustomAppSecret instance) =>
'secret': instance.secret, 'secret': instance.secret,
'created_at': instance.createdAt.toIso8601String(), 'created_at': instance.createdAt.toIso8601String(),
'description': instance.description, 'description': instance.description,
'expires_in': instance.expiresIn,
'is_oidc': instance.isOidc,
}; };

View File

@@ -1,10 +1,12 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/custom_app_secret.dart'; import 'package:island/models/custom_app_secret.dart';
import 'package:island/pods/network.dart'; import 'package:island/pods/network.dart';
import 'package:island/widgets/alert.dart'; import 'package:island/widgets/alert.dart';
import 'package:island/widgets/content/sheet.dart';
import 'package:island/widgets/response.dart'; import 'package:island/widgets/response.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
@@ -46,24 +48,127 @@ class AppSecretsScreen extends HookConsumerWidget {
customAppSecretsProvider(publisherName, projectId, appId), customAppSecretsProvider(publisherName, projectId, appId),
); );
Future<void> generateSecret() async { void showNewSecretSheet(String newSecret) {
final client = ref.read(apiClientProvider); showModalBottomSheet(
try { context: context,
showLoadingModal(context); isScrollControlled: true,
await client builder:
.post( (context) => SheetScaffold(
'/develop/developers/$publisherName/projects/$projectId/apps/$appId/secrets', titleText: 'newSecretGenerated'.tr(),
) child: Padding(
.then((_) { padding: const EdgeInsets.all(20.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: MainAxisSize.min,
children: [
Text('copySecretHint'.tr()),
const SizedBox(height: 16),
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainer,
borderRadius: BorderRadius.circular(8),
),
child: SelectableText(newSecret),
),
const SizedBox(height: 20),
FilledButton.icon(
onPressed: () {
Clipboard.setData(ClipboardData(text: newSecret));
},
icon: const Icon(Symbols.copy_all),
label: Text('copy'.tr()),
),
],
),
),
),
).whenComplete(() {
ref.invalidate( ref.invalidate(
customAppSecretsProvider(publisherName, projectId, appId), customAppSecretsProvider(publisherName, projectId, appId),
); );
}); });
} catch (err) {
showErrorAlert(err);
} finally {
if (context.mounted) hideLoadingModal(context);
} }
void createSecret() {
showModalBottomSheet(
context: context,
isScrollControlled: true,
builder: (context) {
return HookBuilder(
builder: (context) {
final descriptionController = useTextEditingController();
final expiresInController = useTextEditingController();
final isOidc = useState(false);
return SheetScaffold(
titleText: 'generateSecret'.tr(),
child: Padding(
padding: const EdgeInsets.all(20.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: MainAxisSize.min,
children: [
TextFormField(
controller: descriptionController,
decoration: InputDecoration(
labelText: 'description'.tr(),
),
autofocus: true,
),
const SizedBox(height: 20),
TextFormField(
controller: expiresInController,
decoration: InputDecoration(
labelText: 'expiresIn'.tr(),
),
keyboardType: TextInputType.number,
),
const SizedBox(height: 20),
SwitchListTile(
title: Text('isOidc'.tr()),
value: isOidc.value,
onChanged: (value) => isOidc.value = value,
),
const SizedBox(height: 20),
FilledButton.icon(
onPressed: () async {
final description = descriptionController.text;
final expiresIn = int.tryParse(
expiresInController.text,
);
Navigator.pop(context); // Close the sheet
try {
final client = ref.read(apiClientProvider);
final resp = await client.post(
'/develop/developers/$publisherName/projects/$projectId/apps/$appId/secrets',
data: {
'description': description,
'expires_in': expiresIn,
'is_oidc': isOidc.value,
},
);
final newSecret = CustomAppSecret.fromJson(
resp.data,
);
if (newSecret.secret != null) {
showNewSecretSheet(newSecret.secret!);
}
} catch (e) {
showErrorAlert(e.toString());
}
},
icon: const Icon(Symbols.add),
label: Text('create'.tr()),
),
],
),
),
);
},
);
},
);
} }
return secrets.when( return secrets.when(
@@ -81,9 +186,8 @@ class AppSecretsScreen extends HookConsumerWidget {
children: [ children: [
ListTile( ListTile(
leading: const Icon(Symbols.add), leading: const Icon(Symbols.add),
trailing: const Icon(Symbols.chevron_right), title: Text('generateSecret'.tr()),
title: Text('appSecretsGenerate').tr(), onTap: createSecret,
onTap: generateSecret,
), ),
Expanded( Expanded(
child: ListView.builder( child: ListView.builder(
@@ -91,9 +195,9 @@ class AppSecretsScreen extends HookConsumerWidget {
itemBuilder: (context, index) { itemBuilder: (context, index) {
final secret = data[index]; final secret = data[index];
return ListTile( return ListTile(
title: Text(secret.id), title: Text(secret.description ?? secret.id),
subtitle: Text( subtitle: Text(
'created_at'.tr( 'createdAt'.tr(
args: [secret.createdAt.toIso8601String()], args: [secret.createdAt.toIso8601String()],
), ),
), ),
@@ -104,7 +208,7 @@ class AppSecretsScreen extends HookConsumerWidget {
icon: const Icon(Symbols.copy_all), icon: const Icon(Symbols.copy_all),
onPressed: () { onPressed: () {
Clipboard.setData( Clipboard.setData(
ClipboardData(text: secret.secret), ClipboardData(text: secret.secret!),
); );
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('secretCopied'.tr())), SnackBar(content: Text('secretCopied'.tr())),
@@ -120,11 +224,9 @@ class AppSecretsScreen extends HookConsumerWidget {
).then((confirm) { ).then((confirm) {
if (confirm) { if (confirm) {
final client = ref.read(apiClientProvider); final client = ref.read(apiClientProvider);
client client.delete(
.delete(
'/develop/developers/$publisherName/projects/$projectId/apps/$appId/secrets/${secret.id}', '/develop/developers/$publisherName/projects/$projectId/apps/$appId/secrets/${secret.id}',
) );
.then((_) {
ref.invalidate( ref.invalidate(
customAppSecretsProvider( customAppSecretsProvider(
publisherName, publisherName,
@@ -132,7 +234,6 @@ class AppSecretsScreen extends HookConsumerWidget {
appId, appId,
), ),
); );
});
} }
}); });
}, },