Skip to content

Commit

Permalink
Merge pull request #83 from hyphacoop/trial-publisher
Browse files Browse the repository at this point in the history
Add trial publisher API
  • Loading branch information
RangerMauve authored Oct 21, 2024
2 parents a020f7c + 772ed30 commit 594ab1f
Show file tree
Hide file tree
Showing 5 changed files with 108 additions and 6 deletions.
35 changes: 35 additions & 0 deletions api/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,3 +161,38 @@ test('E2E: admin -> publisher -> site flow', async t => {
})
t.is(deleteSiteResponse.statusCode, 200, 'deleting site returns a status code of 200')
})

test('Create trial publisher', async t => {
// create an admin
const trialResponse = await t.context.server.inject({
method: 'POST',
url: '/v1/publisher/trial',
payload: {
name: '[email protected]'
}
})
t.is(trialResponse.statusCode, 200, 'creating trial returns OK')
const token = trialResponse.body

// use access token to create a new site
const createSiteResponse = await t.context.server.inject({
method: 'POST',
url: '/v1/sites',
headers: {
authorization: `Bearer ${token}`
},
payload: exampleSiteConfig
})
t.is(createSiteResponse.statusCode, 200, 'creating a site works with trial token')

// use access token to create a new site
const createSiteResponse2 = await t.context.server.inject({
method: 'POST',
url: '/v1/sites',
headers: {
authorization: `Bearer ${token}`
},
payload: { ...exampleSiteConfig, domain: 'example2' }
})
t.is(createSiteResponse2.statusCode, 402, 'Error when creating second site')
})
24 changes: 24 additions & 0 deletions api/publisher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { FastifyInstance } from 'fastify'
import { StoreI } from '../config/index.js'
import { APIConfig } from './index.js'
import { NewPublisher, Publisher } from './schemas.js'
import { makeJWTToken } from '../authorization/jwt.js'

export const publisherRoutes = (_cfg: APIConfig, store: StoreI) => async (server: FastifyInstance): Promise<void> => {
server.post<{
Expand All @@ -23,6 +24,29 @@ export const publisherRoutes = (_cfg: APIConfig, store: StoreI) => async (server
return await reply.send(await store.publisher.create(request.body))
})

server.post<{
Body: Static<typeof NewPublisher>
}>('/publisher/trial', {
schema: {
body: NewPublisher,
response: {
200: Type.String()
},
description: 'Register yourself as a new publisher',
tags: ['publisher'],
security: []
}
}, async (request, reply) => {
const account = await store.publisher.createTrial(request.body)
const newToken = makeJWTToken({
issuedTo: account.id,
isTrial: true
})
const signed = await reply.jwtSign(newToken)

return await reply.send(signed)
})

server.get<{
Params: {
id: string
Expand Down
1 change: 1 addition & 0 deletions api/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ export const UpdateSite = Type.Partial(Type.Object({
export const Publisher = Type.Object({
id: Type.String(),
name: Type.String(),
limited: Type.Optional(Type.Boolean()),
ownedSites: Type.Array(Type.String()) // array of IDs of sites
})
export const NewPublisher = Type.Omit(Publisher, ['id', 'ownedSites'])
Expand Down
17 changes: 11 additions & 6 deletions authorization/jwt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export enum CAPABILITIES {
ADMIN = 'admin',
PUBLISHER = 'publisher',
REFRESH = 'refresh',
TRIAL = 'trial',
}
export const CAPABILITIES_ARRAY = Type.Array(Type.Enum(CAPABILITIES))

Expand All @@ -29,8 +30,9 @@ export function getExpiry (isRefresh: boolean): number {
}

interface JWTArgs {
isAdmin: boolean
isRefresh: boolean
isAdmin?: boolean
isRefresh?: boolean
isTrial?: boolean
issuedTo?: string
}

Expand All @@ -43,18 +45,21 @@ export const JWTPayload = Type.Object({

export type JWTPayloadT = Static<typeof JWTPayload>

export function makeJWTToken ({ isAdmin, isRefresh, issuedTo }: JWTArgs): JWTPayloadT {
export function makeJWTToken ({ isAdmin, isTrial, isRefresh, issuedTo }: JWTArgs): JWTPayloadT {
const baseCapabilities = [CAPABILITIES.PUBLISHER]
if (isAdmin) {
if (isAdmin === true) {
baseCapabilities.push(CAPABILITIES.ADMIN)
}
if (isRefresh) {
if (isRefresh === true) {
baseCapabilities.push(CAPABILITIES.REFRESH)
}
if (isTrial === true) {
baseCapabilities.push(CAPABILITIES.TRIAL)
}
return {
tokenId: nanoid(),
issuedTo: issuedTo ?? SYSTEM,
expires: getExpiry(isRefresh),
expires: getExpiry(isRefresh === true),
capabilities: baseCapabilities
}
}
Expand Down
37 changes: 37 additions & 0 deletions config/publisher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { NewPublisher, Publisher } from '../api/schemas'
import { Static } from '@sinclair/typebox'
import { Config } from './store.js'
import { nanoid } from 'nanoid'
import createError from 'http-errors'

export class PublisherStore extends Config<Static<typeof Publisher>> {
async create (cfg: Static<typeof NewPublisher>): Promise<Static<typeof Publisher>> {
Expand All @@ -14,8 +15,35 @@ export class PublisherStore extends Config<Static<typeof Publisher>> {
return await this.db.put(id, obj).then(() => obj)
}

async createTrial (cfg: Static<typeof NewPublisher>): Promise<Static<typeof Publisher>> {
const id = cfg.name

const sections = id.split('@')

if (sections.length !== 2 || !sections[1].includes('.')) {
throw createError(400, 'Name must be a valid email address.')
}

if (await this.has(id)) {
throw createError(409, 'A trial for this email already exists. Please contact an administrator if you think this is a mistake.')
}

const obj: Static<typeof Publisher> = {
id,
name: id,
ownedSites: [],
limited: true
}
return await this.db.put(id, obj).then(() => obj)
}

async registerSiteToPublisher (publisherId: string, siteId: string): Promise<void> {
const publisher = await this.db.get(publisherId)

if ((publisher.limited === true) && (publisher.ownedSites.length >= 1)) {
throw createError(402, `Your trial account already has one site registered at ${publisher.ownedSites[0]}`)
}

const newSiteList = new Set([...publisher.ownedSites, siteId])
return await this.db.put(publisherId, {
...publisher,
Expand All @@ -35,6 +63,15 @@ export class PublisherStore extends Config<Static<typeof Publisher>> {
}
}

async has (id: string): Promise<boolean> {
try {
await this.get(id)
return true
} catch {
return false
}
}

async get (id: string): Promise<Static<typeof Publisher>> {
return await this.db.get(id)
}
Expand Down

0 comments on commit 594ab1f

Please sign in to comment.