feat(server): the instance names where its source is
Both licences (AGPL for the app, SSPL for the server) require offering the source of what an instance runs to the people who use it. instance.info() now carries sourceCodeUrl: the operator's SOURCE_URL, which they must set when running modified code, and otherwise this project's repository. Serverpod validates its configuration files strictly, so the value comes from the environment; deploy/compose.yaml passes it on from deploy/.env, whose example explains it. A test covers the default. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
f841542d1d
commit
863f0899e3
9 changed files with 113 additions and 14 deletions
|
|
@ -22,3 +22,9 @@ SESSION_KEY_HASH_PEPPER=
|
|||
# Empty: GitHub sign-in is off, and the app doesn't offer it.
|
||||
GITHUB_CLIENT_ID=
|
||||
GITHUB_CLIENT_SECRET=
|
||||
|
||||
# Where the source of what this instance runs is, shown in the app's About dialog: both
|
||||
# licences (AGPL for the app, SSPL for the server) require offering it to the people who
|
||||
# use the instance. Empty: the project's repository, which is right for unmodified code.
|
||||
# Running modified code, set it to where your modified source is published.
|
||||
SOURCE_URL=
|
||||
|
|
|
|||
|
|
@ -50,6 +50,8 @@ services:
|
|||
# GitHub sign-in, only with this instance's GitHub OAuth app; empty: off.
|
||||
SERVERPOD_PASSWORD_githubClientId: ${GITHUB_CLIENT_ID:-}
|
||||
SERVERPOD_PASSWORD_githubClientSecret: ${GITHUB_CLIENT_SECRET:-}
|
||||
# Where the source of what this instance runs is; empty: the project's.
|
||||
SOURCE_URL: ${SOURCE_URL:-}
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
|
|
|
|||
|
|
@ -144,7 +144,8 @@ class EndpointInstance extends _isc.EndpointRef {
|
|||
@override
|
||||
String get name => 'instance';
|
||||
|
||||
/// This instance's offer; GitHub sign-in only with the operator's OAuth app.
|
||||
/// This instance's offer: GitHub sign-in only with the operator's OAuth
|
||||
/// app, and where its source is.
|
||||
_ida.Future<_i26u8vl4.InstanceInfo> info() =>
|
||||
caller.callServerEndpoint<_i26u8vl4.InstanceInfo>(
|
||||
'instance',
|
||||
|
|
|
|||
|
|
@ -16,13 +16,20 @@ import 'package:serverpod_client/serverpod_client.dart' as _isc;
|
|||
/// someone else, with its own configuration.
|
||||
abstract class InstanceInfo
|
||||
implements _isc.SerializableModel, _isc.ProtocolSerialization {
|
||||
InstanceInfo._({this.gitHubClientId});
|
||||
InstanceInfo._({
|
||||
this.gitHubClientId,
|
||||
required this.sourceCodeUrl,
|
||||
});
|
||||
|
||||
factory InstanceInfo({String? gitHubClientId}) = _InstanceInfoImpl;
|
||||
factory InstanceInfo({
|
||||
String? gitHubClientId,
|
||||
required String sourceCodeUrl,
|
||||
}) = _InstanceInfoImpl;
|
||||
|
||||
factory InstanceInfo.fromJson(Map<String, dynamic> jsonSerialization) {
|
||||
return InstanceInfo(
|
||||
gitHubClientId: jsonSerialization['gitHubClientId'] as String?,
|
||||
sourceCodeUrl: jsonSerialization['sourceCodeUrl'] as String,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -30,15 +37,23 @@ abstract class InstanceInfo
|
|||
/// sign-in is off here.
|
||||
String? gitHubClientId;
|
||||
|
||||
/// Where the source of what this instance runs is; the licences require
|
||||
/// offering it to the people who use it.
|
||||
String sourceCodeUrl;
|
||||
|
||||
/// Returns a shallow copy of this [InstanceInfo]
|
||||
/// with some or all fields replaced by the given arguments.
|
||||
@_isc.useResult
|
||||
InstanceInfo copyWith({String? gitHubClientId});
|
||||
InstanceInfo copyWith({
|
||||
String? gitHubClientId,
|
||||
String? sourceCodeUrl,
|
||||
});
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'__className__': 'InstanceInfo',
|
||||
if (gitHubClientId != null) 'gitHubClientId': gitHubClientId,
|
||||
'sourceCodeUrl': sourceCodeUrl,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -47,6 +62,7 @@ abstract class InstanceInfo
|
|||
return {
|
||||
'__className__': 'InstanceInfo',
|
||||
if (gitHubClientId != null) 'gitHubClientId': gitHubClientId,
|
||||
'sourceCodeUrl': sourceCodeUrl,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -59,18 +75,27 @@ abstract class InstanceInfo
|
|||
class _Undefined {}
|
||||
|
||||
class _InstanceInfoImpl extends InstanceInfo {
|
||||
_InstanceInfoImpl({String? gitHubClientId})
|
||||
: super._(gitHubClientId: gitHubClientId);
|
||||
_InstanceInfoImpl({
|
||||
String? gitHubClientId,
|
||||
required String sourceCodeUrl,
|
||||
}) : super._(
|
||||
gitHubClientId: gitHubClientId,
|
||||
sourceCodeUrl: sourceCodeUrl,
|
||||
);
|
||||
|
||||
/// Returns a shallow copy of this [InstanceInfo]
|
||||
/// with some or all fields replaced by the given arguments.
|
||||
@_isc.useResult
|
||||
@override
|
||||
InstanceInfo copyWith({Object? gitHubClientId = _Undefined}) {
|
||||
InstanceInfo copyWith({
|
||||
Object? gitHubClientId = _Undefined,
|
||||
String? sourceCodeUrl,
|
||||
}) {
|
||||
return InstanceInfo(
|
||||
gitHubClientId: gitHubClientId is String?
|
||||
? gitHubClientId
|
||||
: this.gitHubClientId,
|
||||
sourceCodeUrl: sourceCodeUrl ?? this.sourceCodeUrl,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,13 +16,20 @@ import 'package:serverpod/serverpod.dart' as _is;
|
|||
/// someone else, with its own configuration.
|
||||
abstract class InstanceInfo
|
||||
implements _is.SerializableModel, _is.ProtocolSerialization {
|
||||
InstanceInfo._({this.gitHubClientId});
|
||||
InstanceInfo._({
|
||||
this.gitHubClientId,
|
||||
required this.sourceCodeUrl,
|
||||
});
|
||||
|
||||
factory InstanceInfo({String? gitHubClientId}) = _InstanceInfoImpl;
|
||||
factory InstanceInfo({
|
||||
String? gitHubClientId,
|
||||
required String sourceCodeUrl,
|
||||
}) = _InstanceInfoImpl;
|
||||
|
||||
factory InstanceInfo.fromJson(Map<String, dynamic> jsonSerialization) {
|
||||
return InstanceInfo(
|
||||
gitHubClientId: jsonSerialization['gitHubClientId'] as String?,
|
||||
sourceCodeUrl: jsonSerialization['sourceCodeUrl'] as String,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -30,15 +37,23 @@ abstract class InstanceInfo
|
|||
/// sign-in is off here.
|
||||
String? gitHubClientId;
|
||||
|
||||
/// Where the source of what this instance runs is; the licences require
|
||||
/// offering it to the people who use it.
|
||||
String sourceCodeUrl;
|
||||
|
||||
/// Returns a shallow copy of this [InstanceInfo]
|
||||
/// with some or all fields replaced by the given arguments.
|
||||
@_is.useResult
|
||||
InstanceInfo copyWith({String? gitHubClientId});
|
||||
InstanceInfo copyWith({
|
||||
String? gitHubClientId,
|
||||
String? sourceCodeUrl,
|
||||
});
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'__className__': 'InstanceInfo',
|
||||
if (gitHubClientId != null) 'gitHubClientId': gitHubClientId,
|
||||
'sourceCodeUrl': sourceCodeUrl,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -47,6 +62,7 @@ abstract class InstanceInfo
|
|||
return {
|
||||
'__className__': 'InstanceInfo',
|
||||
if (gitHubClientId != null) 'gitHubClientId': gitHubClientId,
|
||||
'sourceCodeUrl': sourceCodeUrl,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -59,18 +75,27 @@ abstract class InstanceInfo
|
|||
class _Undefined {}
|
||||
|
||||
class _InstanceInfoImpl extends InstanceInfo {
|
||||
_InstanceInfoImpl({String? gitHubClientId})
|
||||
: super._(gitHubClientId: gitHubClientId);
|
||||
_InstanceInfoImpl({
|
||||
String? gitHubClientId,
|
||||
required String sourceCodeUrl,
|
||||
}) : super._(
|
||||
gitHubClientId: gitHubClientId,
|
||||
sourceCodeUrl: sourceCodeUrl,
|
||||
);
|
||||
|
||||
/// Returns a shallow copy of this [InstanceInfo]
|
||||
/// with some or all fields replaced by the given arguments.
|
||||
@_is.useResult
|
||||
@override
|
||||
InstanceInfo copyWith({Object? gitHubClientId = _Undefined}) {
|
||||
InstanceInfo copyWith({
|
||||
Object? gitHubClientId = _Undefined,
|
||||
String? sourceCodeUrl,
|
||||
}) {
|
||||
return InstanceInfo(
|
||||
gitHubClientId: gitHubClientId is String?
|
||||
? gitHubClientId
|
||||
: this.gitHubClientId,
|
||||
sourceCodeUrl: sourceCodeUrl ?? this.sourceCodeUrl,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,16 @@
|
|||
import 'package:halkyon_learn_server/src/auth/github.dart';
|
||||
import 'package:halkyon_learn_server/src/generated/protocol.dart';
|
||||
import 'package:halkyon_learn_server/src/instance/source_code.dart';
|
||||
import 'package:serverpod/serverpod.dart';
|
||||
|
||||
/// What this instance offers. Public: the app asks before anyone is signed in.
|
||||
class InstanceEndpoint extends Endpoint {
|
||||
/// This instance's offer; GitHub sign-in only with the operator's OAuth app.
|
||||
/// This instance's offer: GitHub sign-in only with the operator's OAuth
|
||||
/// app, and where its source is.
|
||||
Future<InstanceInfo> info(Session session) async => InstanceInfo(
|
||||
gitHubClientId: gitHubSignInAvailable(session.serverpod)
|
||||
? session.serverpod.getPassword('githubClientId')
|
||||
: null,
|
||||
sourceCodeUrl: sourceCodeUrl(),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,3 +5,6 @@ fields:
|
|||
### The client ID of this instance's GitHub OAuth app, or null when GitHub
|
||||
### sign-in is off here.
|
||||
gitHubClientId: String?
|
||||
### Where the source of what this instance runs is; the licences require
|
||||
### offering it to the people who use it.
|
||||
sourceCodeUrl: String
|
||||
|
|
|
|||
14
halkyon_learn_server/lib/src/instance/source_code.dart
Normal file
14
halkyon_learn_server/lib/src/instance/source_code.dart
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import 'dart:io';
|
||||
|
||||
/// Where this project's source is published.
|
||||
const upstreamSourceCodeUrl =
|
||||
'https://code.halkyon.app/project-halkyon/halkyon_learn';
|
||||
|
||||
/// Where the source of what this instance runs is: `SOURCE_URL` if the
|
||||
/// operator set it, which they must when running modified code, else this
|
||||
/// project's. 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).
|
||||
String sourceCodeUrl() => switch (Platform.environment['SOURCE_URL']) {
|
||||
final url? when url.isNotEmpty => url,
|
||||
_ => upstreamSourceCodeUrl,
|
||||
};
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
import 'package:halkyon_learn_server/src/instance/source_code.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
import '../test_tools/serverpod_test_tools.dart';
|
||||
|
||||
void main() {
|
||||
withServerpod('What the instance offers', (sessionBuilder, endpoints) {
|
||||
test('names this repository as the source when the operator set '
|
||||
'nothing', () async {
|
||||
// The tests run without SOURCE_URL.
|
||||
final info = await endpoints.instance.info(
|
||||
sessionBuilder.copyWith(
|
||||
authentication: AuthenticationOverride.unauthenticated(),
|
||||
),
|
||||
);
|
||||
|
||||
expect(info.sourceCodeUrl, upstreamSourceCodeUrl);
|
||||
});
|
||||
});
|
||||
}
|
||||
Loading…
Reference in a new issue