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:
parent
38770569ea
commit
28dd3ea65a
16 changed files with 1047 additions and 1741 deletions
|
|
@ -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 {}
|
||||
|
|
@ -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()),
|
||||
);
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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 (_) {}
|
||||
|
|
|
|||
|
|
@ -1,11 +0,0 @@
|
|||
emailIdp:
|
||||
- login:
|
||||
- startRegistration:
|
||||
- verifyRegistrationCode:
|
||||
- finishRegistration:
|
||||
- startPasswordReset:
|
||||
- verifyPasswordResetCode:
|
||||
- finishPasswordReset:
|
||||
- hasAccount:
|
||||
greeting:
|
||||
- hello:
|
||||
|
|
@ -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.
|
||||
///
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
31
apps/todo/halkyon_todo_server/lib/src/pruning.dart
Normal file
31
apps/todo/halkyon_todo_server/lib/src/pruning.dart
Normal 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();
|
||||
}
|
||||
|
|
@ -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.');
|
||||
}
|
||||
}
|
||||
27
apps/todo/halkyon_todo_server/lib/src/todo_base.dart
Normal file
27
apps/todo/halkyon_todo_server/lib/src/todo_base.dart
Normal 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,
|
||||
);
|
||||
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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};
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue