Renamed backend into auth_backend and added server_browser_backend implementation to replace Supabase.

This commit is contained in:
Alessandro Autiero
2025-08-09 02:54:48 +01:00
parent 9c6cd6dd37
commit 52abf5eb95
96 changed files with 678 additions and 3 deletions

3
server_browser_backend/.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
# https://dart.dev/guides/libraries/private-files
# Created by `dart pub`
.dart_tool/

View File

@@ -0,0 +1,2 @@
A sample command-line application with an entrypoint in `bin/`, library code
in `lib/`, and example unit test in `test/`.

View File

@@ -0,0 +1,30 @@
# This file configures the static analysis results for your project (errors,
# warnings, and lints).
#
# This enables the 'recommended' set of lints from `package:lints`.
# This set helps identify many issues that may lead to problems when running
# or consuming Dart code, and enforces writing Dart using a single, idiomatic
# style and format.
#
# If you want a smaller set of lints you can change this to specify
# 'package:lints/core.yaml'. These are just the most critical lints
# (the recommended set includes the core lints).
# The core lints are also what is used by pub.dev for scoring packages.
include: package:lints/recommended.yaml
# Uncomment the following section to specify additional rules.
# linter:
# rules:
# - camel_case_types
# analyzer:
# exclude:
# - path/to/excluded/files/**
# For more information about the core and recommended set of lints, see
# https://dart.dev/go/core-lints
# For additional information about configuring this file, see
# https://dart.dev/guides/language/analysis-options

View File

@@ -0,0 +1,6 @@
import 'package:server_browser_backend/server_browser_backend.dart';
void main() async {
final server = WebSocketServer();
await server.start(port: 8080);
}

View File

@@ -0,0 +1,4 @@
library;
export 'src/server_entry.dart';
export 'src/web_socket.dart';

View File

@@ -0,0 +1,51 @@
class ServerEntry {
final String id;
final String name;
final String description;
final String version;
final String password;
final DateTime timestamp;
final String ip;
final String author;
final bool discoverable;
ServerEntry({
required this.id,
required this.name,
required this.description,
required this.version,
required this.password,
required this.timestamp,
required this.ip,
required this.author,
required this.discoverable,
});
Map<String, dynamic> toJson() {
return {
'id': id,
'name': name,
'description': description,
'version': version,
'password': password,
'timestamp': timestamp.toIso8601String(),
'ip': ip,
'author': author,
'discoverable': discoverable,
};
}
static ServerEntry fromJson(Map<String, dynamic> json) {
return ServerEntry(
id: json['id'],
name: json['name'],
description: json['description'],
version: json['version'],
password: json['password'],
timestamp: DateTime.parse(json['timestamp']),
ip: json['ip'],
author: json['author'],
discoverable: json['discoverable'],
);
}
}

View File

@@ -0,0 +1,126 @@
import 'dart:convert';
import 'dart:io';
import 'package:server_browser_backend/src/server_entry.dart';
class WebSocketServer {
static const String addEvent = 'add';
static const String removeEvent = 'remove';
final Map<String, ServerEntry> _entries = {};
final Set<WebSocket> _clients = {};
late HttpServer _server;
Future<void> start({int port = 8080}) async {
_server = await HttpServer.bind('0.0.0.0', port);
_listen();
}
Future<void> _listen() async {
await for (HttpRequest request in _server) {
if (WebSocketTransformer.isUpgradeRequest(request)) {
_handleWebSocketUpgrade(request);
} else {
request.response.statusCode = 404;
request.response.close();
}
}
}
Future<void> _handleWebSocketUpgrade(HttpRequest request) async {
final client = await WebSocketTransformer.upgrade(request);
_clients.add(client);
await _sendAllEntriesToClient(client);
client.listen(
(message) => _handleMessage(client, message),
onDone: () => _removeClient(client),
onError: (error) => _removeClient(client)
);
}
Future<void> _sendAllEntriesToClient(WebSocket client) async {
final message = {
'type': addEvent,
'data': _entries.values.map((entry) => entry.toJson()).toList()
};
client.add(json.encode(message));
}
void _handleMessage(WebSocket client, dynamic message) {
String? type;
try {
final data = jsonDecode(message);
type = data['type'];
final payload = data['data'];
switch (type) {
case addEvent:
final entry = ServerEntry.fromJson(payload);
_entries[entry.id] = entry;
_broadcastEvent(addEvent, entry.toJson());
break;
case removeEvent:
final deletedEntry = _entries.remove(payload);
if (deletedEntry != null) {
_broadcastEvent(removeEvent, {'id': deletedEntry.id});
}else {
_answer(client, removeEvent, "Invalid server entry");
}
break;
default:
_answer(client, type, 'Unknown type');
break;
}
} catch(error) {
_answer(client, type, error.toString());
}
}
void _broadcastEvent(String eventType, Map<String, dynamic> eventData) {
final message = {
'type': eventType,
'data': [
eventData
]
};
final messageJson = json.encode(message);
final clientsToRemove = <WebSocket>[];
for (final client in _clients) {
try {
client.add(messageJson);
} catch (e) {
clientsToRemove.add(client);
}
}
for (final client in clientsToRemove) {
_removeClient(client);
}
}
void _removeClient(WebSocket client) {
client.close();
_clients.remove(client);
}
void _answer(WebSocket client, String? eventType, [String? error]) {
final message = {};
if(eventType != null) {
message['type'] = eventType;
}
if(error != null) {
message['success'] = false;
message['message'] = error;
}else {
message['success'] = true;
}
client.add(json.encode(message));
}
Future<void> stop() async {
await _server.close(force: true);
_clients.clear();
}
}

View File

@@ -0,0 +1,15 @@
name: server_browser_backend
description: A sample command-line application.
version: 1.0.0
# repository: https://github.com/my_org/my_repo
environment:
sdk: ^3.8.1
# Add regular dependencies here.
dependencies:
# path: ^1.8.0
dev_dependencies:
lints: ^5.0.0
test: ^1.24.0

View File

@@ -0,0 +1,437 @@
import 'dart:convert';
import 'dart:io';
import 'package:server_browser_backend/server_browser_backend.dart';
import 'package:test/test.dart';
void main() {
group('WebSocket Server Tests', () {
late WebSocketServer server;
final int testPort = 8081;
setUp(() async {
server = WebSocketServer();
await server.start(port: testPort);
});
tearDown(() async {
await server.stop();
});
test('should allow client connection and receive initial empty data', () async {
final client = await WebSocket.connect('ws://localhost:$testPort');
final messagesFuture = client.toList();
await client.close();
final messages = await messagesFuture;
expect(messages.length, equals(1));
final firstMessage = jsonDecode(messages[0]);
expect(firstMessage['type'], equals('add'));
expect(firstMessage['data'], equals([]));
});
test('should add server entry and broadcast to all clients', () async {
final client1 = await WebSocket.connect('ws://localhost:$testPort');
final client1MessagesFuture = client1.toList();
final client2 = await WebSocket.connect('ws://localhost:$testPort');
final client2MessagesFuture = client2.toList();
final testEntry = {
'id': 'test-server-1',
'name': 'Test Server',
'description': 'A test server',
'version': '1.0.0',
'password': 'secret123',
'timestamp': DateTime.now().toIso8601String(),
'ip': '127.0.0.1',
'author': 'Test Author',
'discoverable': true,
};
final addMessage = {
'type': 'add',
'data': testEntry,
};
client1.add(jsonEncode(addMessage));
await client1.close();
await client2.close();
final client1Messages = await client1MessagesFuture;
final client2Messages = await client2MessagesFuture;
expect(client1Messages.length, equals(2));
expect(client2Messages.length, equals(2));
final client1Initial = jsonDecode(client1Messages[0]);
final client2Initial = jsonDecode(client2Messages[0]);
expect(client1Initial['type'], equals('add'));
expect(client1Initial['data'], equals([]));
expect(client2Initial['type'], equals('add'));
expect(client2Initial['data'], equals([]));
final client1Broadcast = jsonDecode(client1Messages[1]);
final client2Broadcast = jsonDecode(client2Messages[1]);
expect(client1Broadcast['type'], equals('add'));
expect(client1Broadcast['data'], isA<List>());
expect(client1Broadcast['data'].length, equals(1));
expect(client1Broadcast['data'][0]['id'], equals('test-server-1'));
expect(client2Broadcast['type'], equals('add'));
expect(client2Broadcast['data'], isA<List>());
expect(client2Broadcast['data'].length, equals(1));
expect(client2Broadcast['data'][0]['id'], equals('test-server-1'));
});
test('should send existing entries to new client', () async {
final client1 = await WebSocket.connect('ws://localhost:$testPort');
final client1MessagesFuture = client1.toList();
final testEntry = {
'id': 'existing-server',
'name': 'Existing Server',
'description': 'Already exists',
'version': '2.0.0',
'password': 'existing123',
'timestamp': DateTime.now().toIso8601String(),
'ip': '192.168.1.1',
'author': 'Existing Author',
'discoverable': false,
};
final addMessage = {
'type': 'add',
'data': testEntry,
};
client1.add(jsonEncode(addMessage));
final client2 = await WebSocket.connect('ws://localhost:$testPort');
final client2MessagesFuture = client2.toList();
await client1.close();
await client2.close();
final client1Messages = await client1MessagesFuture;
final client2Messages = await client2MessagesFuture;
expect(client1Messages.length, equals(2));
expect(client2Messages.length, equals(1));
final client2InitialMessage = jsonDecode(client2Messages[0]);
expect(client2InitialMessage['type'], equals('add'));
expect(client2InitialMessage['data'], isA<List>());
expect(client2InitialMessage['data'].length, equals(1));
expect(client2InitialMessage['data'][0]['id'], equals('existing-server'));
});
test('should remove server entry and broadcast removal', () async {
final client1 = await WebSocket.connect('ws://localhost:$testPort');
final client1MessagesFuture = client1.toList();
final client2 = await WebSocket.connect('ws://localhost:$testPort');
final client2MessagesFuture = client2.toList();
final testEntry = {
'id': 'server-to-remove',
'name': 'Server To Remove',
'description': 'Will be removed',
'version': '1.0.0',
'password': 'remove123',
'timestamp': DateTime.now().toIso8601String(),
'ip': '10.0.0.1',
'author': 'Remove Author',
'discoverable': true,
};
final addMessage = {
'type': 'add',
'data': testEntry,
};
client1.add(jsonEncode(addMessage));
final removeMessage = {
'type': 'remove',
'data': 'server-to-remove',
};
client1.add(jsonEncode(removeMessage));
await client1.close();
await client2.close();
final client1Messages = await client1MessagesFuture;
final client2Messages = await client2MessagesFuture;
expect(client1Messages.length, equals(3));
expect(client2Messages.length, equals(3));
final client1RemoveResponse = jsonDecode(client1Messages[2]);
final client2RemoveResponse = jsonDecode(client2Messages[2]);
expect(client1RemoveResponse['type'], equals('remove'));
expect(client1RemoveResponse['data'], isA<List>());
expect(client1RemoveResponse['data'].length, equals(1));
expect(client1RemoveResponse['data'][0]['id'], equals('server-to-remove'));
expect(client2RemoveResponse['type'], equals('remove'));
expect(client2RemoveResponse['data'], isA<List>());
expect(client2RemoveResponse['data'].length, equals(1));
expect(client2RemoveResponse['data'][0]['id'], equals('server-to-remove'));
});
test('should handle removal of non-existent entry', () async {
final client = await WebSocket.connect('ws://localhost:$testPort');
final messagesFuture = client.toList();
final removeMessage = {
'type': 'remove',
'data': 'non-existent-id',
};
client.add(jsonEncode(removeMessage));
await client.close();
final messages = await messagesFuture;
expect(messages.length, equals(2));
final response = jsonDecode(messages[1]);
expect(response['type'], equals('remove'));
expect(response['success'], equals(false));
expect(response['message'], equals('Invalid server entry'));
});
test('should handle unknown message type', () async {
final client = await WebSocket.connect('ws://localhost:$testPort');
final messagesFuture = client.toList();
final unknownMessage = {
'type': 'unknown',
'data': {'some': 'data'},
};
client.add(jsonEncode(unknownMessage));
await client.close();
final messages = await messagesFuture;
expect(messages.length, equals(2));
final response = jsonDecode(messages[1]);
expect(response['type'], equals('unknown'));
expect(response['success'], equals(false));
expect(response['message'], equals('Unknown type'));
});
test('should handle invalid JSON', () async {
final client = await WebSocket.connect('ws://localhost:$testPort');
final messagesFuture = client.toList();
client.add('invalid json string');
await client.close();
final messages = await messagesFuture;
expect(messages.length, equals(2));
final response = jsonDecode(messages[1]);
expect(response['success'], equals(false));
expect(response.containsKey('message'), isTrue);
});
test('should handle multiple clients adding different entries', () async {
final client1 = await WebSocket.connect('ws://localhost:$testPort');
final client1MessagesFuture = client1.toList();
final client2 = await WebSocket.connect('ws://localhost:$testPort');
final client2MessagesFuture = client2.toList();
final client3 = await WebSocket.connect('ws://localhost:$testPort');
final client3MessagesFuture = client3.toList();
final entry1 = {
'id': 'server-1',
'name': 'Server One',
'description': 'First server',
'version': '1.0.0',
'password': 'pass1',
'timestamp': DateTime.now().toIso8601String(),
'ip': '192.168.1.1',
'author': 'Author 1',
'discoverable': true,
};
final entry2 = {
'id': 'server-2',
'name': 'Server Two',
'description': 'Second server',
'version': '2.0.0',
'password': 'pass2',
'timestamp': DateTime.now().toIso8601String(),
'ip': '192.168.1.2',
'author': 'Author 2',
'discoverable': false,
};
client1.add(jsonEncode({'type': 'add', 'data': entry1}));
client2.add(jsonEncode({'type': 'add', 'data': entry2}));
await Future.delayed(Duration(milliseconds: 200));
await client1.close();
await client2.close();
await client3.close();
final client1Messages = await client1MessagesFuture;
final client2Messages = await client2MessagesFuture;
final client3Messages = await client3MessagesFuture;
expect(client1Messages.length, equals(3));
expect(client2Messages.length, equals(3));
expect(client3Messages.length, equals(3));
final allServerIds = <String>{};
for (final messages in [client1Messages, client2Messages, client3Messages]) {
for (int i = 1; i < messages.length; i++) {
final parsed = jsonDecode(messages[i]);
if (parsed['type'] == 'add' && parsed['data'] is List) {
for (final entry in parsed['data']) {
allServerIds.add(entry['id']);
}
}
}
}
expect(allServerIds, containsAll(['server-1', 'server-2']));
});
test('should handle client disconnection gracefully', () async {
final client1 = await WebSocket.connect('ws://localhost:$testPort');
final client1MessagesFuture = client1.toList();
final client2 = await WebSocket.connect('ws://localhost:$testPort');
final client2MessagesFuture = client2.toList();
await client1.close();
final client1Messages = await client1MessagesFuture;
expect(client1Messages.length, equals(1));
final testEntry = {
'id': 'after-disconnect',
'name': 'After Disconnect',
'description': 'Added after client disconnect',
'version': '1.0.0',
'password': 'disconnect123',
'timestamp': DateTime.now().toIso8601String(),
'ip': '172.16.0.1',
'author': 'Disconnect Author',
'discoverable': true,
};
client2.add(jsonEncode({'type': 'add', 'data': testEntry}));
await client2.close();
final client2Messages = await client2MessagesFuture;
expect(client2Messages.length, equals(2));
final response = jsonDecode(client2Messages[1]);
expect(response['type'], equals('add'));
expect(response['data'][0]['id'], equals('after-disconnect'));
});
test('should handle ServerEntry serialization correctly', () async {
final client = await WebSocket.connect('ws://localhost:$testPort');
final messagesFuture = client.toList();
final testEntry = {
'id': 'serialization-test',
'name': 'Serialization Test',
'description': 'Testing serialization',
'version': '3.1.4',
'password': 'serialize123',
'timestamp': DateTime.now().toIso8601String(),
'ip': '203.0.113.1',
'author': 'Serialization Author',
'discoverable': true,
};
client.add(jsonEncode({'type': 'add', 'data': testEntry}));
await client.close();
final messages = await messagesFuture;
expect(messages.length, equals(2));
final broadcastMessage = jsonDecode(messages[1]);
final receivedEntry = broadcastMessage['data'][0];
expect(receivedEntry['id'], equals('serialization-test'));
expect(receivedEntry['name'], equals('Serialization Test'));
expect(receivedEntry['description'], equals('Testing serialization'));
expect(receivedEntry['version'], equals('3.1.4'));
expect(receivedEntry['password'], equals('serialize123'));
expect(receivedEntry['ip'], equals('203.0.113.1'));
expect(receivedEntry['author'], equals('Serialization Author'));
expect(receivedEntry['discoverable'], equals(true));
expect(receivedEntry['timestamp'], isA<String>());
expect(() => DateTime.parse(receivedEntry['timestamp']), returnsNormally);
});
test('should handle rapid sequential operations', () async {
final client1 = await WebSocket.connect('ws://localhost:$testPort');
final client1MessagesFuture = client1.toList();
final client2 = await WebSocket.connect('ws://localhost:$testPort');
final client2MessagesFuture = client2.toList();
final entry1 = {
'id': 'rapid-1',
'name': 'Rapid Server 1',
'description': 'First rapid server',
'version': '1.0.0',
'password': 'rapid123',
'timestamp': DateTime.now().toIso8601String(),
'ip': '10.0.0.1',
'author': 'Rapid Author',
'discoverable': true,
};
final entry2 = {
'id': 'rapid-2',
'name': 'Rapid Server 2',
'description': 'Second rapid server',
'version': '1.0.1',
'password': 'rapid456',
'timestamp': DateTime.now().toIso8601String(),
'ip': '10.0.0.2',
'author': 'Rapid Author',
'discoverable': false,
};
client1.add(jsonEncode({'type': 'add', 'data': entry1}));
client1.add(jsonEncode({'type': 'add', 'data': entry2}));
client1.add(jsonEncode({'type': 'remove', 'data': 'rapid-1'}));
await Future.delayed(Duration(milliseconds: 200));
await client1.close();
await client2.close();
final client1Messages = await client1MessagesFuture;
final client2Messages = await client2MessagesFuture;
expect(client1Messages.length, equals(4));
expect(client2Messages.length, equals(4));
final lastAddMessage = jsonDecode(client1Messages[2]);
expect(lastAddMessage['type'], equals('add'));
expect(lastAddMessage['data'][0]['id'], equals('rapid-2'));
final removeMessage = jsonDecode(client1Messages[3]);
expect(removeMessage['type'], equals('remove'));
expect(removeMessage['data'][0]['id'], equals('rapid-1'));
});
});
}