diff --git a/packages/common/src/IAPIDef.ts b/packages/common/src/IAPIDef.ts index 12271f8..c542922 100644 --- a/packages/common/src/IAPIDef.ts +++ b/packages/common/src/IAPIDef.ts @@ -5,6 +5,7 @@ import {ISite} from './ISite' import {IStory} from './IStory' import {ITeam} from './ITeam' import {IUser} from './IUser' +import {IUserTeam} from './IUserTeam' export interface IAPIDef { '/auth/register': { @@ -59,7 +60,7 @@ export interface IAPIDef { '/my/teams': { get: { - response: ITeam[] + response: IUserTeam[] } } @@ -94,6 +95,7 @@ export interface IAPIDef { } body: { name: string + domain: string } response: ISite } diff --git a/packages/common/src/IRole.ts b/packages/common/src/IRole.ts new file mode 100644 index 0000000..320eb65 --- /dev/null +++ b/packages/common/src/IRole.ts @@ -0,0 +1,4 @@ +export interface IRole { + readonly id: number + readonly name: string +} diff --git a/packages/common/src/ISite.ts b/packages/common/src/ISite.ts index e18f812..34745f8 100644 --- a/packages/common/src/ISite.ts +++ b/packages/common/src/ISite.ts @@ -1,6 +1,7 @@ export interface ISite { readonly id: number readonly name: string + readonly domain: string readonly teamId: number readonly userId: number } diff --git a/packages/common/src/IUserTeam.ts b/packages/common/src/IUserTeam.ts new file mode 100644 index 0000000..f7eb572 --- /dev/null +++ b/packages/common/src/IUserTeam.ts @@ -0,0 +1,8 @@ +import {ITeam} from './ITeam' + +export interface IUserTeam { + userId: number + teamId: number + roleId: number + team?: ITeam +} diff --git a/packages/common/src/index.ts b/packages/common/src/index.ts index 6940ecc..59df822 100644 --- a/packages/common/src/index.ts +++ b/packages/common/src/index.ts @@ -2,8 +2,9 @@ export * from './IAPIDef' export * from './IComment' export * from './ICommentTree' export * from './ICredentials' -export * from './IRoutes' export * from './IRequestParams' +export * from './IRole' +export * from './IRoutes' export * from './ISite' export * from './ITeam' export * from './IUser' diff --git a/packages/server/src/application/Application.ts b/packages/server/src/application/Application.ts index 572b614..83bb9f6 100644 --- a/packages/server/src/application/Application.ts +++ b/packages/server/src/application/Application.ts @@ -5,6 +5,7 @@ import * as services from '../services' import * as site from '../site' import * as story from '../story' import * as team from '../team' +import * as user from '../user' import express from 'express' import {AsyncRouter, TransactionalRouter} from '../router' import {IApplication} from './IApplication' @@ -25,6 +26,7 @@ export class Application implements IApplication { readonly siteService: site.ISiteService readonly storyService: story.IStoryService readonly commentService: comment.ICommentService + readonly userPermissions: user.IUserPermissions readonly authenticator: middleware.Authenticator @@ -39,6 +41,7 @@ export class Application implements IApplication { this.storyService = new story.StoryService( this.transactionManager, this.siteService) this.commentService = new comment.CommentService(this.transactionManager) + this.userPermissions = new user.UserPermissions(this.transactionManager) this.authenticator = new middleware.Authenticator(this.userService) @@ -107,6 +110,7 @@ export class Application implements IApplication { router.use('/api', new site.SiteRoutes( this.siteService, + this.userPermissions, this.createTransactionalRouter(), ).handle) diff --git a/packages/server/src/entities/Role.ts b/packages/server/src/entities/Role.ts index 4d799f1..2a20c9c 100644 --- a/packages/server/src/entities/Role.ts +++ b/packages/server/src/entities/Role.ts @@ -3,6 +3,6 @@ import {Column, Entity} from 'typeorm' @Entity() export class Role extends BaseEntity { - @Column() + @Column({ unique: true }) name!: string } diff --git a/packages/server/src/middleware/RequestLogger.ts b/packages/server/src/middleware/RequestLogger.ts index 7e41981..db11a47 100644 --- a/packages/server/src/middleware/RequestLogger.ts +++ b/packages/server/src/middleware/RequestLogger.ts @@ -8,8 +8,9 @@ export class RequestLogger implements IMiddleware { handle: IHandler = (req, res, next) => { const start = Date.now() res.on('finish', () => { - const duration = Date.now() - start const { method, originalUrl } = req + const duration = Date.now() - start + this.logger.debug('%s %s %j', method, originalUrl, req.body) const { statusCode } = res this.logger.info('%s %s %d %sms', method, originalUrl, statusCode, duration) diff --git a/packages/server/src/migrations/1548412884820-role-unique.ts b/packages/server/src/migrations/1548412884820-role-unique.ts new file mode 100644 index 0000000..7856d00 --- /dev/null +++ b/packages/server/src/migrations/1548412884820-role-unique.ts @@ -0,0 +1,80 @@ +import {MigrationInterface, QueryRunner} from "typeorm"; + +export class roleUnique1548412884820 implements MigrationInterface { + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP INDEX "IDX_28c5d1d16da7908c97c9bc2f74"`); + await queryRunner.query(`DROP INDEX "IDX_55a938fda82579fd3ec29b3c28"`); + await queryRunner.query(`DROP INDEX "IDX_4a06baede7d9cf51aef879fb0e"`); + await queryRunner.query(`DROP INDEX "IDX_e03827c061fbf85fd3aae454ae"`); + await queryRunner.query(`DROP INDEX "IDX_fe13edd1431a248a0eeac11ae4"`); + await queryRunner.query(`CREATE TABLE "temporary_spam" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "createDate" datetime NOT NULL DEFAULT (datetime('now')), "updateDate" datetime NOT NULL DEFAULT (datetime('now')), "userId" integer NOT NULL, "commentId" integer NOT NULL, CONSTRAINT "FK_1bf468db8f4d18b424bb3eafae5" FOREIGN KEY ("commentId") REFERENCES "comment" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION, CONSTRAINT "FK_ec8bc4fa789466cf62f5949f5cc" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION)`); + await queryRunner.query(`INSERT INTO "temporary_spam"("id", "createDate", "updateDate", "userId", "commentId") SELECT "id", "createDate", "updateDate", "userId", "commentId" FROM "spam"`); + await queryRunner.query(`DROP TABLE "spam"`); + await queryRunner.query(`ALTER TABLE "temporary_spam" RENAME TO "spam"`); + await queryRunner.query(`CREATE TABLE "temporary_vote" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "createDate" datetime NOT NULL DEFAULT (datetime('now')), "updateDate" datetime NOT NULL DEFAULT (datetime('now')), "userId" integer NOT NULL, "commentId" integer NOT NULL, CONSTRAINT "FK_ad37adcff60fdb9670a97868ab1" FOREIGN KEY ("commentId") REFERENCES "comment" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION, CONSTRAINT "FK_f5de237a438d298031d11a57c3b" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION)`); + await queryRunner.query(`INSERT INTO "temporary_vote"("id", "createDate", "updateDate", "userId", "commentId") SELECT "id", "createDate", "updateDate", "userId", "commentId" FROM "vote"`); + await queryRunner.query(`DROP TABLE "vote"`); + await queryRunner.query(`ALTER TABLE "temporary_vote" RENAME TO "vote"`); + await queryRunner.query(`CREATE TABLE "temporary_role" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "createDate" datetime NOT NULL DEFAULT (datetime('now')), "updateDate" datetime NOT NULL DEFAULT (datetime('now')), "name" varchar NOT NULL)`); + await queryRunner.query(`INSERT INTO "temporary_role"("id", "createDate", "updateDate", "name") SELECT "id", "createDate", "updateDate", "name" FROM "role"`); + await queryRunner.query(`DROP TABLE "role"`); + await queryRunner.query(`ALTER TABLE "temporary_role" RENAME TO "role"`); + await queryRunner.query(`CREATE TABLE "temporary_role" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "createDate" datetime NOT NULL DEFAULT (datetime('now')), "updateDate" datetime NOT NULL DEFAULT (datetime('now')), "name" varchar NOT NULL, CONSTRAINT "UQ_d430b72bf1eaebce7f87068a431" UNIQUE ("name"))`); + await queryRunner.query(`INSERT INTO "temporary_role"("id", "createDate", "updateDate", "name") SELECT "id", "createDate", "updateDate", "name" FROM "role"`); + await queryRunner.query(`DROP TABLE "role"`); + await queryRunner.query(`ALTER TABLE "temporary_role" RENAME TO "role"`); + await queryRunner.query(`CREATE INDEX "IDX_28c5d1d16da7908c97c9bc2f74" ON "session" ("expiredAt") `); + await queryRunner.query(`CREATE INDEX "IDX_55a938fda82579fd3ec29b3c28" ON "team" ("userId") `); + await queryRunner.query(`CREATE INDEX "IDX_e03827c061fbf85fd3aae454ae" ON "site" ("userId") `); + await queryRunner.query(`CREATE INDEX "IDX_4a06baede7d9cf51aef879fb0e" ON "site" ("teamId") `); + await queryRunner.query(`CREATE INDEX "IDX_fe13edd1431a248a0eeac11ae4" ON "comment" ("storyId") `); + await queryRunner.query(`CREATE TABLE "temporary_spam" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "createDate" datetime NOT NULL DEFAULT (datetime('now')), "updateDate" datetime NOT NULL DEFAULT (datetime('now')), "userId" integer NOT NULL, "commentId" integer NOT NULL, CONSTRAINT "spam_userid_commentid" UNIQUE ("userId", "commentId"), CONSTRAINT "FK_1bf468db8f4d18b424bb3eafae5" FOREIGN KEY ("commentId") REFERENCES "comment" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION, CONSTRAINT "FK_ec8bc4fa789466cf62f5949f5cc" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION)`); + await queryRunner.query(`INSERT INTO "temporary_spam"("id", "createDate", "updateDate", "userId", "commentId") SELECT "id", "createDate", "updateDate", "userId", "commentId" FROM "spam"`); + await queryRunner.query(`DROP TABLE "spam"`); + await queryRunner.query(`ALTER TABLE "temporary_spam" RENAME TO "spam"`); + await queryRunner.query(`CREATE TABLE "temporary_vote" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "createDate" datetime NOT NULL DEFAULT (datetime('now')), "updateDate" datetime NOT NULL DEFAULT (datetime('now')), "userId" integer NOT NULL, "commentId" integer NOT NULL, CONSTRAINT "vote_userid_commentid" UNIQUE ("userId", "commentId"), CONSTRAINT "FK_ad37adcff60fdb9670a97868ab1" FOREIGN KEY ("commentId") REFERENCES "comment" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION, CONSTRAINT "FK_f5de237a438d298031d11a57c3b" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION)`); + await queryRunner.query(`INSERT INTO "temporary_vote"("id", "createDate", "updateDate", "userId", "commentId") SELECT "id", "createDate", "updateDate", "userId", "commentId" FROM "vote"`); + await queryRunner.query(`DROP TABLE "vote"`); + await queryRunner.query(`ALTER TABLE "temporary_vote" RENAME TO "vote"`); + await queryRunner.query(`INSERT INTO "role"("id", "name") VALUES (1, 'ADMIN')`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "vote" RENAME TO "temporary_vote"`); + await queryRunner.query(`CREATE TABLE "vote" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "createDate" datetime NOT NULL DEFAULT (datetime('now')), "updateDate" datetime NOT NULL DEFAULT (datetime('now')), "userId" integer NOT NULL, "commentId" integer NOT NULL, CONSTRAINT "FK_ad37adcff60fdb9670a97868ab1" FOREIGN KEY ("commentId") REFERENCES "comment" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION, CONSTRAINT "FK_f5de237a438d298031d11a57c3b" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION)`); + await queryRunner.query(`INSERT INTO "vote"("id", "createDate", "updateDate", "userId", "commentId") SELECT "id", "createDate", "updateDate", "userId", "commentId" FROM "temporary_vote"`); + await queryRunner.query(`DROP TABLE "temporary_vote"`); + await queryRunner.query(`ALTER TABLE "spam" RENAME TO "temporary_spam"`); + await queryRunner.query(`CREATE TABLE "spam" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "createDate" datetime NOT NULL DEFAULT (datetime('now')), "updateDate" datetime NOT NULL DEFAULT (datetime('now')), "userId" integer NOT NULL, "commentId" integer NOT NULL, CONSTRAINT "FK_1bf468db8f4d18b424bb3eafae5" FOREIGN KEY ("commentId") REFERENCES "comment" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION, CONSTRAINT "FK_ec8bc4fa789466cf62f5949f5cc" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION)`); + await queryRunner.query(`INSERT INTO "spam"("id", "createDate", "updateDate", "userId", "commentId") SELECT "id", "createDate", "updateDate", "userId", "commentId" FROM "temporary_spam"`); + await queryRunner.query(`DROP TABLE "temporary_spam"`); + await queryRunner.query(`DROP INDEX "IDX_fe13edd1431a248a0eeac11ae4"`); + await queryRunner.query(`DROP INDEX "IDX_4a06baede7d9cf51aef879fb0e"`); + await queryRunner.query(`DROP INDEX "IDX_e03827c061fbf85fd3aae454ae"`); + await queryRunner.query(`DROP INDEX "IDX_55a938fda82579fd3ec29b3c28"`); + await queryRunner.query(`DROP INDEX "IDX_28c5d1d16da7908c97c9bc2f74"`); + await queryRunner.query(`ALTER TABLE "role" RENAME TO "temporary_role"`); + await queryRunner.query(`CREATE TABLE "role" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "createDate" datetime NOT NULL DEFAULT (datetime('now')), "updateDate" datetime NOT NULL DEFAULT (datetime('now')), "name" varchar NOT NULL)`); + await queryRunner.query(`INSERT INTO "role"("id", "createDate", "updateDate", "name") SELECT "id", "createDate", "updateDate", "name" FROM "temporary_role"`); + await queryRunner.query(`DROP TABLE "temporary_role"`); + await queryRunner.query(`ALTER TABLE "role" RENAME TO "temporary_role"`); + await queryRunner.query(`CREATE TABLE "role" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "createDate" datetime NOT NULL DEFAULT (datetime('now')), "updateDate" datetime NOT NULL DEFAULT (datetime('now')), "name" varchar NOT NULL)`); + await queryRunner.query(`INSERT INTO "role"("id", "createDate", "updateDate", "name") SELECT "id", "createDate", "updateDate", "name" FROM "temporary_role"`); + await queryRunner.query(`DROP TABLE "temporary_role"`); + await queryRunner.query(`ALTER TABLE "vote" RENAME TO "temporary_vote"`); + await queryRunner.query(`CREATE TABLE "vote" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "createDate" datetime NOT NULL DEFAULT (datetime('now')), "updateDate" datetime NOT NULL DEFAULT (datetime('now')), "userId" integer NOT NULL, "commentId" integer NOT NULL, CONSTRAINT "UQ_5ef3b030c86a67d7c3cce97a978" UNIQUE ("userId", "commentId"), CONSTRAINT "FK_ad37adcff60fdb9670a97868ab1" FOREIGN KEY ("commentId") REFERENCES "comment" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION, CONSTRAINT "FK_f5de237a438d298031d11a57c3b" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION)`); + await queryRunner.query(`INSERT INTO "vote"("id", "createDate", "updateDate", "userId", "commentId") SELECT "id", "createDate", "updateDate", "userId", "commentId" FROM "temporary_vote"`); + await queryRunner.query(`DROP TABLE "temporary_vote"`); + await queryRunner.query(`ALTER TABLE "spam" RENAME TO "temporary_spam"`); + await queryRunner.query(`CREATE TABLE "spam" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "createDate" datetime NOT NULL DEFAULT (datetime('now')), "updateDate" datetime NOT NULL DEFAULT (datetime('now')), "userId" integer NOT NULL, "commentId" integer NOT NULL, CONSTRAINT "UQ_885dac94f112af83664ccd06dd9" UNIQUE ("userId", "commentId"), CONSTRAINT "FK_1bf468db8f4d18b424bb3eafae5" FOREIGN KEY ("commentId") REFERENCES "comment" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION, CONSTRAINT "FK_ec8bc4fa789466cf62f5949f5cc" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION)`); + await queryRunner.query(`INSERT INTO "spam"("id", "createDate", "updateDate", "userId", "commentId") SELECT "id", "createDate", "updateDate", "userId", "commentId" FROM "temporary_spam"`); + await queryRunner.query(`DROP TABLE "temporary_spam"`); + await queryRunner.query(`CREATE INDEX "IDX_fe13edd1431a248a0eeac11ae4" ON "comment" ("storyId") `); + await queryRunner.query(`CREATE INDEX "IDX_e03827c061fbf85fd3aae454ae" ON "site" ("userId") `); + await queryRunner.query(`CREATE INDEX "IDX_4a06baede7d9cf51aef879fb0e" ON "site" ("teamId") `); + await queryRunner.query(`CREATE INDEX "IDX_55a938fda82579fd3ec29b3c28" ON "team" ("userId") `); + await queryRunner.query(`CREATE INDEX "IDX_28c5d1d16da7908c97c9bc2f74" ON "session" ("expiredAt") `); + } + +} diff --git a/packages/server/src/role/IRoleService.ts b/packages/server/src/role/IRoleService.ts new file mode 100644 index 0000000..32835b0 --- /dev/null +++ b/packages/server/src/role/IRoleService.ts @@ -0,0 +1,5 @@ +import {IRole} from '@rondo/common' + +export interface IRoleService { + create(name: string): Promise +} diff --git a/packages/server/src/role/RoleService.ts b/packages/server/src/role/RoleService.ts new file mode 100644 index 0000000..666010b --- /dev/null +++ b/packages/server/src/role/RoleService.ts @@ -0,0 +1,12 @@ +import {BaseService} from '../services/BaseService' +import {IRoleService} from './IRoleService' +import {Role} from '../entities/Role' + +export class RoleService extends BaseService implements IRoleService { + create(name: string) { + return this.getRepository(Role) + .save({ + name, + }) + } +} diff --git a/packages/server/src/role/index.ts b/packages/server/src/role/index.ts new file mode 100644 index 0000000..1930642 --- /dev/null +++ b/packages/server/src/role/index.ts @@ -0,0 +1,2 @@ +export * from './IRoleService' +export * from './RoleService' diff --git a/packages/server/src/routes/LoginRoutes.ts b/packages/server/src/routes/LoginRoutes.ts index 7221b45..b3c28e9 100644 --- a/packages/server/src/routes/LoginRoutes.ts +++ b/packages/server/src/routes/LoginRoutes.ts @@ -20,7 +20,7 @@ export class LoginRoutes extends BaseRoute { password: req.body.password, }) await req.logInPromise(user) - res.redirect(req.baseUrl) + return user }) t.post('/auth/login', async (req, res, next) => { @@ -28,11 +28,10 @@ export class LoginRoutes extends BaseRoute { .authenticate('local')(req, res, next) if (!user) { - res.redirect(`${req.baseUrl}/auth/login`) + res.status(401) return } await req.logInPromise(user) - res.redirect(req.baseUrl) return user }) diff --git a/packages/server/src/site/ISiteCreateParams.ts b/packages/server/src/site/ISiteCreateParams.ts new file mode 100644 index 0000000..b3189a3 --- /dev/null +++ b/packages/server/src/site/ISiteCreateParams.ts @@ -0,0 +1,6 @@ +export interface ISiteCreateParams { + name: string + domain: string + teamId: number + userId: number +} diff --git a/packages/server/src/site/ISiteService.ts b/packages/server/src/site/ISiteService.ts index 1010ceb..a61801a 100644 --- a/packages/server/src/site/ISiteService.ts +++ b/packages/server/src/site/ISiteService.ts @@ -1,7 +1,8 @@ import {ISite} from '@rondo/common' +import {ISiteCreateParams} from './ISiteCreateParams' export interface ISiteService { - create(name: string, teamId: number, userId: number): Promise + create(params: ISiteCreateParams): Promise findOne(id: number, teamId: number): Promise diff --git a/packages/server/src/site/SiteRoutes.test.ts b/packages/server/src/site/SiteRoutes.test.ts new file mode 100644 index 0000000..9055fa1 --- /dev/null +++ b/packages/server/src/site/SiteRoutes.test.ts @@ -0,0 +1,54 @@ +import {ITeam} from '@rondo/common' +import {UserTeam} from '../entities/UserTeam' +import {createTeam} from '../team/TeamTestUtils' +import {createSite} from './SiteTestUtils' +import {test} from '../test' + +describe('team', () => { + + test.withDatabase() + const t = test.request('/api') + + let cookie!: string + let token!: string + let team!: ITeam + let userId: number + beforeEach(async () => { + const session = await test.registerAccount() + cookie = session.cookie + token = session.token + userId = session.userId + t.setHeaders({ cookie, 'x-csrf-token': token }) + + team = await createTeam(t, 'test') + }) + + describe('POST /teams/:teamId/sites', () => { + it('creates a new site', async () => { + await createSite(t, 'test.example.com') + }) + + describe('no team access', () => { + beforeEach(async () => { + await test.transactionManager + .getRepository(UserTeam) + .delete({ + userId, + teamId: team.id, + }) + }) + + it('results with 403 when user does not have team access ', async () => { + await t + .post('/teams/:teamId/sites', {teamId: team.id}) + .send({ + domain: 'test.example.com', + name: 'test', + }) + .expect(403) + }) + }) + + }) + +}) diff --git a/packages/server/src/site/SiteRoutes.ts b/packages/server/src/site/SiteRoutes.ts index 64ecc89..657fb60 100644 --- a/packages/server/src/site/SiteRoutes.ts +++ b/packages/server/src/site/SiteRoutes.ts @@ -3,11 +3,13 @@ import {BaseRoute} from '../routes/BaseRoute' import {IAPIDef} from '@rondo/common' import {ISiteService} from './ISiteService' import {ensureLoggedInApi} from '../middleware' +import {IUserPermissions} from '../user/IUserPermissions' export class SiteRoutes extends BaseRoute { constructor( protected readonly siteService: ISiteService, - protected readonly t: AsyncRouter, + protected readonly permissions: IUserPermissions, + t: AsyncRouter, ) { super(t) } @@ -35,9 +37,19 @@ export class SiteRoutes extends BaseRoute { }) t.post('/teams/:teamId/sites', async req => { - const {name} = req.body + const {name, domain} = req.body const {teamId} = req.params - return this.siteService.create(name, teamId, req.user!.id) + + await this.permissions.belongsToTeam({ + teamId, + userId: req.user!.id, + }) + return this.siteService.create({ + name, + domain, + teamId, + userId: req.user!.id, + }) }) } diff --git a/packages/server/src/site/SiteService.ts b/packages/server/src/site/SiteService.ts index b41b60d..8ca5481 100644 --- a/packages/server/src/site/SiteService.ts +++ b/packages/server/src/site/SiteService.ts @@ -1,15 +1,12 @@ import {BaseService} from '../services/BaseService' +import {ISiteCreateParams} from './ISiteCreateParams' import {ISiteService} from './ISiteService' import {Site} from '../entities/Site' export class SiteService extends BaseService implements ISiteService { - async create(name: string, teamId: number, userId: number) { + async create(params: ISiteCreateParams) { // TODO check site limit per user - return this.getRepository(Site).save({ - name, - teamId, - userId, - }) + return this.getRepository(Site).save(params) } async findOne(id: number, teamId: number) { diff --git a/packages/server/src/site/SiteTestUtils.ts b/packages/server/src/site/SiteTestUtils.ts new file mode 100644 index 0000000..2177e2f --- /dev/null +++ b/packages/server/src/site/SiteTestUtils.ts @@ -0,0 +1,18 @@ +import {RequestTester} from '../test-utils' +import {IAPIDef} from '@rondo/common' +import {createTeam} from '../team/TeamTestUtils' + +export async function createSite(t: RequestTester, domain: string) { + const team = await createTeam(t, 'test') + const response = await t + .post('/teams/:teamId/sites', { + teamId: team.id, + }) + .send({ + domain, + name: 'test-site', + }) + .expect(200) + expect(response.body.id).toBeTruthy() + return response.body +} diff --git a/packages/server/src/story/StoryRoutes.test.ts b/packages/server/src/story/StoryRoutes.test.ts new file mode 100644 index 0000000..730e92b --- /dev/null +++ b/packages/server/src/story/StoryRoutes.test.ts @@ -0,0 +1,40 @@ +// import {ISite, IStory} from '@rondo/common' +// import {createTeam} from '../team/TeamTestUtils' +// import {test} from '../test' + +// describe('team', () => { + +// test.withDatabase() +// const t = test.request('/api') + +// let cookie!: string +// let token!: string +// let team!: ITeam +// beforeEach(async () => { +// const session = await test.registerAccount() +// cookie = session.cookie +// token = session.token +// t.setHeaders({ cookie, 'x-csrf-token': token }) + +// team = await createTeam(t, 'test') +// }) + +// describe('/stories/by-url', () => { +// it('returns undefined when a site is not configured', async () => { + +// }) + +// it('creates a story when it does not exist', async () => { + +// }) + +// it('retrieves existing story after it is created', async () => { + +// }) + +// it('prevents unique exceptions', async () => { + +// }) +// }) + +// }) diff --git a/packages/server/src/team/ITeamService.ts b/packages/server/src/team/ITeamService.ts index 1f6f2c9..5936fba 100644 --- a/packages/server/src/team/ITeamService.ts +++ b/packages/server/src/team/ITeamService.ts @@ -1,11 +1,17 @@ import {Team} from '../entities/Team' +import {UserTeam} from '../entities/UserTeam' +import {IUserTeamParams} from './IUserTeamParams' export interface ITeamService { - create(name: string, userId: number): Promise + create(params: {name: string, userId: number}): Promise - findOne(id: number, userId: number): Promise + addUser(params: IUserTeamParams): Promise - find(userId: number): Promise + removeUser(params: IUserTeamParams): Promise + + findOne(id: number): Promise + + find(userId: number): Promise // TODO add other methods } diff --git a/packages/server/src/team/IUserTeamParams.ts b/packages/server/src/team/IUserTeamParams.ts new file mode 100644 index 0000000..7794b73 --- /dev/null +++ b/packages/server/src/team/IUserTeamParams.ts @@ -0,0 +1,5 @@ +export interface IUserTeamParams { + teamId: number + userId: number + roleId: number +} diff --git a/packages/server/src/team/TeamRoutes.test.ts b/packages/server/src/team/TeamRoutes.test.ts index 2d43655..37c7ed7 100644 --- a/packages/server/src/team/TeamRoutes.test.ts +++ b/packages/server/src/team/TeamRoutes.test.ts @@ -1,4 +1,5 @@ import {test} from '../test' +import {createTeam} from './TeamTestUtils' describe('team', () => { @@ -14,27 +15,16 @@ describe('team', () => { t.setHeaders({ cookie, 'x-csrf-token': token }) }) - async function createTeam(name: string) { - const response = await t - .post('/teams') - .send({ - name: 'test', - }) - .expect(200) - expect(response.body.id).toBeTruthy() - return response.body - } - describe('POST /teams', () => { it('creates a new team', async () => { - const team = await createTeam('test') + const team = await createTeam(t, 'test') expect(team.name).toEqual('test') }) }) describe('GET /teams/:id', () => { it('retrieves a team by id', async () => { - const team = await createTeam('test') + const team = await createTeam(t, 'test') const response = await t .get('/teams/:id', { id: team.id, @@ -46,11 +36,14 @@ describe('team', () => { describe('GET /my/teams', () => { it('retrieves all teams belonging to current user', async () => { - const team = await createTeam('test') + const team = await createTeam(t, 'test') const response = await t .get('/my/teams') .expect(200) - expect(response.body).toContainEqual(team) + expect(response.body.map(ut => ({teamId: ut.teamId}))) + .toContainEqual({ + teamId: team.id, + }) }) }) diff --git a/packages/server/src/team/TeamRoutes.ts b/packages/server/src/team/TeamRoutes.ts index 464a667..d3b5e7c 100644 --- a/packages/server/src/team/TeamRoutes.ts +++ b/packages/server/src/team/TeamRoutes.ts @@ -16,7 +16,7 @@ export class TeamRoutes extends BaseRoute { t.get('/teams/:id', async req => { const {id} = req.params - return this.teamService.findOne(id, req.user!.id) + return this.teamService.findOne(id) }) t.use(ensureLoggedInApi) @@ -27,7 +27,10 @@ export class TeamRoutes extends BaseRoute { t.post('/teams', async req => { const {name} = req.body - return this.teamService.create(name, req.user!.id) + return this.teamService.create({ + name, + userId: req.user!.id, + }) }) } diff --git a/packages/server/src/team/TeamService.ts b/packages/server/src/team/TeamService.ts index 3a55cbe..e291b5b 100644 --- a/packages/server/src/team/TeamService.ts +++ b/packages/server/src/team/TeamService.ts @@ -1,24 +1,47 @@ import {BaseService} from '../services/BaseService' import {ITeamService} from './ITeamService' +import {IUserTeamParams} from './IUserTeamParams' import {Team} from '../entities/Team' +import {UserTeam} from '../entities/UserTeam' export class TeamService extends BaseService implements ITeamService { // TODO check team limit per user - async create(name: string, userId: number) { - return this.getRepository(Team).save({ + async create({name, userId}: {name: string, userId: number}) { + const team = await this.getRepository(Team).save({ name, userId, }) + + await this.addUser({ + teamId: team.id, + userId, + // ADMIN role + roleId: 1, + }) + + return team } - findOne(id: number) { + async addUser(params: IUserTeamParams) { + const {userId, teamId, roleId} = params + return this.getRepository(UserTeam) + .save({userId, teamId, roleId}) + } + + async removeUser({teamId, userId, roleId}: IUserTeamParams) { + await this.getRepository(UserTeam) + .delete({userId, teamId, roleId}) + } + + async findOne(id: number) { return this.getRepository(Team).findOne(id) } - find(userId: number) { + async find(userId: number) { // TODO find all teams via UserTeam instead of userId - return this.getRepository(Team).find({ - where: { userId }, + return this.getRepository(UserTeam).find({ + relations: ['team'], + where: {userId}, }) } diff --git a/packages/server/src/team/TeamTestUtils.ts b/packages/server/src/team/TeamTestUtils.ts new file mode 100644 index 0000000..3352bec --- /dev/null +++ b/packages/server/src/team/TeamTestUtils.ts @@ -0,0 +1,13 @@ +import {RequestTester} from '../test-utils' +import {IAPIDef} from '@rondo/common' + +export async function createTeam(t: RequestTester, name: string) { + const response = await t + .post('/teams') + .send({ + name: 'test', + }) + .expect(200) + expect(response.body.id).toBeTruthy() + return response.body +} diff --git a/packages/server/src/test-utils/RequestTester.ts b/packages/server/src/test-utils/RequestTester.ts index ed0836c..f17d7c0 100644 --- a/packages/server/src/test-utils/RequestTester.ts +++ b/packages/server/src/test-utils/RequestTester.ts @@ -71,6 +71,6 @@ export class RequestTester { } post

(path: P, params?: IRequestParams) { - return this.request('post', path) + return this.request('post', path, params) } } diff --git a/packages/server/src/test-utils/TestUtils.ts b/packages/server/src/test-utils/TestUtils.ts index bc5fc63..bb31873 100644 --- a/packages/server/src/test-utils/TestUtils.ts +++ b/packages/server/src/test-utils/TestUtils.ts @@ -7,6 +7,7 @@ import { import {IRoutes} from '@rondo/common' import {IBootstrap} from '../application/IBootstrap' import {RequestTester} from './RequestTester' +import {Role} from '../entities/Role' export class TestUtils { readonly username = 'test@user.com' @@ -52,6 +53,12 @@ export class TestUtils { }) } + async createRole(name: string) { + return this.transactionManager + .getRepository(Role) + .save({name}) + } + async getError(promise: Promise): Promise { let error!: Error try { @@ -95,12 +102,11 @@ export class TestUtils { .post(`${context}/app/auth/register`) .set('cookie', cookie) .send(this.getLoginBody(token)) - .expect(302) - .expect('location', `${context}/app`) + .expect(200) return { cookie: response.header['set-cookie'] as string, - userId: response.body.userId, + userId: response.body.id, token, } } @@ -113,8 +119,7 @@ export class TestUtils { .post(`${context}/app/auth/login`) .set('cookie', cookie) .send(`username=${_username}&password=${_password}&_csrf=${token}`) - .expect(302) - .expect('location', `${context}/app`) + .expect(200) return {cookie: response.header['set-cookie'] as string, token} } diff --git a/packages/server/src/user/IUserPermissions.ts b/packages/server/src/user/IUserPermissions.ts new file mode 100644 index 0000000..79937e5 --- /dev/null +++ b/packages/server/src/user/IUserPermissions.ts @@ -0,0 +1,4 @@ +export interface IUserPermissions { + // TODO check for role too + belongsToTeam(params: {userId: number, teamId: number}): void +} diff --git a/packages/server/src/user/UserPermissions.ts b/packages/server/src/user/UserPermissions.ts new file mode 100644 index 0000000..850b8f7 --- /dev/null +++ b/packages/server/src/user/UserPermissions.ts @@ -0,0 +1,22 @@ +import createError from 'http-errors' +import {BaseService} from '../services/BaseService' +import {UserTeam} from '../entities/UserTeam' +import {IUserPermissions} from './IUserPermissions' + +export class UserPermissions extends BaseService implements IUserPermissions { + async belongsToTeam(params: {userId: number, teamId: number}) { + const {userId, teamId} = params + const result = await this.getRepository(UserTeam) + .findOne({ + where: {userId, teamId}, + }) + + this.assert(result) + } + + protected assert(value: any) { + if (!value) { + throw createError(403, 'Forbidden') + } + } +} diff --git a/packages/server/src/user/index.ts b/packages/server/src/user/index.ts new file mode 100644 index 0000000..f323d57 --- /dev/null +++ b/packages/server/src/user/index.ts @@ -0,0 +1,2 @@ +export * from './IUserPermissions' +export * from './UserPermissions'