T04.f: backend Festipod ActivityPods + dockerisation
- Ajoute la configuration et les services backend (app, actors, registration, attendees, contacts, catalog, core auth/nodeinfo/proxy/notification). - Corrige le démarrage : désactive resourcesWithContainerPath pour éviter le conflit entre l'URI de l'acteur applicatif (/as/actor/app) et la route /app. - Corrige l'URL du dépôt de shapes : https://shapes.activitypods.org/. - Ajoute backend/Dockerfile et docker-compose.yml racine (frontend + backend). - Met à jour .env.example racine avec les variables SEMAPPS_* et VITE_BACKEND_*. - Gates verts : bunx tsc --noEmit (0 erreur) et bun test (50 pass).
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
const urlJoin = require('url-join');
|
||||
const { AppService } = require('@activitypods/app');
|
||||
const CONFIG = require('../config/config');
|
||||
|
||||
module.exports = {
|
||||
mixins: [AppService],
|
||||
settings: {
|
||||
baseUrl: CONFIG.HOME_URL,
|
||||
app: {
|
||||
name: CONFIG.APP_NAME,
|
||||
description: CONFIG.APP_DESCRIPTION,
|
||||
thumbnail: urlJoin(CONFIG.FRONT_URL, 'logo.svg'),
|
||||
frontUrl: CONFIG.FRONT_URL,
|
||||
supportedLocales: CONFIG.APP_LANG
|
||||
},
|
||||
oidc: {
|
||||
clientUri: CONFIG.FRONT_URL,
|
||||
redirectUris: urlJoin(CONFIG.FRONT_URL, 'login'),
|
||||
postLogoutRedirectUris: urlJoin(CONFIG.FRONT_URL, 'login?logout=true'),
|
||||
tosUri: null
|
||||
},
|
||||
accessNeeds: {
|
||||
required: [
|
||||
{
|
||||
shapeTreeUri: urlJoin(CONFIG.SHAPE_REPOSITORY_URL, 'shapetrees/as/Event'),
|
||||
accessMode: ['acl:Read', 'acl:Write', 'acl:Control'],
|
||||
preferredScope: 'interop:AllFromRegistry'
|
||||
},
|
||||
{
|
||||
shapeTreeUri: urlJoin(CONFIG.SHAPE_REPOSITORY_URL, 'shapetrees/as/Profile'),
|
||||
accessMode: ['acl:Read', 'acl:Write'],
|
||||
preferredScope: 'interop:AllFromRegistry'
|
||||
},
|
||||
{
|
||||
shapeTreeUri: urlJoin(CONFIG.SHAPE_REPOSITORY_URL, 'shapetrees/vcard/Group'),
|
||||
accessMode: 'acl:Read',
|
||||
preferredScope: 'interop:AllFromRegistry'
|
||||
},
|
||||
'apods:ReadInbox',
|
||||
'apods:ReadOutbox',
|
||||
'apods:PostOutbox',
|
||||
'apods:QuerySparqlEndpoint',
|
||||
'apods:CreateWacGroup',
|
||||
'apods:CreateCollection',
|
||||
'apods:UpdateWebId'
|
||||
],
|
||||
optional: []
|
||||
},
|
||||
queueServiceUrl: CONFIG.QUEUE_SERVICE_URL
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
const urlJoin = require('url-join');
|
||||
const { PodCollectionsHandlerMixin } = require('@activitypods/app');
|
||||
const CONFIG = require('../config/config');
|
||||
|
||||
module.exports = {
|
||||
name: 'attendees',
|
||||
mixins: [PodCollectionsHandlerMixin],
|
||||
settings: {
|
||||
shapeTreeUri: urlJoin(CONFIG.SHAPE_REPOSITORY_URL, 'shapetrees/as/Event'),
|
||||
attachPredicate: 'http://activitypods.org/ns/core#attendees',
|
||||
collectionOptions: {
|
||||
ordered: false,
|
||||
summary: 'Meeting point attendees'
|
||||
},
|
||||
createWacGroup: true
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,41 @@
|
||||
const urlJoin = require('url-join');
|
||||
const { PodResourcesHandlerMixin } = require('@activitypods/app');
|
||||
const CONFIG = require('../config/config');
|
||||
|
||||
module.exports = {
|
||||
name: 'catalog',
|
||||
mixins: [PodResourcesHandlerMixin],
|
||||
settings: {
|
||||
shapeTreeUri: urlJoin(CONFIG.SHAPE_REPOSITORY_URL, 'shapetrees/as/Event')
|
||||
},
|
||||
actions: {
|
||||
discover: {
|
||||
async handler() {
|
||||
return {
|
||||
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||
type: 'Collection',
|
||||
summary: 'Public Festipod events and meeting points',
|
||||
totalItems: this.index ? Object.keys(this.index).length : 0,
|
||||
items: this.index ? Object.values(this.index) : []
|
||||
};
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async onCreate(ctx, resource) {
|
||||
if (!this.index) this.index = {};
|
||||
this.index[resource.id || resource['@id']] = resource;
|
||||
this.logger.info(`Catalog indexed new event ${resource.id || resource['@id']}`);
|
||||
},
|
||||
async onUpdate(ctx, resource) {
|
||||
if (!this.index) this.index = {};
|
||||
this.index[resource.id || resource['@id']] = resource;
|
||||
this.logger.info(`Catalog updated event ${resource.id || resource['@id']}`);
|
||||
},
|
||||
async onDelete(ctx, resourceUri) {
|
||||
if (!this.index) return;
|
||||
delete this.index[resourceUri];
|
||||
this.logger.info(`Catalog removed event ${resourceUri}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
const urlJoin = require('url-join');
|
||||
const { PodCollectionsHandlerMixin } = require('@activitypods/app');
|
||||
const CONFIG = require('../config/config');
|
||||
|
||||
module.exports = {
|
||||
name: 'contacts',
|
||||
mixins: [PodCollectionsHandlerMixin],
|
||||
settings: {
|
||||
shapeTreeUri: urlJoin(CONFIG.SHAPE_REPOSITORY_URL, 'shapetrees/as/Profile'),
|
||||
attachPredicate: 'http://activitypods.org/ns/core#contacts',
|
||||
collectionOptions: {
|
||||
ordered: false,
|
||||
summary: 'Festipod contacts'
|
||||
},
|
||||
createWacGroup: true
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,8 @@
|
||||
const { AuthAccountService } = require('@semapps/auth');
|
||||
const { TripleStoreAdapter } = require('@semapps/triplestore');
|
||||
const CONFIG = require('../../config/config');
|
||||
|
||||
module.exports = {
|
||||
mixins: [AuthAccountService],
|
||||
adapter: new TripleStoreAdapter({ type: 'AuthAccount', dataset: CONFIG.AUTH_ACCOUNTS_DATASET_NAME })
|
||||
};
|
||||
@@ -0,0 +1,53 @@
|
||||
const path = require('path');
|
||||
const { CoreService } = require('@semapps/core');
|
||||
const { apods, notify, interop, oidc, skos } = require('@semapps/ontologies');
|
||||
const CONFIG = require('../../config/config');
|
||||
|
||||
module.exports = {
|
||||
mixins: [CoreService],
|
||||
settings: {
|
||||
baseUrl: CONFIG.HOME_URL,
|
||||
baseDir: path.resolve(__dirname, '../..'),
|
||||
triplestore: {
|
||||
url: CONFIG.SPARQL_ENDPOINT,
|
||||
user: CONFIG.JENA_USER,
|
||||
password: CONFIG.JENA_PASSWORD,
|
||||
mainDataset: CONFIG.MAIN_DATASET
|
||||
},
|
||||
ontologies: [apods, notify, interop, oidc, skos],
|
||||
activitypub: {
|
||||
queueServiceUrl: CONFIG.QUEUE_SERVICE_URL
|
||||
},
|
||||
api: {
|
||||
port: CONFIG.PORT,
|
||||
routes: [
|
||||
{
|
||||
path: '/app',
|
||||
aliases: {
|
||||
'GET /': 'app.get'
|
||||
},
|
||||
mappingPolicy: 'restrict',
|
||||
authentication: false,
|
||||
authorization: false,
|
||||
bodyParsers: {
|
||||
json: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/catalog',
|
||||
aliases: {
|
||||
'GET /': 'catalog.discover'
|
||||
},
|
||||
mappingPolicy: 'restrict',
|
||||
authentication: false,
|
||||
authorization: false,
|
||||
bodyParsers: {
|
||||
json: true
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
webacl: false,
|
||||
void: false
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
const urlJoin = require('url-join');
|
||||
const { NodeinfoService } = require('@semapps/nodeinfo');
|
||||
const CONFIG = require('../../config/config');
|
||||
|
||||
module.exports = {
|
||||
mixins: [NodeinfoService],
|
||||
settings: {
|
||||
baseUrl: CONFIG.HOME_URL,
|
||||
software: {
|
||||
name: 'festipod'
|
||||
},
|
||||
protocols: ['activitypub'],
|
||||
metadata: {
|
||||
frontend_url: CONFIG.FRONT_URL,
|
||||
login_url: CONFIG.FRONT_URL && urlJoin(CONFIG.FRONT_URL, 'login'),
|
||||
logout_url: CONFIG.FRONT_URL && urlJoin(CONFIG.FRONT_URL, 'login?logout=true')
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
async getUsersCount(ctx) {
|
||||
const containerUri = await ctx.call('app-registrations.getContainerUri');
|
||||
const registrationsUris = await ctx.call('ldp.container.getUris', { containerUri });
|
||||
return {
|
||||
total: registrationsUris.length,
|
||||
activeHalfYear: registrationsUris.length,
|
||||
activeMonth: registrationsUris.length
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,11 @@
|
||||
const { TripleStoreAdapter } = require('@semapps/triplestore');
|
||||
const { NotificationsListenerService } = require('@semapps/solid');
|
||||
const CONFIG = require('../../config/config');
|
||||
|
||||
module.exports = {
|
||||
mixins: [NotificationsListenerService],
|
||||
adapter: new TripleStoreAdapter({ type: 'WebhookChannelListener', dataset: CONFIG.AUTH_ACCOUNTS_DATASET_NAME }),
|
||||
settings: {
|
||||
baseUrl: CONFIG.HOME_URL
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,3 @@
|
||||
const { ProxyService } = require('@semapps/crypto');
|
||||
|
||||
module.exports = ProxyService;
|
||||
@@ -0,0 +1,97 @@
|
||||
const { ACTIVITY_TYPES, OBJECT_TYPES } = require('@semapps/activitypub');
|
||||
const { PodActivitiesHandlerMixin } = require('@activitypods/app');
|
||||
const { arrayOf } = require('@semapps/ldp');
|
||||
|
||||
module.exports = {
|
||||
name: 'registration',
|
||||
mixins: [PodActivitiesHandlerMixin],
|
||||
async started() {
|
||||
this.logger.info('Registration watcher subscribed');
|
||||
},
|
||||
activities: {
|
||||
join: {
|
||||
match: {
|
||||
type: ACTIVITY_TYPES.JOIN,
|
||||
object: {
|
||||
type: OBJECT_TYPES.EVENT
|
||||
}
|
||||
},
|
||||
async onEmit(ctx, activity, actorUri) {
|
||||
const event = activity.object;
|
||||
const organizerUri = event['dc:creator'];
|
||||
if (!organizerUri) return;
|
||||
|
||||
const { body: actor } = await ctx.call('pod-resources.get', { resourceUri: actorUri, actorUri });
|
||||
if (!actor || !actor['apods:contacts']) return;
|
||||
|
||||
const contactsGroupUri = await ctx.call('pod-wac-groups.getUriFromCollectionUri', {
|
||||
collectionUri: actor['apods:contacts']
|
||||
});
|
||||
|
||||
await ctx.call('pod-wac-groups.addMember', {
|
||||
groupUri: contactsGroupUri,
|
||||
memberUri: organizerUri,
|
||||
actorUri
|
||||
});
|
||||
},
|
||||
async onReceive(ctx, activity, actorUri) {
|
||||
const event = activity.object;
|
||||
const attendeesCollectionUri = event['apods:attendees'];
|
||||
|
||||
if (!attendeesCollectionUri) {
|
||||
this.logger.warn(`No attendees collection on ${event.id}, skipping Join`);
|
||||
return;
|
||||
}
|
||||
|
||||
const attendees = await ctx.call('pod-collections.getItems', {
|
||||
collectionUri: attendeesCollectionUri,
|
||||
actorUri
|
||||
});
|
||||
|
||||
if (arrayOf(attendees).includes(activity.actor)) {
|
||||
this.logger.info(`${activity.actor} is already attending ${event.id}`);
|
||||
return;
|
||||
}
|
||||
|
||||
await ctx.call('attendees.add', {
|
||||
collectionUri: attendeesCollectionUri,
|
||||
itemUri: activity.actor,
|
||||
actorUri
|
||||
});
|
||||
}
|
||||
},
|
||||
leave: {
|
||||
match: {
|
||||
type: ACTIVITY_TYPES.LEAVE,
|
||||
object: {
|
||||
type: OBJECT_TYPES.EVENT
|
||||
}
|
||||
},
|
||||
async onReceive(ctx, activity, actorUri) {
|
||||
const event = activity.object;
|
||||
const attendeesCollectionUri = event['apods:attendees'];
|
||||
|
||||
if (!attendeesCollectionUri) {
|
||||
this.logger.warn(`No attendees collection on ${event.id}, skipping Leave`);
|
||||
return;
|
||||
}
|
||||
|
||||
const attendees = await ctx.call('pod-collections.getItems', {
|
||||
collectionUri: attendeesCollectionUri,
|
||||
actorUri
|
||||
});
|
||||
|
||||
if (!arrayOf(attendees).includes(activity.actor)) {
|
||||
this.logger.info(`${activity.actor} is not attending ${event.id}, skipping Leave`);
|
||||
return;
|
||||
}
|
||||
|
||||
await ctx.call('attendees.remove', {
|
||||
collectionUri: attendeesCollectionUri,
|
||||
itemUri: activity.actor,
|
||||
actorUri
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user