mirror of
https://codeberg.org/shroff/phylum.git
synced 2026-01-26 22:19:32 -06:00
[client] Show last used info
This commit is contained in:
@@ -6,6 +6,7 @@ class ApiKey {
|
||||
final DateTime? expires;
|
||||
final String description;
|
||||
final List<String> scopes;
|
||||
final LastUsedInfo lastUsed;
|
||||
|
||||
ApiKey({
|
||||
required this.id,
|
||||
@@ -13,9 +14,18 @@ class ApiKey {
|
||||
required this.expires,
|
||||
required this.description,
|
||||
required this.scopes,
|
||||
required this.lastUsed,
|
||||
});
|
||||
}
|
||||
|
||||
class LastUsedInfo {
|
||||
final int timestamp;
|
||||
final String ip;
|
||||
final String device;
|
||||
|
||||
LastUsedInfo(this.timestamp, this.ip, this.device);
|
||||
}
|
||||
|
||||
class ListApiKeysResponse extends PhylumApiSuccessResponse {
|
||||
final List<ApiKey> keys;
|
||||
|
||||
@@ -30,11 +40,16 @@ class ListApiKeysResponse extends PhylumApiSuccessResponse {
|
||||
expires: k['expires'] == 0 ? null : DateTime.fromMillisecondsSinceEpoch(k['expires'] * 1000),
|
||||
description: k['description'],
|
||||
scopes: (k['scopes'] as List).cast<String>(),
|
||||
lastUsed: _parseLastUsed((k['last_used'] as Map).cast<String, dynamic>()),
|
||||
);
|
||||
}).toList(growable: false);
|
||||
return ListApiKeysResponse._(keys: keys);
|
||||
}
|
||||
|
||||
static LastUsedInfo _parseLastUsed(Map<String, dynamic> data) {
|
||||
return LastUsedInfo(data['timestamp'], data['ip'], data['device']);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> process(PhylumAccount account) => SynchronousFuture(null);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:moment_dart/moment_dart.dart';
|
||||
import 'package:phylum/libphylum/phylum_account.dart';
|
||||
import 'package:phylum/libphylum/requests/api_key_generate_request.dart';
|
||||
import 'package:phylum/libphylum/requests/api_keys_list_request.dart';
|
||||
import 'package:phylum/libphylum/responses/responses.dart';
|
||||
import 'package:phylum/ui/profile/generate_api_key_dialog.dart';
|
||||
import 'package:phylum/util/dialogs.dart';
|
||||
import 'package:phylum/util/time.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class ApiKeysView extends StatefulWidget {
|
||||
@@ -35,35 +35,33 @@ class _ApiKeysViewState extends State<ApiKeysView> {
|
||||
);
|
||||
}
|
||||
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
ElevatedButton.icon(
|
||||
onPressed: () => createKey(context),
|
||||
label: Text('Generate New API Key'),
|
||||
icon: Icon(Icons.add),
|
||||
),
|
||||
for (final k in keys)
|
||||
Card(
|
||||
margin: EdgeInsets.all(2.0),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(6.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
k.description,
|
||||
style: const TextStyle(fontSize: 18),
|
||||
),
|
||||
Text('Created ${k.created.formatLong()}', style: const TextStyle(fontSize: 15)),
|
||||
Text('Expires ${k.expires?.formatLong() ?? 'Never'}', style: const TextStyle(fontSize: 15)),
|
||||
Text('Scopes: ${k.scopes.join(', ')}', style: const TextStyle(fontSize: 15)),
|
||||
],
|
||||
),
|
||||
return ListTileTheme(
|
||||
data: ListTileThemeData(
|
||||
tileColor: Theme.of(context).colorScheme.surfaceBright,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 16),
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: () => createKey(context),
|
||||
label: Text('Generate New API Key'),
|
||||
icon: Icon(Icons.add),
|
||||
),
|
||||
),
|
||||
],
|
||||
...ListTile.divideTiles(context: context, tiles: [
|
||||
for (final k in keys)
|
||||
ListTile(
|
||||
title: Text(k.description),
|
||||
subtitle: k.lastUsed.used
|
||||
? Text('${k.lastUsed.device} \u2022 ${k.lastUsed.fromNow()}')
|
||||
: const Text('Never Used'),
|
||||
),
|
||||
]),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -75,6 +73,12 @@ class _ApiKeysViewState extends State<ApiKeysView> {
|
||||
);
|
||||
|
||||
if (response is ListApiKeysResponse) {
|
||||
response.keys.sort((a, b) {
|
||||
if (a.lastUsed.timestamp == b.lastUsed.timestamp) {
|
||||
b.created.compareTo(a.created);
|
||||
}
|
||||
return b.lastUsed.timestamp - a.lastUsed.timestamp;
|
||||
});
|
||||
setState(() => keys = response.keys);
|
||||
} else {
|
||||
setState(() => error = response.description);
|
||||
@@ -147,3 +151,14 @@ class _ApiKeysViewState extends State<ApiKeysView> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension on LastUsedInfo {
|
||||
String fromNow() {
|
||||
if (timestamp == 0) {
|
||||
return "Never";
|
||||
}
|
||||
return Moment.fromMillisecondsSinceEpoch(timestamp * 1000).fromNow();
|
||||
}
|
||||
|
||||
bool get used => timestamp != 0;
|
||||
}
|
||||
|
||||
@@ -698,6 +698,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
moment_dart:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: moment_dart
|
||||
sha256: "74e4241d412b87aac99fc2eaaf948e682c9c876b31850b82e35bdf2606fdf178"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.2.3"
|
||||
nested:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
@@ -28,6 +28,7 @@ dependencies:
|
||||
http_parser:
|
||||
intl:
|
||||
logger:
|
||||
moment_dart:
|
||||
mime:
|
||||
offtheline:
|
||||
git:
|
||||
|
||||
Reference in New Issue
Block a user