Skip to content

Commit

Permalink
feat(content-service): add registration notification
Browse files Browse the repository at this point in the history
Also include a content type as reference.
  • Loading branch information
tzuge committed Nov 13, 2024
1 parent 8f1a28f commit 8b836a1
Show file tree
Hide file tree
Showing 13 changed files with 206 additions and 210 deletions.
4 changes: 1 addition & 3 deletions .openshift/managed/content-service.yml
Original file line number Diff line number Diff line change
Expand Up @@ -104,8 +104,6 @@ objects:
env:
- name: PORT
value: "3333"
- name: DATABASE_FILENAME
value: /opt/app-root/data/data.db
imagePullPolicy: IfNotPresent
name: content-service
ports:
Expand All @@ -123,7 +121,7 @@ objects:
terminationMessagePolicy: File
volumeMounts:
- name: content-service-data
mountPath: /opt/app-root/data
mountPath: /opt/app-root/src/apps/content-service/.tmp
# readinessProbe:
# httpGet:
# path: /
Expand Down
12 changes: 9 additions & 3 deletions apps/content-service/src/adsp-strapi/server/controllers/user.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { EventService } from '@abgov/adsp-service-sdk';
import { adspId, type EventService, type ServiceDirectory } from '@abgov/adsp-service-sdk';
import type { Core } from '@strapi/strapi';
import { errors, yup, validateYupSchema } from '@strapi/utils';
import type { Context } from 'koa';
Expand Down Expand Up @@ -43,13 +43,19 @@ const user = ({ strapi }: { strapi: Core.Strapi }) => ({
ctx.created({ data: { ...userInfo, registrationToken: createdUser.registrationToken } });

// Complete registration at /admin/auth/register?registrationToken=<registrationToken>
const eventService = await strapi.service('plugin::adsp-strapi.eventService') as EventService;
const directory = (await strapi.service('plugin::adsp-strapi.directory')) as ServiceDirectory;
const contentServiceUrl = await directory.getServiceUrl(adspId`urn:ads:platform:content-service`);

const eventService = (await strapi.service('plugin::adsp-strapi.eventService')) as EventService;
eventService.send(
userRegistered(tenantId, {
email,
firstName,
lastName,
registrationToken: createdUser.registrationToken,
registrationUrl: new URL(
`/admin/auth/register?registrationToken=${createdUser.registrationToken}`,
contentServiceUrl,
).href,
isEditor: !!isEditor,
}),
);
Expand Down
8 changes: 4 additions & 4 deletions apps/content-service/src/adsp-strapi/server/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export const UserRegisteredEventDefinition: DomainEventDefinition = {
email: { type: 'string' },
firstName: { type: 'string' },
lastName: { type: 'string' },
registrationToken: { type: 'string' },
registrationUrl: { type: 'string' },
isEditor: { type: 'boolean' },
},
},
Expand All @@ -19,13 +19,13 @@ interface NewUser {
email: string;
firstName: string;
lastName: string;
registrationToken: string;
registrationUrl: string;
isEditor: boolean;
}

export const userRegistered = (
tenantId: AdspId,
{ email, firstName, lastName, registrationToken, isEditor }: NewUser,
{ email, firstName, lastName, registrationUrl, isEditor }: NewUser,
): DomainEvent => ({
tenantId,
name: UserRegisteredEventDefinition.name,
Expand All @@ -34,7 +34,7 @@ export const userRegistered = (
email,
firstName,
lastName,
registrationToken,
registrationUrl,
isEditor,
},
});
30 changes: 30 additions & 0 deletions apps/content-service/src/adsp-strapi/server/notifications.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Channel, NotificationType } from '@abgov/adsp-service-sdk';
import { UserRegisteredEventDefinition } from './events';

const CONTENT_EVENT_NAMESPACE = 'content-service';
export const UserRegistrationInvite: NotificationType = {
name: 'user-registration-invite',
displayName: 'User registration invite',
description: 'Provides notification to user to complete their content manager registration.',
publicSubscribe: false,
subscriberRoles: [],
channels: [Channel.email],
addressPath: 'email',
events: [
{
namespace: CONTENT_EVENT_NAMESPACE,
name: UserRegisteredEventDefinition.name,
templates: {
email: {
subject: 'Welcome to content service {{ event.payload.firstName }}!',
body: `
<section>
<p>You're invited to be an {{#if event.payload.isEditor}}editor{{else}}author{{/if}} in content service.</p>
<p>Click on the following link to complete your registration: <a href="{{ event.payload.registrationUrl }}">{{ event.payload.registrationUrl }}</a></p>
</section>
`,
},
},
},
],
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import type { Core, UID } from '@strapi/strapi';
import type { Request } from 'koa';

export async function applyRequestTenant(
strapi: Core.Strapi,
request: Request,
tenantId: string,
model: UID.ContentType,
documentId: string,
) {
let documentTenant: string;
if (tenantId) {
switch (request.method) {
case 'POST': {
// This is a create request, so set the tenantId in the data.
request.body.tenantId = tenantId;
break;
}
case 'GET': {
// This is a read request, so...
if (documentId) {
// for specific document read, check tenancy.
const document = await strapi.documents(model).findOne({ documentId });
documentTenant = document?.['tenantId'];
} else {
// for collection reads, add a tenantId criteria to the filter.
const filters: { $and: unknown[] } = { $and: [{ tenantId }] };
if (request.query.filters) {
filters.$and.push(request.query.filters);
}
request.query.filters = filters;
}
break;
}
case 'PUT':
case 'DELETE': {
// This is an update or delete request, so we need to verify user access to the associated content.
const document = await strapi.documents(model).findOne({ documentId });
documentTenant = document?.['tenantId'];
break;
}
default:
break;
}
}

return documentTenant;
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { Core, UID } from '@strapi/strapi';
import { applyRequestTenant } from './apply-request-tenant';

/**
* Policy that enforces user tenant context on content manager requests for authoring and publishing content.
Expand All @@ -15,51 +16,21 @@ const isTenantContentManager = async (
) => {
const { model, id: documentId }: { model: UID.ContentType; id?: string } = policyContext['params'];

// This is the user object based on the built-in admin strategy.
const user = policyContext['state']?.auth?.credentials;
try {
// This is the user object based on the built-in admin strategy.
const user = policyContext['state']?.auth?.credentials;

const request = policyContext.request;
const tenantId = user?.tenantId?.toString();
let requestedTenantId: string;
if (tenantId) {
switch (request.method) {
case 'POST': {
// This is a create request, so set the tenantId in the data.
request.body.tenantId = tenantId;
break;
}
case 'GET': {
// This is a read request, so...
if (documentId) {
// for specific document read, check tenancy.
const document = await strapi.documents(model).findOne({ documentId });
requestedTenantId = document?.['tenantId'];
} else {
// for collection reads, add a tenantId criteria to the filter.
const filters: { $and: unknown[] } = { $and: [{ tenantId }] };
if (request.query.filters) {
filters.$and.push(request.query.filters);
}
request.query.filters = filters;
}
break;
}
case 'PUT':
case 'DELETE': {
// This is an update or delete request, so we need to verify user access to the associated content.
const document = await strapi.documents(model).findOne({ documentId });
requestedTenantId = document?.['tenantId'];
break;
}
default:
break;
}
}
const request = policyContext.request;
const tenantId = user?.tenantId?.toString();
const requestedTenantId: string = await applyRequestTenant(strapi, request, tenantId, model, documentId);

// Pass if user isn't in a tenant context, or
// requested document isn't in a tenant context, or
// tenant contexts are the same.
return !tenantId || !requestedTenantId || tenantId === requestedTenantId;
// Pass if user isn't in a tenant context, or
// requested document isn't in a tenant context, or
// tenant contexts are the same.
return !tenantId || !requestedTenantId || tenantId === requestedTenantId;
} catch (err) {
return false;
}
};

export default isTenantContentManager;
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { AdspId, isAllowedUser } from '@abgov/adsp-service-sdk';
import type { Core, UID } from '@strapi/strapi';
import type { Core } from '@strapi/strapi';
import { ServiceRoles } from '../roles';
import { applyRequestTenant } from './apply-request-tenant';

/**
* Policy that enforces user tenant context on content API requests.
Expand All @@ -22,39 +23,7 @@ const isTenantUserWithRole = async (policyContext: Core.PolicyContext, config, {

const user = policyContext['state']?.auth?.credentials;
const tenantId = user?.tenantId;
let requestedTenantId: string;
switch (request.method) {
case 'POST': {
// This is a create request, so set the tenantId in the data.
request.body.tenantId = tenantId;
break;
}
case 'GET': {
// This is a read request, so...
if (documentId) {
// for specific document read, check tenancy.
const document = await strapi.documents(model as UID.ContentType).findOne({ documentId });
requestedTenantId = document?.['tenantId'];
} else {
// for collection reads, add a tenantId criteria to the filter.
const filters: { $and: unknown[] } = { $and: [{ tenantId }] };
if (request.query.filters) {
filters.$and.push(request.query.filters);
}
request.query.filters = filters;
}
break;
}
case 'PUT':
case 'DELETE': {
// This is an update or delete request, so we need to verify user access to the associated content.
const document = await strapi.documents(model as UID.ContentType).findOne({ documentId });
requestedTenantId = document?.['tenantId'];
break;
}
default:
break;
}
const requestedTenantId: string = await applyRequestTenant(strapi, request, tenantId, model, documentId);

return isAllowedUser(user, requestedTenantId ? AdspId.parse(requestedTenantId) : null, [
ServiceRoles.Admin,
Expand Down
2 changes: 2 additions & 0 deletions apps/content-service/src/adsp-strapi/server/register.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { ADSP_SERVICE_NAME } from './constants';
import { UserRegisteredEventDefinition } from './events';
import { ServiceRoles } from './roles';
import getStrategies from './strategies';
import { UserRegistrationInvite } from './notifications';

const register = async ({ strapi }: { strapi: Core.Strapi }) => {
const capabilities = await initializePlatform(
Expand All @@ -19,6 +20,7 @@ const register = async ({ strapi }: { strapi: Core.Strapi }) => {
{ role: ServiceRoles.Reader, description: 'Reader role permitted to access content via the content API.' },
],
events: [UserRegisteredEventDefinition],
notifications: [UserRegistrationInvite]
},
{ logger: strapi.log },
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
{
"kind": "collectionType",
"collectionName": "service_releases",
"info": {
"singularName": "service-release",
"pluralName": "service-releases",
"displayName": "Service release",
"description": ""
},
"options": {
"draftAndPublish": true
},
"pluginOptions": {},
"attributes": {
"tenantId": {
"type": "string"
},
"service": {
"type": "string",
"required": true
},
"releasedOn": {
"type": "date",
"required": true
},
"notes": {
"type": "blocks",
"required": true
},
"version": {
"type": "string",
"required": true
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/**
* service-release controller
*/

import { factories } from '@strapi/strapi'

export default factories.createCoreController('api::service-release.service-release');
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/**
* service-release router
*/

import { factories } from '@strapi/strapi';

const model = 'api::service-release.service-release';
export default factories.createCoreRouter(model, {
config: {
find: { policies: [{ name: 'plugin::adsp-strapi.isTenantUserWithRole', config: { model } }] },
findOne: { policies: [{ name: 'plugin::adsp-strapi.isTenantUserWithRole', config: { model } }] },
create: { policies: [{ name: 'plugin::adsp-strapi.isTenantUserWithRole', config: { model } }] },
update: { policies: [{ name: 'plugin::adsp-strapi.isTenantUserWithRole', config: { model } }] },
delete: { policies: [{ name: 'plugin::adsp-strapi.isTenantUserWithRole', config: { model } }] },
},
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/**
* service-release service
*/

import { factories } from '@strapi/strapi';

export default factories.createCoreService('api::service-release.service-release');
Loading

0 comments on commit 8b836a1

Please sign in to comment.