Skip to content

Commit 6931cc6

Browse files
committed
Add user service
1 parent 7ae869f commit 6931cc6

File tree

14 files changed

+276
-33
lines changed

14 files changed

+276
-33
lines changed

api/lib/src/event/process/client.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -304,7 +304,7 @@ Future<ServerResponse?> processClientEvent(
304304
return ServerResponse.builder(
305305
AuthenticatedRequested(challenge, isRequired: true), channel);
306306
}
307-
userManager?.addUser(channel, event.publicKey);
307+
userManager?.addUser(channel, generateFingerprint(event.publicKey));
308308
return ServerResponse.builder(buildInitialize(), channel);
309309
}
310310
}

api/lib/src/helpers/crypto.dart

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,16 @@ import 'dart:typed_data';
33
import 'package:convert/convert.dart';
44
import 'package:crypto/crypto.dart';
55

6-
String generateFingerprint(Uint8List publicKeyBytes, [bool short = false]) {
6+
String generateFingerprint(Uint8List publicKeyBytes,
7+
{bool short = false, bool pretty = false}) {
78
final digest = sha256.convert(publicKeyBytes);
89
var hexString = hex.encode(digest.bytes);
910
if (short) {
1011
hexString = hexString.substring(0, 32);
1112
}
13+
if (!pretty) {
14+
return hexString;
15+
}
1216
final output = hexString.replaceAllMapped(
1317
RegExp(r'.{2}'),
1418
(match) => '${match.group(0)!}:',

api/lib/src/models/meta.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,6 @@ final class SetonixAccount {
8888
type: KeyPairType.ed25519,
8989
);
9090

91-
String getFingerprint([bool short = false]) =>
92-
generateFingerprint(publicKey, short);
91+
String getFingerprint({bool short = false, bool pretty = false}) =>
92+
generateFingerprint(publicKey, short: short, pretty: pretty);
9393
}

api/lib/src/services/user.dart

Lines changed: 57 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import 'dart:async';
12
import 'dart:typed_data';
23

34
import 'package:dart_mappable/dart_mappable.dart';
@@ -8,21 +9,42 @@ part 'user.mapper.dart';
89

910
@MappableClass(includeCustomMappers: [Base64Uint8ListHook()])
1011
final class SetonixUser with SetonixUserMappable {
11-
final Uint8List? publicKey;
12+
final String? fingerprint;
1213
final String name;
14+
final bool onWhitelist;
15+
final DateTime? createdAt, updatedAt, lastLogin;
1316

1417
const SetonixUser({
15-
this.publicKey,
18+
this.fingerprint,
1619
required this.name,
20+
this.onWhitelist = false,
21+
this.createdAt,
22+
this.updatedAt,
23+
this.lastLogin,
24+
});
25+
}
26+
27+
abstract class UserService {
28+
FutureOr<SetonixUser?> getUser(String fingerprint);
29+
FutureOr<SetonixUser?> getUserFromName(String name);
30+
FutureOr<bool> updateUser(
31+
String fingerprint, {
32+
String? name,
33+
bool? onWhitelist,
34+
DateTime? lastLogin,
1735
});
1836
}
1937

2038
final class UserManager {
2139
final Map<Channel, SetonixUser> _users = {};
2240
final String guestPrefix;
41+
final UserService? service;
2342
int _nextGuestId = 1;
2443

25-
UserManager([this.guestPrefix = SetonixConfig.defaultGuestPrefix]);
44+
UserManager({
45+
this.service,
46+
this.guestPrefix = SetonixConfig.defaultGuestPrefix,
47+
});
2648

2749
bool containsUserName(String name) =>
2850
_users.values.any((u) => u.name == name);
@@ -53,12 +75,42 @@ final class UserManager {
5375
return name;
5476
}
5577

56-
bool addUser(Channel channel, [Uint8List? publicKey, String? name]) {
78+
Future<bool> addUser(Channel channel,
79+
[String? fingerprint, String? name]) async {
80+
SetonixUser? user;
81+
if (fingerprint != null) user = await service?.getUser(fingerprint);
5782
name ??= _generateGuestName();
5883
if (containsUserName(name)) {
5984
return false;
6085
}
61-
_users[channel] = SetonixUser(publicKey: publicKey, name: name);
86+
if (user == null) {
87+
user = SetonixUser(
88+
fingerprint: fingerprint,
89+
name: name,
90+
);
91+
if (fingerprint != null) {
92+
await service?.updateUser(fingerprint, name: name, onWhitelist: false);
93+
}
94+
}
95+
_users[channel] = user;
96+
return true;
97+
}
98+
99+
Future<bool> changeName(Channel channel, String newName) async {
100+
if (containsUserName(newName)) {
101+
return false;
102+
}
103+
final user = _users[channel];
104+
if (user == null) {
105+
return false;
106+
}
107+
final fingerprint = user.fingerprint;
108+
final result = fingerprint == null
109+
? null
110+
: await service?.updateUser(fingerprint, name: newName);
111+
if (result == false) return false;
112+
final updatedUser = user.copyWith(name: newName);
113+
_users[channel] = updatedUser;
62114
return true;
63115
}
64116
}

api/lib/src/services/user.mapper.dart

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,21 +21,21 @@ class SetonixUserMapper extends ClassMapperBase<SetonixUser> {
2121
@override
2222
final String id = 'SetonixUser';
2323

24-
static Uint8List? _$publicKey(SetonixUser v) => v.publicKey;
25-
static const Field<SetonixUser, Uint8List> _f$publicKey =
26-
Field('publicKey', _$publicKey, opt: true);
24+
static String? _$fingerprint(SetonixUser v) => v.fingerprint;
25+
static const Field<SetonixUser, Uint8List> _f$fingerprint =
26+
Field('fingerprint', _$fingerprint, opt: true);
2727
static String _$name(SetonixUser v) => v.name;
2828
static const Field<SetonixUser, String> _f$name = Field('name', _$name);
2929

3030
@override
3131
final MappableFields<SetonixUser> fields = const {
32-
#publicKey: _f$publicKey,
32+
#fingerprint: _f$fingerprint,
3333
#name: _f$name,
3434
};
3535

3636
static SetonixUser _instantiate(DecodingData data) {
3737
return SetonixUser(
38-
publicKey: data.dec(_f$publicKey), name: data.dec(_f$name));
38+
fingerprint: data.dec(_f$fingerprint), name: data.dec(_f$name));
3939
}
4040

4141
@override
@@ -90,7 +90,7 @@ extension SetonixUserValueCopy<$R, $Out>
9090

9191
abstract class SetonixUserCopyWith<$R, $In extends SetonixUser, $Out>
9292
implements ClassCopyWith<$R, $In, $Out> {
93-
$R call({Uint8List? publicKey, String? name});
93+
$R call({Uint8List? fingerprint, String? name});
9494
SetonixUserCopyWith<$R2, $In, $Out2> $chain<$R2, $Out2>(Then<$Out2, $R2> t);
9595
}
9696

@@ -103,14 +103,14 @@ class _SetonixUserCopyWithImpl<$R, $Out>
103103
late final ClassMapperBase<SetonixUser> $mapper =
104104
SetonixUserMapper.ensureInitialized();
105105
@override
106-
$R call({Object? publicKey = $none, String? name}) =>
106+
$R call({Object? fingerprint = $none, String? name}) =>
107107
$apply(FieldCopyWithData({
108-
if (publicKey != $none) #publicKey: publicKey,
108+
if (fingerprint != $none) #fingerprint: fingerprint,
109109
if (name != null) #name: name
110110
}));
111111
@override
112112
SetonixUser $make(CopyWithData data) => SetonixUser(
113-
publicKey: data.get(#publicKey, or: $value.publicKey),
113+
fingerprint: data.get(#fingerprint, or: $value.fingerprint),
114114
name: data.get(#name, or: $value.name));
115115

116116
@override

app/lib/pages/game/auth.dart

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -135,8 +135,10 @@ class _AuthGameViewState extends State<AuthGameView> {
135135
return ListTile(
136136
title: Text(
137137
account.name.substring(1)),
138-
subtitle: Text(account
139-
.getFingerprint(true)),
138+
subtitle: Text(
139+
account.getFingerprint(
140+
pretty: true,
141+
short: true)),
140142
onTap: () async {
141143
final bloc =
142144
context.read<WorldBloc>();

app/lib/pages/settings/accounts.dart

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,8 @@ class _AccountsSettingsPageState extends State<AccountsSettingsPage> {
9898
itemBuilder: (context, index) {
9999
final account = accounts[index];
100100
final key = account.name;
101-
final fingerprint = account.getFingerprint(true);
101+
final fingerprint =
102+
account.getFingerprint(pretty: true, short: true);
102103
void deleteKey() {
103104
_privateKeyFileSystem.deleteFile(key);
104105
_publicKeyFileSystem.deleteFile(key);

app/lib/services/file_system.dart

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -300,11 +300,12 @@ class SetonixFileSystem {
300300
return data;
301301
}
302302

303-
Future<String> getFingerprint(String key, [bool short = false]) async {
303+
Future<String> getFingerprint(String key,
304+
{bool short = false, bool pretty = false}) async {
304305
final publicKey = await publicKeySystem.getFile(key);
305306
if (publicKey == null) {
306307
return '';
307308
}
308-
return generateFingerprint(publicKey, short);
309+
return generateFingerprint(publicKey, short: short, pretty: pretty);
309310
}
310311
}

server/lib/src/server.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ final class SetonixServer {
9090
await _runStaticLogZone(
9191
consoler, () => assetManager.init(console: consoler));
9292
final configManager = ConfigManager();
93-
final userManager = UserManager(configManager.guestPrefix);
93+
final userManager = UserManager(guestPrefix: configManager.guestPrefix);
9494
final challengeManager = ChallengeManager();
9595
return SetonixServer._(
9696
consoler, assetManager, configManager, userManager, challengeManager);
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import 'dart:async';
2+
3+
import 'package:setonix_api/setonix_api.dart';
4+
import 'package:setonix_server/src/services/user/migrations.dart';
5+
import 'package:sqlite3/sqlite3.dart';
6+
7+
final class FileUserService extends UserService {
8+
Database? _database;
9+
10+
Future<void> setup() async {
11+
final database = sqlite3.open('setonix.db');
12+
// Basic migration system
13+
14+
final result = database.select('PRAGMA user_version');
15+
final currentVersion =
16+
result.isNotEmpty ? result.first['user_version'] as int : 0;
17+
18+
for (var version = currentVersion + 1;
19+
version <= migrations.length;
20+
version++) {
21+
database.execute(migrations[version]!);
22+
database.execute('PRAGMA user_version = $version;');
23+
}
24+
_database = database;
25+
}
26+
27+
SetonixUser _fromRow(Row row) {
28+
return SetonixUser(
29+
fingerprint: row['fingerprint'] as String?,
30+
name: row['name'] as String,
31+
onWhitelist: row['on_whitelist'] == 1,
32+
createdAt: row['created_at'] != null
33+
? DateTime.parse(row['created_at'] as String)
34+
: null,
35+
updatedAt: row['updated_at'] != null
36+
? DateTime.parse(row['updated_at'] as String)
37+
: null,
38+
lastLogin: row['last_login'] != null
39+
? DateTime.parse(row['last_login'] as String)
40+
: null,
41+
);
42+
}
43+
44+
@override
45+
SetonixUser? getUser(String fingerprint) => _database
46+
?.select(
47+
'SELECT * FROM users WHERE fingerprint = ?',
48+
[fingerprint],
49+
)
50+
.map(_fromRow)
51+
.firstOrNull;
52+
53+
@override
54+
SetonixUser? getUserFromName(String name) => _database
55+
?.select(
56+
'SELECT * FROM users WHERE name = ?',
57+
[name],
58+
)
59+
.map(_fromRow)
60+
.firstOrNull;
61+
62+
@override
63+
bool updateUser(String fingerprint,
64+
{String? name, bool? onWhitelist, DateTime? lastLogin}) {
65+
final updates = <String>[];
66+
final values = <dynamic>[];
67+
68+
if (name != null) {
69+
updates.add('name = ?');
70+
values.add(name);
71+
}
72+
if (onWhitelist != null) {
73+
updates.add('on_whitelist = ?');
74+
values.add(onWhitelist ? 1 : 0);
75+
}
76+
if (lastLogin != null) {
77+
updates.add('last_login = ?');
78+
values.add(lastLogin.toIso8601String());
79+
}
80+
81+
if (updates.isEmpty) return false;
82+
83+
values.add(fingerprint);
84+
_database?.execute(
85+
'UPDATE users SET ${updates.join(', ')} WHERE fingerprint = ?',
86+
values,
87+
);
88+
return _database?.updatedRows == 1;
89+
}
90+
}

0 commit comments

Comments
 (0)