halkyon_base: a Serverpod module for sign-in, accounts and instance basics

What every Halkyon server has, taken from Halkyon Learn's: anonymous,
GitHub and email-code sign-in with their endpoints, an account's ways
in and its deletion, the pruning of abandoned anonymous people, of
sign-up attempts and of expired codes, and the instance's basics (the
sign-in ways offered, the source link, the minimum app build, where
people's data and its backups are). Endpoints sit under the nickname
`base`. A host configures it in one place before its pod starts: its
name for the mails, its source link, its minimum build, and hooks that
delete its own data with a person.

The tables keep Learn's names, columns and indexes, so a host moving
onto the module migrates nothing but version rows. Server package under
the SSPL, client under the AGPL, as Learn's are.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
Tizian 「ティツィアーン」 2026-10-19 14:28:09 +02:00
commit af1de799f4
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64

View file

@ -0,0 +1,222 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:halkyon_base_server/base.dart';
import 'package:mailer/mailer.dart' as mailer;
import 'package:mailer/smtp_server.dart';
import 'package:serverpod/serverpod.dart';
import 'package:test/test.dart';
/// An SMTP server on a loopback port that takes one mail at a time, offers
/// no TLS, and keeps what it is told.
final class _FakeSmtp {
_FakeSmtp._(this._socket) {
_socket.listen(_serve);
}
static Future<_FakeSmtp> start() async =>
_FakeSmtp._(await ServerSocket.bind(InternetAddress.loopbackIPv4, 0));
final ServerSocket _socket;
/// The commands received, in order.
final commands = <String>[];
/// What each mail's DATA held, its lines joined.
final mails = <String>[];
int get port => _socket.port;
Future<void> close() => _socket.close();
void _serve(Socket client) {
client.write('220 fake ESMTP\r\n');
var step = '';
final data = StringBuffer();
utf8.decoder.bind(client).transform(const LineSplitter()).listen((line) {
if (step == 'data') {
if (line == '.') {
step = '';
mails.add(data.toString());
data.clear();
client.write('250 2.0.0 queued\r\n');
} else {
data.writeln(line);
}
return;
}
if (step == 'auth') {
step = '';
client.write('235 2.7.0 Authentication successful\r\n');
return;
}
commands.add(line);
final command = line.toUpperCase();
if (command.startsWith('EHLO')) {
client.write('250-fake\r\n250 AUTH PLAIN\r\n');
} else if (command == 'AUTH PLAIN') {
step = 'auth';
client.write('334 \r\n');
} else if (command == 'DATA') {
step = 'data';
client.write('354 End data with <CR><LF>.<CR><LF>\r\n');
} else if (command == 'QUIT') {
client
..write('221 2.0.0 Bye\r\n')
..close();
} else {
client.write('250 2.0.0 OK\r\n');
}
});
}
}
/// The text of [mail] as DATA held it: its headers and its body, decoded
/// from base64 where it was encoded so.
({String headers, String body}) _read(String mail) {
final at = mail.indexOf('\n\n');
final headers = mail.substring(0, at);
final body = mail.substring(at + 2);
return (
headers: headers,
body: headers.toLowerCase().contains('content-transfer-encoding: base64')
? utf8.decode(base64.decode(body.replaceAll(RegExp(r'\s'), '')))
: body,
);
}
void main() {
group('the mail', () {
test('is plain text with the code first, in the subject too, naming '
'the instance and how long the code lasts', () {
final mail = codeMail(
code: 'K7QX-M2PD',
language: 'en',
app: 'Halkyon Learn',
instance: 'learn.halkyon.eu',
);
expect(mail.subject, 'K7QX-M2PD is your Halkyon Learn code');
expect(mail.body, startsWith('K7QX-M2PD\n\n'));
expect(mail.body, contains('sign in on learn.halkyon.eu'));
expect(mail.body, contains('15 minutes'));
expect(mail.body, isNot(contains('http')));
});
test('in German for German, in English for anything else', () {
({String subject, String body}) mailIn(String language) => codeMail(
code: 'K7QX-M2PD',
language: language,
app: 'Halkyon Learn',
instance: 'x',
);
expect(mailIn('de').subject, 'K7QX-M2PD ist dein Halkyon-Learn-Code');
expect(mailIn('fr').subject, 'K7QX-M2PD is your Halkyon Learn code');
});
test('names the app the host is, in its text too', () {
({String subject, String body}) mailIn(String language) => codeMail(
code: 'K7QX-M2PD',
language: language,
app: 'Halkyon Todo',
instance: 'todo.example.org',
);
expect(mailIn('en').subject, 'K7QX-M2PD is your Halkyon Todo code');
expect(mailIn('en').body, contains('Type this code in Halkyon Todo'));
expect(mailIn('de').subject, 'K7QX-M2PD ist dein Halkyon-Todo-Code');
expect(mailIn('de').body, contains('Gib diesen Code in Halkyon Todo'));
});
});
group('codes go', () {
final smtp = SmtpCodeMailer(
server: SmtpServer('localhost'),
from: const mailer.Address('learn@halkyon.eu'),
app: 'Halkyon Learn',
instance: 'learn.halkyon.eu',
);
test('through the SMTP service set up, in every run mode', () {
for (final runMode in [
ServerpodRunMode.development,
ServerpodRunMode.production,
]) {
expect(codeMailerFor(runMode: runMode, smtp: smtp), same(smtp));
}
});
test('into the log in development without one, and nowhere elsewhere '
'(no email sign-in)', () {
expect(
codeMailerFor(runMode: ServerpodRunMode.development, smtp: null),
isA<LogCodeMailer>(),
);
for (final runMode in [
ServerpodRunMode.staging,
ServerpodRunMode.production,
ServerpodRunMode.test,
]) {
expect(codeMailerFor(runMode: runMode, smtp: null), isNull);
}
});
});
group('over SMTP', () {
late _FakeSmtp server;
setUp(() async => server = await _FakeSmtp.start());
tearDown(() => server.close());
SmtpCodeMailer mailerFor({required bool allowInsecure}) => SmtpCodeMailer(
server: SmtpServer(
'127.0.0.1',
port: server.port,
username: 'learn@halkyon.eu',
password: 'token',
allowInsecure: allowInsecure,
),
from: const mailer.Address('learn@halkyon.eu', 'Halkyon Learn'),
app: 'Halkyon Learn',
instance: 'learn.halkyon.eu',
);
test(
'mails the code from the address set up to the address typed',
() async {
await mailerFor(allowInsecure: true).mail(
to: 'anna@example.org',
code: 'K7QX-M2PD',
language: 'de',
);
expect(server.commands, contains('MAIL FROM:<learn@halkyon.eu>'));
expect(server.commands, contains('RCPT TO:<anna@example.org>'));
final mail = _read(server.mails.single);
expect(mail.headers, contains('Halkyon Learn'));
expect(mail.headers, contains('<learn@halkyon.eu>'));
expect(mail.headers, contains('text/plain'));
expect(mail.body, startsWith('K7QX-M2PD'));
expect(mail.body, contains('auf learn.halkyon.eu anzumelden'));
},
);
test('never over a connection without TLS', () async {
await expectLater(
mailerFor(allowInsecure: false).mail(
to: 'anna@example.org',
code: 'K7QX-M2PD',
language: 'en',
),
throwsA(isA<Exception>()),
);
expect(server.mails, isEmpty);
expect(
server.commands.where((command) => command.startsWith('AUTH')),
isEmpty,
);
});
});
}