feat(todo): the server on halkyon_base

Todo's server includes halkyon_base (ADR 0014), configured for Todo:
its basics say it serves `todo`, sign-in mails name Halkyon Todo, the
source is this repository, minimumAppBuild 1. Authentication through
the module (anonymous, GitHub, email codes, server-side sessions) in
place of the scaffold's email-and-password provider; the module's
pruning scheduled once however often the server starts. The scaffold's
greeting, its Flutter web hosting, cloud storage and Docker files go;
session logs are never persisted. Todo was never deployed, so its
migrations start again with one initial migration. The root's lints
apply, with the four constructor rules off as on Learn's server.

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 21:56:12 +02:00
commit 28dd3ea65a
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64
17 changed files with 1047 additions and 1741 deletions

View file

@ -1,109 +1,25 @@
import 'dart:io';
import 'package:halkyon_base_server/base.dart';
import 'package:halkyon_todo_server/src/generated/serverpod.dart';
import 'package:halkyon_todo_server/src/pruning.dart';
import 'package:halkyon_todo_server/src/todo_base.dart';
import 'package:serverpod_auth_idp_server/core.dart';
import 'package:serverpod_auth_idp_server/providers/email.dart';
import 'package:serverpod_cloud_storage/serverpod_cloud_storage.dart';
import 'src/cache_busting.dart';
import 'src/generated/serverpod.dart';
import 'src/web/routes/app_config_route.dart';
/// The starting point of the Serverpod server.
void run(List<String> args) async {
// Initialize Serverpod. The generated Serverpod class is already connected
// with your project's generated code.
/// The starting point of Halkyon Todo's server.
Future<void> run(List<String> args) async {
// The generated Serverpod is already connected with the generated code.
final pod = Serverpod(args);
// Initialize authentication services for the server.
// Token managers will be used to validate and issue authentication keys,
// and the identity providers will be the authentication options available for users.
// What every Halkyon server has (ADR 0014): sign-in, the account and its
// deletion, the pruning, the instance's basics, set up for Todo.
HalkyonBase.configure(pod, todoBase());
// Token managers validate and issue authentication keys; identity
// providers are the ways people can sign in (halkyon_base's
// auth_config.dart).
pod.initializeAuthServices(
tokenManagerBuilders: [
// Server-side sessions: every request looks its session up in the
// database, so signing out or deleting an account takes effect at once.
// A session unused for 90 days expires; one in use never does.
ServerSideSessionsConfigFromPasswords(
defaultSessionInactivityTimeout: const Duration(days: 90),
),
],
identityProviderBuilders: [
// Configure the email identity provider for email/password authentication.
// The default setup works with Serverpod Cloud without configuration. In
// development the verification codes are logged to the console, and in
// staging and production they are sent through the Serverpod Cloud email
// service. If you want to use a custom provider for sending emails, use
// `EmailIdpConfigFromPasswords`.
ServerpodCloudEmailIdpConfig(
appDisplayName: 'halkyon_todo',
),
],
tokenManagerBuilders: tokenManagerBuilders,
identityProviderBuilders: identityProviderBuilders(pod),
);
// Serve all files in the web/static relative directory under /web.
// These are used by the default web page.
pod.webServer.addRoute(
StaticRoute.withCacheBusting(cacheBustingConfig),
cacheBustingConfig.mountPrefix,
);
// Setup the app config route.
// We build this configuration based on the servers api url and serve it to
// the flutter app.
pod.webServer.addRoute(
AppConfigRoute(apiConfig: pod.config.apiServer),
'/assets/assets/config.json',
);
// Checks if the flutter web app has been built and serves it if it has.
final appDir = Directory(Uri(path: 'web/app').toFilePath());
if (appDir.existsSync()) {
// Serve the flutter web app under /.
pod.webServer.addRoute(
FlutterRoute(
appDir,
// If building the Flutter app with WASM, set the below parameter to
// true and add the --wasm flag to the flutter build command.
enableWasmHeaders: false,
),
'/',
);
} else {
// If the flutter web app has not been built, serve the build app page.
final defaultRoute = StaticRoute.file(
File(
Uri(path: 'web/pages/build_flutter_app.html').toFilePath(),
),
);
pod.webServer.addMiddleware(
FallbackMiddleware(
fallback: defaultRoute,
on: (response) => response.statusCode == 404,
).call,
'/',
);
pod.webServer.addRoute(
defaultRoute,
'/**',
);
}
// Configure cloud storage.
// This setup works with Serverpod Cloud without extra configuration.
// If you want to use a custom provider for cloud storage, replace these
// with your preferred provider.
pod.addCloudStorage(
await ServerpodCloudProvider.private(
fallback: () => DatabaseCloudStorage('private'),
),
);
pod.addCloudStorage(
await ServerpodCloudProvider.public(
fallback: () => DatabaseCloudStorage('public'),
),
);
// Start the server.
await pod.start();
await schedulePruning(pod);
}

View file

@ -1,6 +0,0 @@
import 'package:serverpod_auth_idp_server/providers/email.dart';
/// By extending [EmailIdpBaseEndpoint], the email identity provider endpoints
/// are made available on the server and enable the corresponding sign-in widget
/// on the client.
class EmailIdpEndpoint extends EmailIdpBaseEndpoint {}

View file

@ -1,10 +0,0 @@
import 'dart:io';
import 'package:serverpod/serverpod.dart';
/// Cache busting configuration.
/// Used in WidgetRoutes to cache-bust asset paths.
final cacheBustingConfig = CacheBustingConfig(
mountPrefix: '/web',
fileSystemRoot: Directory(Uri(path: 'web/static').toFilePath()),
);

View file

@ -10,239 +10,29 @@
// ignore_for_file: invalid_use_of_internal_member
// ignore_for_file: no_leading_underscores_for_library_prefixes
import 'package:halkyon_base_server/halkyon_base_server.dart' as _ik8molhd;
import 'package:halkyon_todo_server/src/generated/future_calls.dart'
as _i772x17y;
import 'package:serverpod/serverpod.dart' as _is;
import 'package:serverpod_auth_core_server/serverpod_auth_core_server.dart'
as _iacs;
import 'package:serverpod_auth_idp_server/serverpod_auth_idp_server.dart'
as _iais;
import '../auth/email_idp_endpoint.dart' as _iuc1hd5t;
import '../greetings/greeting_endpoint.dart' as _il624ik7;
export 'future_calls.dart' show ServerpodFutureCallsGetter;
class Endpoints extends _is.EndpointDispatch {
@override
void initializeEndpoints(_is.Server server) {
var endpoints = <String, _is.Endpoint>{
'emailIdp': _iuc1hd5t.EmailIdpEndpoint()
..initialize(
server,
'emailIdp',
null,
),
'greeting': _il624ik7.GreetingEndpoint()
..initialize(
server,
'greeting',
null,
),
};
connectors['emailIdp'] = _is.EndpointConnector(
name: 'emailIdp',
endpoint: endpoints['emailIdp']!,
methodConnectors: {
'login': _is.MethodConnector(
name: 'login',
params: {
'email': _is.ParameterDescription(
name: 'email',
type: _is.getType<String>(),
nullable: false,
),
'password': _is.ParameterDescription(
name: 'password',
type: _is.getType<String>(),
nullable: false,
),
},
call:
(
_is.Session session,
Map<String, dynamic> params,
) async =>
(endpoints['emailIdp'] as _iuc1hd5t.EmailIdpEndpoint).login(
session,
email: params['email'],
password: params['password'],
),
),
'startRegistration': _is.MethodConnector(
name: 'startRegistration',
params: {
'email': _is.ParameterDescription(
name: 'email',
type: _is.getType<String>(),
nullable: false,
),
},
call:
(
_is.Session session,
Map<String, dynamic> params,
) async => (endpoints['emailIdp'] as _iuc1hd5t.EmailIdpEndpoint)
.startRegistration(
session,
email: params['email'],
),
),
'verifyRegistrationCode': _is.MethodConnector(
name: 'verifyRegistrationCode',
params: {
'accountRequestId': _is.ParameterDescription(
name: 'accountRequestId',
type: _is.getType<_is.UuidValue>(),
nullable: false,
),
'verificationCode': _is.ParameterDescription(
name: 'verificationCode',
type: _is.getType<String>(),
nullable: false,
),
},
call:
(
_is.Session session,
Map<String, dynamic> params,
) async => (endpoints['emailIdp'] as _iuc1hd5t.EmailIdpEndpoint)
.verifyRegistrationCode(
session,
accountRequestId: params['accountRequestId'],
verificationCode: params['verificationCode'],
),
),
'finishRegistration': _is.MethodConnector(
name: 'finishRegistration',
params: {
'registrationToken': _is.ParameterDescription(
name: 'registrationToken',
type: _is.getType<String>(),
nullable: false,
),
'password': _is.ParameterDescription(
name: 'password',
type: _is.getType<String>(),
nullable: false,
),
},
call:
(
_is.Session session,
Map<String, dynamic> params,
) async => (endpoints['emailIdp'] as _iuc1hd5t.EmailIdpEndpoint)
.finishRegistration(
session,
registrationToken: params['registrationToken'],
password: params['password'],
),
),
'startPasswordReset': _is.MethodConnector(
name: 'startPasswordReset',
params: {
'email': _is.ParameterDescription(
name: 'email',
type: _is.getType<String>(),
nullable: false,
),
},
call:
(
_is.Session session,
Map<String, dynamic> params,
) async => (endpoints['emailIdp'] as _iuc1hd5t.EmailIdpEndpoint)
.startPasswordReset(
session,
email: params['email'],
),
),
'verifyPasswordResetCode': _is.MethodConnector(
name: 'verifyPasswordResetCode',
params: {
'passwordResetRequestId': _is.ParameterDescription(
name: 'passwordResetRequestId',
type: _is.getType<_is.UuidValue>(),
nullable: false,
),
'verificationCode': _is.ParameterDescription(
name: 'verificationCode',
type: _is.getType<String>(),
nullable: false,
),
},
call:
(
_is.Session session,
Map<String, dynamic> params,
) async => (endpoints['emailIdp'] as _iuc1hd5t.EmailIdpEndpoint)
.verifyPasswordResetCode(
session,
passwordResetRequestId: params['passwordResetRequestId'],
verificationCode: params['verificationCode'],
),
),
'finishPasswordReset': _is.MethodConnector(
name: 'finishPasswordReset',
params: {
'finishPasswordResetToken': _is.ParameterDescription(
name: 'finishPasswordResetToken',
type: _is.getType<String>(),
nullable: false,
),
'newPassword': _is.ParameterDescription(
name: 'newPassword',
type: _is.getType<String>(),
nullable: false,
),
},
call:
(
_is.Session session,
Map<String, dynamic> params,
) async => (endpoints['emailIdp'] as _iuc1hd5t.EmailIdpEndpoint)
.finishPasswordReset(
session,
finishPasswordResetToken:
params['finishPasswordResetToken'],
newPassword: params['newPassword'],
),
),
'hasAccount': _is.MethodConnector(
name: 'hasAccount',
params: {},
call:
(
_is.Session session,
Map<String, dynamic> params,
) async => (endpoints['emailIdp'] as _iuc1hd5t.EmailIdpEndpoint)
.hasAccount(session),
),
},
);
connectors['greeting'] = _is.EndpointConnector(
name: 'greeting',
endpoint: endpoints['greeting']!,
methodConnectors: {
'hello': _is.MethodConnector(
name: 'hello',
params: {
'name': _is.ParameterDescription(
name: 'name',
type: _is.getType<String>(),
nullable: false,
),
},
call:
(
_is.Session session,
Map<String, dynamic> params,
) async =>
(endpoints['greeting'] as _il624ik7.GreetingEndpoint).hello(
session,
params['name'],
),
),
},
);
modules['halkyon_base'] = _ik8molhd.Endpoints()
..initializeEndpoints(server);
modules['serverpod_auth_idp'] = _iais.Endpoints()
..initializeEndpoints(server);
modules['serverpod_auth_core'] = _iacs.Endpoints()
..initializeEndpoints(server);
}
@override
_is.FutureCallDispatch? get futureCalls {
return _i772x17y.FutureCalls();
}
}

View file

@ -0,0 +1,232 @@
/* AUTOMATICALLY GENERATED CODE DO NOT MODIFY */
/* To generate run: "serverpod generate" */
// ignore_for_file: implementation_imports
// ignore_for_file: library_private_types_in_public_api
// ignore_for_file: non_constant_identifier_names
// ignore_for_file: public_member_api_docs
// ignore_for_file: type_literal_in_constant_pattern
// ignore_for_file: use_super_parameters
// ignore_for_file: invalid_use_of_internal_member
// ignore_for_file: depend_on_referenced_packages
// ignore_for_file: no_leading_underscores_for_library_prefixes
import 'dart:async' as _ida;
import 'package:clock/clock.dart' as _io0w16m8;
import 'package:serverpod/serverpod.dart' as _is;
import '../pruning_future_call.dart' as _ibrplims;
/// Invokes a future call.
typedef _InvokeFutureCall =
Future<void> Function(String name, _is.SerializableModel? object);
extension ServerpodFutureCallsGetter on _is.Serverpod {
/// Generated future calls.
FutureCalls get futureCalls => FutureCalls();
}
class FutureCalls extends _is.FutureCallDispatch<_FutureCallRef> {
FutureCalls._();
factory FutureCalls() {
return _instance;
}
static final FutureCalls _instance = FutureCalls._();
_is.FutureCallManager? _futureCallManager;
String? _serverId;
String get _effectiveServerId {
if (_serverId == null) {
throw StateError('FutureCalls is not initialized.');
}
return _serverId!;
}
_is.FutureCallManager get _effectiveFutureCallManager {
if (_futureCallManager == null) {
throw StateError('FutureCalls is not initialized.');
}
return _futureCallManager!;
}
@override
void initialize(
_is.FutureCallManager futureCallManager,
String serverId,
) {
var registeredFutureCalls = <String, _is.InvokableFutureCall>{
'PruningPruneAnonymousPeopleFutureCall':
PruningPruneAnonymousPeopleFutureCall(),
'PruningForgetSignUpAttemptsFutureCall':
PruningForgetSignUpAttemptsFutureCall(),
};
_futureCallManager = futureCallManager;
_serverId = serverId;
for (final entry in registeredFutureCalls.entries) {
_futureCallManager?.registerFutureCall(entry.value, entry.key);
}
}
@override
_FutureCallRef callAtTime(
DateTime time, {
String? identifier,
}) {
return _FutureCallRef(
(name, object) {
return _effectiveFutureCallManager.scheduleFutureCall(
name,
object,
time,
_effectiveServerId,
identifier,
);
},
);
}
@override
_FutureCallRef callWithDelay(
Duration delay, {
String? identifier,
}) {
return _FutureCallRef(
(name, object) {
return _effectiveFutureCallManager.scheduleFutureCall(
name,
object,
DateTime.now().toUtc().add(delay),
_effectiveServerId,
identifier,
);
},
);
}
@override
_is.RecurringFutureCallDispatch<_FutureCallRef> callRecurring({
String? identifier,
}) {
return _RecurringFutureCallDispatchImpl(
_effectiveFutureCallManager,
_effectiveServerId,
identifier,
);
}
@override
Future<void> cancel(String identifier) async {
await _effectiveFutureCallManager.cancelFutureCall(identifier);
}
}
class _RecurringFutureCallDispatchImpl
extends _is.RecurringFutureCallDispatch<_FutureCallRef> {
_RecurringFutureCallDispatchImpl(
this._futureCallManager,
this._serverId,
this._identifier,
);
final _is.FutureCallManager _futureCallManager;
final String _serverId;
final String? _identifier;
@override
_FutureCallRef cron(String cronExpression) {
return _FutureCallRef(
(name, object) {
return _futureCallManager.scheduleFutureCall(
name,
object,
_is.Cron.parse(cronExpression).nextTime(),
_serverId,
_identifier,
scheduling: _is.CronFutureCallScheduling(cron: cronExpression),
);
},
);
}
@override
_FutureCallRef every(
Duration interval, {
DateTime? start,
}) {
final now = _io0w16m8.clock.now().toUtc();
return _FutureCallRef(
(name, object) {
return _futureCallManager.scheduleFutureCall(
name,
object,
start ?? now.add(interval),
_serverId,
_identifier,
scheduling: _is.IntervalFutureCallScheduling(
interval: interval,
start: start,
),
);
},
);
}
}
class _FutureCallRef {
_FutureCallRef(this._invokeFutureCall);
final _InvokeFutureCall _invokeFutureCall;
late final pruning = _PruningFutureCallDispatcher(_invokeFutureCall);
}
class _PruningFutureCallDispatcher {
_PruningFutureCallDispatcher(this._invokeFutureCall);
final _InvokeFutureCall _invokeFutureCall;
Future<void> pruneAnonymousPeople() {
return _invokeFutureCall(
'PruningPruneAnonymousPeopleFutureCall',
null,
);
}
Future<void> forgetSignUpAttempts() {
return _invokeFutureCall(
'PruningForgetSignUpAttemptsFutureCall',
null,
);
}
}
/// Prunes one batch and logs how many people went.
class PruningPruneAnonymousPeopleFutureCall extends _is.FutureCall
implements _is.InvokableFutureCall {
@override
_ida.Future<void> invoke(
_is.Session session,
_is.SerializableModel? object,
) async {
await _ibrplims.PruningFutureCall().pruneAnonymousPeople(session);
}
}
/// Forgets the sign-up attempts the rate limit is done with, with their IP
/// addresses, and the email sign-in codes past their time, and logs how
/// many.
class PruningForgetSignUpAttemptsFutureCall extends _is.FutureCall
implements _is.InvokableFutureCall {
@override
_ida.Future<void> invoke(
_is.Session session,
_is.SerializableModel? object,
) async {
await _ibrplims.PruningFutureCall().forgetSignUpAttempts(session);
}
}

View file

@ -1,109 +0,0 @@
/* AUTOMATICALLY GENERATED CODE DO NOT MODIFY */
/* To generate run: "serverpod generate" */
// ignore_for_file: implementation_imports
// ignore_for_file: library_private_types_in_public_api
// ignore_for_file: non_constant_identifier_names
// ignore_for_file: public_member_api_docs
// ignore_for_file: type_literal_in_constant_pattern
// ignore_for_file: use_super_parameters
// ignore_for_file: invalid_use_of_internal_member
// ignore_for_file: no_leading_underscores_for_library_prefixes
import 'package:serverpod/serverpod.dart' as _is;
/// A greeting message which can be sent to or from the server.
abstract class Greeting
implements _is.SerializableModel, _is.ProtocolSerialization {
Greeting._({
required this.message,
required this.author,
required this.timestamp,
});
factory Greeting({
required String message,
required String author,
required DateTime timestamp,
}) = _GreetingImpl;
factory Greeting.fromJson(Map<String, dynamic> jsonSerialization) {
return Greeting(
message: jsonSerialization['message'] as String,
author: jsonSerialization['author'] as String,
timestamp: _is.DateTimeJsonExtension.fromJson(
jsonSerialization['timestamp'],
),
);
}
/// The greeting message.
String message;
/// The author of the greeting message.
String author;
/// The time when the message was created.
DateTime timestamp;
/// Returns a shallow copy of this [Greeting]
/// with some or all fields replaced by the given arguments.
@_is.useResult
Greeting copyWith({
String? message,
String? author,
DateTime? timestamp,
});
@override
Map<String, dynamic> toJson() {
return {
'__className__': 'Greeting',
'message': message,
'author': author,
'timestamp': timestamp.toJson(),
};
}
@override
Map<String, dynamic> toJsonForProtocol() {
return {
'__className__': 'Greeting',
'message': message,
'author': author,
'timestamp': timestamp.toJson(),
};
}
@override
String toString() {
return _is.SerializationManager.encode(this);
}
}
class _GreetingImpl extends Greeting {
_GreetingImpl({
required String message,
required String author,
required DateTime timestamp,
}) : super._(
message: message,
author: author,
timestamp: timestamp,
);
/// Returns a shallow copy of this [Greeting]
/// with some or all fields replaced by the given arguments.
@_is.useResult
@override
Greeting copyWith({
String? message,
String? author,
DateTime? timestamp,
}) {
return Greeting(
message: message ?? this.message,
author: author ?? this.author,
timestamp: timestamp ?? this.timestamp,
);
}
}

View file

@ -11,14 +11,13 @@
// ignore_for_file: dead_code, unnecessary_type_check
// ignore_for_file: no_leading_underscores_for_library_prefixes
import 'package:halkyon_base_server/halkyon_base_server.dart' as _ik8molhd;
import 'package:serverpod/protocol.dart' as _isp;
import 'package:serverpod/serverpod.dart' as _is;
import 'package:serverpod_auth_core_server/serverpod_auth_core_server.dart'
as _iacs;
import 'package:serverpod_auth_idp_server/serverpod_auth_idp_server.dart'
as _iais;
import 'greetings/greeting.dart' as _izw8z7ou;
export 'greetings/greeting.dart';
class Protocol extends _is.DatabaseSerializationManager {
Protocol._();
@ -28,6 +27,7 @@ class Protocol extends _is.DatabaseSerializationManager {
static final Protocol _instance = Protocol._().._registerHostProtocols();
static List<_isp.TableDefinition> get targetTableDefinitions => [
..._ik8molhd.Protocol.targetTableDefinitions,
..._iais.Protocol.targetTableDefinitions,
..._iacs.Protocol.targetTableDefinitions,
..._isp.Protocol.targetTableDefinitions,
@ -60,12 +60,9 @@ class Protocol extends _is.DatabaseSerializationManager {
}
}
if (t == _izw8z7ou.Greeting) {
return _izw8z7ou.Greeting.fromJson(data) as T;
}
if (t == _is.getType<_izw8z7ou.Greeting?>()) {
return (data != null ? _izw8z7ou.Greeting.fromJson(data) : null) as T;
}
try {
return _ik8molhd.Protocol().deserialize<T>(data, t);
} on _is.DeserializationTypeNotFoundException catch (_) {}
try {
return _iais.Protocol().deserialize<T>(data, t);
} on _is.DeserializationTypeNotFoundException catch (_) {}
@ -80,7 +77,6 @@ class Protocol extends _is.DatabaseSerializationManager {
static String? getClassNameForType(Type type) {
return switch (type) {
_izw8z7ou.Greeting => 'Greeting',
_ => null,
};
}
@ -97,9 +93,9 @@ class Protocol extends _is.DatabaseSerializationManager {
);
}
switch (data) {
case _izw8z7ou.Greeting():
return 'Greeting';
className = _ik8molhd.Protocol().getClassNameForObject(data);
if (className != null) {
return className.contains('.') ? className : 'halkyon_base.$className';
}
className = _iais.Protocol().getClassNameForObject(data);
if (className != null) {
@ -126,8 +122,9 @@ class Protocol extends _is.DatabaseSerializationManager {
if (dataClassName is! String) {
return super.deserializeByClassName(data);
}
if (dataClassName == 'Greeting') {
return deserialize<_izw8z7ou.Greeting>(data['data']);
if (dataClassName.startsWith('halkyon_base.')) {
data['className'] = dataClassName.substring(13);
return _ik8molhd.Protocol().deserializeByClassName(data);
}
if (dataClassName.startsWith('serverpod_auth_idp.')) {
data['className'] = dataClassName.substring(19);
@ -145,12 +142,19 @@ class Protocol extends _is.DatabaseSerializationManager {
}
void _registerHostProtocols() {
_ik8molhd.Protocol().registerHostProtocol('halkyon_todo', this);
_iais.Protocol().registerHostProtocol('halkyon_todo', this);
_iacs.Protocol().registerHostProtocol('halkyon_todo', this);
}
@override
_is.Table? getTableForType(Type t) {
{
var table = _ik8molhd.Protocol().getTableForType(t);
if (table != null) {
return table;
}
}
{
var table = _iais.Protocol().getTableForType(t);
if (table != null) {
@ -188,6 +192,9 @@ class Protocol extends _is.DatabaseSerializationManager {
if (record == null) {
return null;
}
try {
return _ik8molhd.Protocol().mapRecordToJson(record);
} catch (_) {}
try {
return _iais.Protocol().mapRecordToJson(record);
} catch (_) {}

View file

@ -1,11 +0,0 @@
emailIdp:
- login:
- startRegistration:
- verifyRegistrationCode:
- finishRegistration:
- startPasswordReset:
- verifyPasswordResetCode:
- finishPasswordReset:
- hasAccount:
greeting:
- hello:

View file

@ -15,6 +15,7 @@ import 'package:serverpod/serverpod.dart' as _is;
import 'endpoints.dart' as _iavctuc6;
import 'protocol.dart' as _il2as5qe;
export 'package:serverpod/serverpod.dart' hide Serverpod;
export 'future_calls.dart' show ServerpodFutureCallsGetter;
/// The Serverpod server for this project.
///

View file

@ -1,24 +0,0 @@
# Yaml-files in the `models` directory specify which serializable objects
# should be generated. When you add or modify a file, you will need to run
# `serverpod generate` to make the generated classes available in the server and
# client.
#
# Please consult the documentation for more information on what you can add to
# your yaml-files.
### A greeting message which can be sent to or from the server.
class: Greeting
# Add the table key, if this class represents a row in the database.
#table: greeting
# The fields (and columns if connected to the database) of the class.
# For a list of supported types, please see the documentation.
# https://docs.serverpod.dev/concepts/working-with-endpoints
fields:
### The greeting message.
message: String
### The author of the greeting message.
author: String
### The time when the message was created.
timestamp: DateTime

View file

@ -1,35 +0,0 @@
import '../generated/protocol.dart';
import 'package:serverpod/serverpod.dart';
// This is an example endpoint of your server. It's best practice to use the
// `Endpoint` ending of the class name, but it will be removed when accessing
// the endpoint from the client. I.e., this endpoint can be accessed through
// `client.greeting` on the client side.
// After adding or modifying an endpoint, you will need to run
// `serverpod generate` to update the server and client code.
/// This is an example endpoint that returns a greeting message through
/// its [hello] method.
class GreetingEndpoint extends Endpoint {
// This method is called when the client calls the `hello` method on the
// `greeting` endpoint.
//
// The `Session` parameter contains the context of the client request.
// It provides access to the database and other server-side resources like
// secrets from your password file, the cache, storage, and server-event
// messaging.
//
// You can use any serializable type as a parameter or return type, read more
// in the [docs](https://docs.serverpod.dev/concepts/working-with-endpoints).
/// Returns a personalized greeting message: "Hello {name}".
Future<Greeting> hello(Session session, String name) async {
return Greeting(
message: 'Hello $name',
author: 'Serverpod',
timestamp: DateTime.now(),
);
}
}

View file

@ -0,0 +1,31 @@
import 'package:halkyon_todo_server/src/generated/future_calls.dart';
import 'package:serverpod/serverpod.dart';
/// The identifier of the daily pruning of abandoned anonymous people.
const _pruning = 'prune-abandoned-anonymous-people';
/// The identifier of the hourly forgetting of sign-up attempts.
const _attemptPruning = 'forget-sign-up-attempts';
/// The future calls [schedulePruning] keeps scheduled, by their identifiers.
const pruningIdentifiers = [_pruning, _attemptPruning];
/// Schedules the pruning (`PruningFutureCall`): abandoned anonymous people
/// daily, sign-up attempts and expired email codes hourly. Scheduling always
/// adds an entry, so the previous one goes first, and starting the server
/// again leaves one of each.
Future<void> schedulePruning(Serverpod pod) async {
await pod.futureCalls.cancel(_pruning);
await pod.futureCalls
.callRecurring(identifier: _pruning)
.cron('17 3 * * *')
.pruning
.pruneAnonymousPeople();
await pod.futureCalls.cancel(_attemptPruning);
await pod.futureCalls
.callRecurring(identifier: _attemptPruning)
.cron('27 * * * *')
.pruning
.forgetSignUpAttempts();
}

View file

@ -0,0 +1,28 @@
import 'package:clock/clock.dart';
import 'package:halkyon_base_server/base.dart';
import 'package:serverpod/serverpod.dart';
/// Deletes the anonymous people nobody can return to, daily, and the IP
/// addresses of sign-up attempts the rate limit is done with and the email
/// sign-in codes past their time, hourly: halkyon_base's pruning (ADR 0014),
/// which the server schedules when it starts (`schedulePruning`).
class PruningFutureCall extends FutureCall {
/// Prunes one batch and logs how many people went.
Future<void> pruneAnonymousPeople(Session session) async {
final pruned = await pruneAbandonedAnonymousPeople(
session,
now: clock.now(),
);
session.log('Pruned $pruned abandoned anonymous people.');
}
/// Forgets the sign-up attempts the rate limit is done with, with their IP
/// addresses, and the email sign-in codes past their time, and logs how
/// many.
Future<void> forgetSignUpAttempts(Session session) async {
final now = clock.now();
final forgotten = await forgetOldSignUpAttempts(session, now: now);
final codes = await forgetExpiredEmailCodes(session, now: now);
session.log('Forgot $forgotten sign-up attempts and $codes email codes.');
}
}

View file

@ -0,0 +1,27 @@
import 'package:halkyon_base_server/base.dart';
/// Where this project's source is published, which the instance names
/// unless its operator sets `SOURCE_URL`. Both licences require offering it
/// to the people who use the instance (AGPL-3.0-or-later for the app,
/// SSPL-1.0 for the server).
const upstreamSourceCodeUrl =
'https://code.halkyon.app/project-halkyon/halkyon';
/// The oldest build of the app this server works with (the number after `+`
/// in the app's version); apps below it are asked to update. Raise it in the
/// commit that breaks older apps, to the build of the first that works again.
const minimumAppBuild = 1;
/// How Halkyon Todo sets halkyon_base up (ADR 0014), the module giving every
/// Halkyon server its sign-in, the account and its deletion, the pruning and
/// the instance's basics: the instance says it serves `todo` (the app accepts
/// only such an instance; never change it), sign-in mails name the app, and
/// the instance names this project's source. Todo keeps nothing of a person's
/// yet, so it deletes nothing of its own with them. The server sets it up
/// before it starts (`server.dart`), and so do its tests.
HalkyonBase todoBase() => HalkyonBase(
app: 'todo',
appName: 'Halkyon Todo',
upstreamSourceCodeUrl: upstreamSourceCodeUrl,
minimumAppBuild: minimumAppBuild,
);

View file

@ -1,30 +0,0 @@
import 'package:serverpod/serverpod.dart';
class AppConfigWidget extends JsonWidget {
final String apiUrl;
AppConfigWidget({
required this.apiUrl,
}) : super(object: {'apiUrl': apiUrl});
}
class AppConfigRoute extends WidgetRoute {
AppConfigWidget widget;
AppConfigRoute({
required ServerConfig apiConfig,
}) : widget = AppConfigWidget(apiUrl: apiConfig.apiUrl.toString());
@override
Future<WebWidget> build(Session session, Request request) async {
return widget;
}
}
extension on ServerConfig {
Uri get apiUrl => Uri(
scheme: publicScheme,
host: publicHost,
port: publicPort,
);
}

View file

@ -1,12 +0,0 @@
import 'package:halkyon_todo_server/src/cache_busting.dart';
import 'package:halkyon_todo_server/src/web/widgets/built_with_serverpod_page.dart';
import 'package:serverpod/serverpod.dart';
class RootRoute extends WidgetRoute {
RootRoute() : super(cacheBustingConfig: cacheBustingConfig);
@override
Future<TemplateWidget> build(Session session, Request request) async {
return BuiltWithServerpodPageWidget();
}
}

View file

@ -1,11 +0,0 @@
import 'package:serverpod/serverpod.dart';
/// A widget that displays the Serverpod version and the current run mode.
/// It uses the built_with_serverpod.html template to render the page.
/// The [name] of the template should correspond to a template file in your
/// server's web/templates directory.
class BuiltWithServerpodPageWidget extends TemplateWidget {
BuiltWithServerpodPageWidget() : super(name: 'built_with_serverpod') {
values = {'served': DateTime.now(), 'runmode': Serverpod.instance.runMode};
}
}