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,76 @@
import 'package:halkyon_base_server/base.dart';
import 'package:serverpod/serverpod.dart';
import 'package:serverpod_auth_idp_server/core.dart';
import '../test_tools/serverpod_test_tools.dart';
/// The app the tests' host is.
const testAppName = 'Halkyon Test';
/// Where the tests' host says its project publishes its source.
const testUpstreamSourceCodeUrl = 'https://code.example.org/halkyon_test';
/// The oldest app build the tests' host works with.
const testMinimumAppBuild = 7;
/// Sets the module up on the pod [sessionBuilder]'s sessions run on, as a
/// host does before its pod starts, with [deletePersonData] as its
/// deletions, and authentication exactly as hosts run it
/// (`auth_config.dart`). Not on `Serverpod.instance`: that is the pod made
/// last, and a test file with several `withServerpod` groups has a pod each.
void useBase(
TestSessionBuilder sessionBuilder, {
List<PersonDataDeletion> deletePersonData = const [],
}) {
final pod = sessionBuilder.build().serverpod;
HalkyonBase.configure(
pod,
HalkyonBase(
appName: testAppName,
upstreamSourceCodeUrl: testUpstreamSourceCodeUrl,
minimumAppBuild: testMinimumAppBuild,
deletePersonData: deletePersonData,
),
);
AuthServices.set(
tokenManagerBuilders: tokenManagerBuilders,
identityProviderBuilders: identityProviderBuilders(pod),
);
}
/// Signs [person] in on a new device and returns the new session's id.
Future<UuidValue> signInNewDevice(
Session session,
UuidValue person, {
Set<Scope> scopes = const {},
}) async {
Future<Set<UuidValue>> sessionIds() async => {
for (final serverSideSession in await ServerSideSession.db.find(
session,
where: (t) => t.authUserId.equals(person),
))
serverSideSession.id!,
};
final before = await sessionIds();
await AuthServices.instance.tokenManager.createToken(
session,
authUserId: person,
method: 'test',
scopes: scopes,
);
return (await sessionIds()).difference(before).single;
}
/// A person as an app's first launch creates them: anonymous.
Future<UuidValue> newAnonymousPerson(
Session session, {
DateTime? createdAt,
}) async {
final person = (await AuthServices.instance.authUsers.create(session)).id;
await AnonymousAccount.db.insertRow(
session,
AnonymousAccount(authUserId: person, createdAt: createdAt),
);
return person;
}