Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
396 changes: 395 additions & 1 deletion package-lock.json

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions packages/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,16 +24,21 @@
"@apollo/subgraph": "^2.4.12",
"@google-cloud/storage": "^7.7.0",
"@nestjs/apollo": "^12.0.7",
"@nestjs/axios": "^3.0.1",
"@nestjs/common": "^9.0.0",
"@nestjs/config": "^3.0.0",
"@nestjs/core": "^9.0.0",
"@nestjs/graphql": "^12.0.8",
"@nestjs/jwt": "^10.2.0",
"@nestjs/mongoose": "^10.0.1",
"@nestjs/passport": "^10.0.3",
"@nestjs/platform-express": "^9.0.0",
"@types/passport-jwt": "^3.0.13",
"csv-parser": "^3.0.0",
"graphql-type-json": "^0.3.2",
"jsonschema": "^1.4.1",
"mongoose": "^7.4.3",
"passport-jwt": "^4.0.1",
"reflect-metadata": "^0.1.13",
"rimraf": "^3.0.2",
"rxjs": "^7.2.0"
Expand Down
2 changes: 0 additions & 2 deletions packages/server/schema.gql
Original file line number Diff line number Diff line change
Expand Up @@ -131,8 +131,6 @@ type Mutation {
createEntry(entry: EntryCreate!, dataset: ID!): Entry!
createUploadSession(dataset: ID!): UploadSession!
completeUploadSession(session: ID!): UploadResult!
uploadEntryCSV: Boolean!
processEntryUploads: Boolean!
createTags(study: ID!, entries: [ID!]!): [Tag!]!
assignTag(study: ID!): Tag
completeTag(tag: ID!, data: JSON!): Boolean!
Expand Down
4 changes: 3 additions & 1 deletion packages/server/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { StudyModule } from './study/study.module';
import { EntryModule } from './entry/entry.module';
import { TagModule } from './tag/tag.module';
import { SharedModule } from './shared/shared.module';
import { AuthModule } from './auth/auth.module';

@Module({
imports: [
Expand Down Expand Up @@ -38,7 +39,8 @@ import { SharedModule } from './shared/shared.module';
StudyModule,
EntryModule,
TagModule,
SharedModule
SharedModule,
AuthModule
],
})
export class AppModule {}
43 changes: 43 additions & 0 deletions packages/server/src/auth/auth.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { forwardRef, Module } from '@nestjs/common';
import { JwtModule, JwtModuleOptions } from '@nestjs/jwt';
import { AuthService } from './auth.service';
import { PassportModule } from '@nestjs/passport';
import { JwtStrategy } from './jwt.strategy';
import { JwtAuthGuard } from './jwt.guard';
import { OrganizationModule } from '../organization/organization.module';
import { HttpModule } from '@nestjs/axios';

@Module({
imports: [
PassportModule,
OrganizationModule,
HttpModule,
JwtModule.registerAsync({
imports: [forwardRef(() => AuthModule)],
inject: [AuthService],
useFactory: async (authService: AuthService) => {
const options: JwtModuleOptions = {
publicKey: await authService.getPublicKey(),
signOptions: {
algorithm: 'RS256'
}
};
return options;
}
})
],
providers: [
AuthService,
JwtAuthGuard,
{
provide: JwtStrategy,
inject: [AuthService],
useFactory: async (authService: AuthService) => {
const key = await authService.getPublicKey();
return new JwtStrategy(key);
}
}
],
exports: [AuthService]
})
export class AuthModule {}
29 changes: 29 additions & 0 deletions packages/server/src/auth/auth.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { Injectable } from '@nestjs/common';
import { HttpService } from '@nestjs/axios';
import { ConfigService } from '@nestjs/config';
import { firstValueFrom } from 'rxjs';

@Injectable()
export class AuthService {
private publicKey: string | null = null;

constructor(private readonly httpService: HttpService, private readonly configService: ConfigService) {}

// TODO: In the future this will be replaced by a library which handles
// key rotation
async queryForPublicKey(): Promise<string> {
const query = this.configService.getOrThrow('auth.publicKeyUrl');

const response = await firstValueFrom(this.httpService.get(query));
return response.data[0];
}

async getPublicKey(): Promise<string> {
// TODO: Replace with an actual call to the auth service
if (this.publicKey === null) {
this.publicKey = await this.queryForPublicKey();
}

return this.publicKey;
}
}
16 changes: 16 additions & 0 deletions packages/server/src/auth/jwt.guard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Injectable, ExecutionContext } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { GqlExecutionContext } from '@nestjs/graphql';

@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
getRequest(context: ExecutionContext) {
// Return HTTP context
if (context.getType() === 'http') {
return context.switchToHttp().getRequest();
}
// Return GraphQL context
const ctx = GqlExecutionContext.create(context);
return ctx.getContext().req;
}
}
19 changes: 19 additions & 0 deletions packages/server/src/auth/jwt.strategy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { TokenPayload } from './user.dto';

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(publicKey: string) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: publicKey
});
}

validate(payload: TokenPayload) {
return payload;
}
}
7 changes: 7 additions & 0 deletions packages/server/src/auth/user.decorator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { GqlExecutionContext } from '@nestjs/graphql';
import { ExecutionContext, createParamDecorator } from '@nestjs/common';

export const UserContext = createParamDecorator((_data: unknown, ctx: ExecutionContext) => {
const gqlCtx = GqlExecutionContext.create(ctx);
return gqlCtx.getContext().req.user;
});
9 changes: 9 additions & 0 deletions packages/server/src/auth/user.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// TODO: In the future this type will be retrived from the auth microservice
export interface TokenPayload {
id: string;
projectId: string;
role: number;
iat: number;
exp: number;
iss: string;
}
3 changes: 3 additions & 0 deletions packages/server/src/config/configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,8 @@ export default () => ({
},
entry: {
signedURLExpiration: process.env.GCP_STORAGE_ENTRY_SIGNED_URL_EXPIRATION || (15 * 60 * 1000) // 15 minutes
},
auth: {
publicKeyUrl: process.env.AUTH_PUBLIC_KEY_URL || 'https://test-auth-service.sail.codes/public-key'
}
});
4 changes: 3 additions & 1 deletion packages/server/src/dataset/dataset.resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ import { Organization } from '../organization/organization.model';
import { OrganizationContext } from '../organization/organization.context';
import { DatasetCreate } from './dtos/create.dto';
import { DatasetPipe } from './pipes/dataset.pipe';
import { BadRequestException } from '@nestjs/common';
import { BadRequestException, UseGuards } from '@nestjs/common';
import { JwtAuthGuard } from 'src/auth/jwt.guard';

// TODO: Add authentication
@UseGuards(JwtAuthGuard)
@Resolver(() => Dataset)
export class DatasetResolver {
constructor(private readonly datasetService: DatasetService) {}
Expand Down
2 changes: 0 additions & 2 deletions packages/server/src/entry/entry.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import { UploadSession, UploadSessionSchema } from './models/upload-session.mode
import { EntryUpload, EntryUploadSchema } from './models/entry-upload.model';
import { UploadSessionResolver } from './resolvers/upload-session.resolver';
import { UploadSessionService } from './services/upload-session.service';
import { EntryUploadResolver } from './resolvers/entry-upload.resolver';
import { EntryUploadService } from './services/entry-upload.service';
import { UploadSessionPipe } from './pipes/upload-session.pipe';
import { GcpModule } from '../gcp/gcp.module';
Expand All @@ -33,7 +32,6 @@ import { CsvValidationService } from './services/csv-validation.service';
UploadSessionService,
UploadSessionResolver,
UploadSessionPipe,
EntryUploadResolver,
EntryUploadService,
CsvValidationService
],
Expand Down
18 changes: 0 additions & 18 deletions packages/server/src/entry/resolvers/entry-upload.resolver.ts

This file was deleted.

3 changes: 3 additions & 0 deletions packages/server/src/entry/resolvers/entry.resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ import { Entry } from '../models/entry.model';
import { EntryCreate } from '../dtos/create.dto';
import { EntryService } from '../services/entry.service';
import { DatasetPipe } from '../../dataset/pipes/dataset.pipe';
import { UseGuards } from '@nestjs/common';
import { JwtAuthGuard } from '../../auth/jwt.guard';

@UseGuards(JwtAuthGuard)
@Resolver(() => Entry)
export class EntryResolver {
constructor(private readonly entryService: EntryService) {}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import { Injectable } from '@nestjs/common';
import { Injectable, UseGuards } from '@nestjs/common';
import { UploadSession } from '../models/upload-session.model';
import { UploadSessionService } from '../services/upload-session.service';
import { Dataset } from '../../dataset/dataset.model';
import { Args, ID, Mutation, Query } from '@nestjs/graphql';
import { UploadSessionPipe } from '../pipes/upload-session.pipe';
import { DatasetPipe } from '../../dataset/pipes/dataset.pipe';
import { UploadResult } from '../dtos/upload-result.dto';
import { JwtAuthGuard } from '../../auth/jwt.guard';


@UseGuards(JwtAuthGuard)
@Injectable()
export class UploadSessionResolver {
constructor(private readonly uploadSessionService: UploadSessionService) {}
Expand Down
6 changes: 5 additions & 1 deletion packages/server/src/organization/organization.resolver.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { Resolver, Query, Mutation, Args } from '@nestjs/graphql';
import {OrganizationCreate} from './dtos/create.dto';
import { OrganizationCreate } from './dtos/create.dto';
import { Organization } from './organization.model';
import { OrganizationService } from './organization.service';
import { CreateOrganizationPipe } from './pipes/create.pipe';
import { UseGuards } from '@nestjs/common';
import { JwtAuthGuard } from 'src/auth/jwt.guard';

@Resolver(() => Organization)
export class OrganizationResolver {
Expand All @@ -13,13 +15,15 @@ export class OrganizationResolver {
return this.orgService.find();
}

@UseGuards(JwtAuthGuard)
@Query(() => Boolean)
async exists(@Args('name') name: string): Promise<boolean> {
const existingProject = await this.orgService.findByName(name);
return existingProject !== null;
}

// TODO: Add authentication guard
@UseGuards(JwtAuthGuard)
@Mutation(() => Organization)
async createOrganization(@Args('organization', CreateOrganizationPipe) organization: OrganizationCreate): Promise<Organization> {
return this.orgService.create(organization);
Expand Down
7 changes: 5 additions & 2 deletions packages/server/src/project/project.resolver.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import {BadRequestException} from '@nestjs/common';
import { BadRequestException, UseGuards } from '@nestjs/common';
import { Resolver, Mutation, Query, Args, ID } from '@nestjs/graphql';
import { OrganizationContext } from 'src/organization/organization.context';
import { Organization } from 'src/organization/organization.model';
import { ProjectCreate } from './dtos/create.dto';
import { Project } from './project.model';
import { ProjectService } from './project.service';
import {ProjectPipe} from './pipes/project.pipe';
import { ProjectPipe } from './pipes/project.pipe';
import { JwtAuthGuard } from '../auth/jwt.guard';


@UseGuards(JwtAuthGuard)
@Resolver(() => Project)
export class ProjectResolver {
constructor(private readonly projectService: ProjectService) {}
Expand Down
3 changes: 3 additions & 0 deletions packages/server/src/study/study.resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ import { StudyPipe } from './pipes/study.pipe';
import { StudyCreate } from './dtos/create.dto';
import { StudyService } from './study.service';
import { StudyCreatePipe } from './pipes/create.pipe';
import { UseGuards } from '@nestjs/common';
import { JwtAuthGuard } from '../auth/jwt.guard';

@UseGuards(JwtAuthGuard)
@Resolver(() => Study)
export class StudyResolver {
constructor(private readonly studyService: StudyService) {}
Expand Down
4 changes: 4 additions & 0 deletions packages/server/src/tag/tag.resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@ import { EntriesPipe, EntryPipe } from '../entry/pipes/entry.pipe';
import { Entry } from '../entry/models/entry.model';
import { TagPipe } from './pipes/tag.pipe';
import JSON from 'graphql-type-json';
import { UseGuards } from '@nestjs/common';
import { JwtAuthGuard } from '../auth/jwt.guard';


@UseGuards(JwtAuthGuard)
@Resolver(() => Tag)
export class TagResolver {
constructor(private readonly tagService: TagService, private readonly entryPipe: EntryPipe, private readonly studyPipe: StudyPipe) {}
Expand Down