Add StoryRoutes.test.ts
This commit is contained in:
parent
2b431bee78
commit
3b295b2e13
@ -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
|
||||
}
|
||||
|
||||
4
packages/common/src/IRole.ts
Normal file
4
packages/common/src/IRole.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export interface IRole {
|
||||
readonly id: number
|
||||
readonly name: string
|
||||
}
|
||||
@ -1,6 +1,7 @@
|
||||
export interface ISite {
|
||||
readonly id: number
|
||||
readonly name: string
|
||||
readonly domain: string
|
||||
readonly teamId: number
|
||||
readonly userId: number
|
||||
}
|
||||
|
||||
8
packages/common/src/IUserTeam.ts
Normal file
8
packages/common/src/IUserTeam.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import {ITeam} from './ITeam'
|
||||
|
||||
export interface IUserTeam {
|
||||
userId: number
|
||||
teamId: number
|
||||
roleId: number
|
||||
team?: ITeam
|
||||
}
|
||||
@ -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'
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -3,6 +3,6 @@ import {Column, Entity} from 'typeorm'
|
||||
|
||||
@Entity()
|
||||
export class Role extends BaseEntity {
|
||||
@Column()
|
||||
@Column({ unique: true })
|
||||
name!: string
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
80
packages/server/src/migrations/1548412884820-role-unique.ts
Normal file
80
packages/server/src/migrations/1548412884820-role-unique.ts
Normal file
@ -0,0 +1,80 @@
|
||||
import {MigrationInterface, QueryRunner} from "typeorm";
|
||||
|
||||
export class roleUnique1548412884820 implements MigrationInterface {
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<any> {
|
||||
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<any> {
|
||||
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") `);
|
||||
}
|
||||
|
||||
}
|
||||
5
packages/server/src/role/IRoleService.ts
Normal file
5
packages/server/src/role/IRoleService.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import {IRole} from '@rondo/common'
|
||||
|
||||
export interface IRoleService {
|
||||
create(name: string): Promise<IRole>
|
||||
}
|
||||
12
packages/server/src/role/RoleService.ts
Normal file
12
packages/server/src/role/RoleService.ts
Normal file
@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
2
packages/server/src/role/index.ts
Normal file
2
packages/server/src/role/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './IRoleService'
|
||||
export * from './RoleService'
|
||||
@ -20,7 +20,7 @@ export class LoginRoutes extends BaseRoute<IAPIDef> {
|
||||
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<IAPIDef> {
|
||||
.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
|
||||
})
|
||||
|
||||
|
||||
6
packages/server/src/site/ISiteCreateParams.ts
Normal file
6
packages/server/src/site/ISiteCreateParams.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export interface ISiteCreateParams {
|
||||
name: string
|
||||
domain: string
|
||||
teamId: number
|
||||
userId: number
|
||||
}
|
||||
@ -1,7 +1,8 @@
|
||||
import {ISite} from '@rondo/common'
|
||||
import {ISiteCreateParams} from './ISiteCreateParams'
|
||||
|
||||
export interface ISiteService {
|
||||
create(name: string, teamId: number, userId: number): Promise<ISite>
|
||||
create(params: ISiteCreateParams): Promise<ISite>
|
||||
|
||||
findOne(id: number, teamId: number): Promise<ISite | undefined>
|
||||
|
||||
|
||||
54
packages/server/src/site/SiteRoutes.test.ts
Normal file
54
packages/server/src/site/SiteRoutes.test.ts
Normal file
@ -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)
|
||||
})
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
})
|
||||
@ -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<IAPIDef> {
|
||||
constructor(
|
||||
protected readonly siteService: ISiteService,
|
||||
protected readonly t: AsyncRouter<IAPIDef>,
|
||||
protected readonly permissions: IUserPermissions,
|
||||
t: AsyncRouter<IAPIDef>,
|
||||
) {
|
||||
super(t)
|
||||
}
|
||||
@ -35,9 +37,19 @@ export class SiteRoutes extends BaseRoute<IAPIDef> {
|
||||
})
|
||||
|
||||
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,
|
||||
})
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
@ -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) {
|
||||
|
||||
18
packages/server/src/site/SiteTestUtils.ts
Normal file
18
packages/server/src/site/SiteTestUtils.ts
Normal file
@ -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<IAPIDef>, 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
|
||||
}
|
||||
40
packages/server/src/story/StoryRoutes.test.ts
Normal file
40
packages/server/src/story/StoryRoutes.test.ts
Normal file
@ -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 () => {
|
||||
|
||||
// })
|
||||
// })
|
||||
|
||||
// })
|
||||
@ -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<Team>
|
||||
create(params: {name: string, userId: number}): Promise<Team>
|
||||
|
||||
findOne(id: number, userId: number): Promise<Team | undefined>
|
||||
addUser(params: IUserTeamParams): Promise<UserTeam>
|
||||
|
||||
find(userId: number): Promise<Team[]>
|
||||
removeUser(params: IUserTeamParams): Promise<void>
|
||||
|
||||
findOne(id: number): Promise<Team | undefined>
|
||||
|
||||
find(userId: number): Promise<UserTeam[]>
|
||||
|
||||
// TODO add other methods
|
||||
}
|
||||
|
||||
5
packages/server/src/team/IUserTeamParams.ts
Normal file
5
packages/server/src/team/IUserTeamParams.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export interface IUserTeamParams {
|
||||
teamId: number
|
||||
userId: number
|
||||
roleId: number
|
||||
}
|
||||
@ -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,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@ -16,7 +16,7 @@ export class TeamRoutes extends BaseRoute<IAPIDef> {
|
||||
|
||||
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<IAPIDef> {
|
||||
|
||||
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,
|
||||
})
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
@ -1,23 +1,46 @@
|
||||
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({
|
||||
return this.getRepository(UserTeam).find({
|
||||
relations: ['team'],
|
||||
where: {userId},
|
||||
})
|
||||
}
|
||||
|
||||
13
packages/server/src/team/TeamTestUtils.ts
Normal file
13
packages/server/src/team/TeamTestUtils.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import {RequestTester} from '../test-utils'
|
||||
import {IAPIDef} from '@rondo/common'
|
||||
|
||||
export async function createTeam(t: RequestTester<IAPIDef>, name: string) {
|
||||
const response = await t
|
||||
.post('/teams')
|
||||
.send({
|
||||
name: 'test',
|
||||
})
|
||||
.expect(200)
|
||||
expect(response.body.id).toBeTruthy()
|
||||
return response.body
|
||||
}
|
||||
@ -71,6 +71,6 @@ export class RequestTester<R extends IRoutes> {
|
||||
}
|
||||
|
||||
post<P extends keyof R & string>(path: P, params?: IRequestParams) {
|
||||
return this.request('post', path)
|
||||
return this.request('post', path, params)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<T extends IRoutes> {
|
||||
readonly username = 'test@user.com'
|
||||
@ -52,6 +53,12 @@ export class TestUtils<T extends IRoutes> {
|
||||
})
|
||||
}
|
||||
|
||||
async createRole(name: string) {
|
||||
return this.transactionManager
|
||||
.getRepository(Role)
|
||||
.save({name})
|
||||
}
|
||||
|
||||
async getError(promise: Promise<any>): Promise<Error> {
|
||||
let error!: Error
|
||||
try {
|
||||
@ -95,12 +102,11 @@ export class TestUtils<T extends IRoutes> {
|
||||
.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<T extends IRoutes> {
|
||||
.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}
|
||||
}
|
||||
|
||||
4
packages/server/src/user/IUserPermissions.ts
Normal file
4
packages/server/src/user/IUserPermissions.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export interface IUserPermissions {
|
||||
// TODO check for role too
|
||||
belongsToTeam(params: {userId: number, teamId: number}): void
|
||||
}
|
||||
22
packages/server/src/user/UserPermissions.ts
Normal file
22
packages/server/src/user/UserPermissions.ts
Normal file
@ -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')
|
||||
}
|
||||
}
|
||||
}
|
||||
2
packages/server/src/user/index.ts
Normal file
2
packages/server/src/user/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './IUserPermissions'
|
||||
export * from './UserPermissions'
|
||||
Loading…
x
Reference in New Issue
Block a user