Skip to content

Commit fe182c8

Browse files
authored
Merge pull request #3427 from SeedCompany/session-host
2 parents 1d48287 + dc9f91b commit fe182c8

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

49 files changed

+844
-800
lines changed

src/common/context.type.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
import { type OperationDefinitionNode } from 'graphql';
2-
import { type BehaviorSubject } from 'rxjs';
32
import type { IRequest, IResponse } from '~/core/http';
4-
import { type Session } from './session';
53

64
/**
75
* The type for graphql @Context() decorator
@@ -10,5 +8,4 @@ export interface GqlContextType {
108
operation: OperationDefinitionNode;
119
request?: IRequest;
1210
response?: IResponse;
13-
readonly session$: BehaviorSubject<Session | undefined>;
1411
}

src/common/session.ts

Lines changed: 14 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,15 @@
11
import {
2-
createParamDecorator,
3-
type ExecutionContext,
2+
Injectable,
3+
Param,
44
type PipeTransform,
55
type Type,
66
} from '@nestjs/common';
77
import { CONTROLLER_WATERMARK } from '@nestjs/common/constants.js';
88
import { Context } from '@nestjs/graphql';
99
import { uniq } from 'lodash';
1010
import { type DateTime } from 'luxon';
11-
import { NoSessionException } from '../components/authentication/no-session.exception';
11+
import { SessionHost } from '../components/authentication/session.host';
1212
import { type ScopedRole } from '../components/authorization/dto';
13-
import { type GqlContextType } from './context.type';
1413
import { UnauthenticatedException } from './exceptions';
1514
import { type ID } from './id-field';
1615

@@ -41,38 +40,34 @@ export function loggedInSession(session: Session): Session {
4140
return session;
4241
}
4342

44-
export const sessionFromContext = (context: GqlContextType) => {
45-
const session = context.session$.value;
46-
if (!session) {
47-
throw new NoSessionException();
43+
@Injectable()
44+
export class SessionPipe implements PipeTransform {
45+
constructor(private readonly sessionHost: SessionHost) {}
46+
47+
transform() {
48+
return this.sessionHost.currentMaybe;
4849
}
49-
return session;
50-
};
50+
}
5151

52+
/** @deprecated */
5253
export const LoggedInSession = () =>
5354
AnonSession({ transform: loggedInSession });
5455

56+
/** @deprecated */
5557
export const AnonSession =
5658
(...pipes: Array<Type<PipeTransform> | PipeTransform>): ParameterDecorator =>
5759
(...args) => {
58-
Context({ transform: sessionFromContext }, ...pipes)(...args);
60+
Context(SessionPipe, ...pipes)(...args);
5961
process.nextTick(() => {
6062
// Only set this metadata if it's a controller method.
6163
// Waiting for the next tick as class decorators execute after methods.
6264
if (Reflect.getMetadata(CONTROLLER_WATERMARK, args[0].constructor)) {
63-
HttpSession(...pipes)(...args);
65+
Param(SessionPipe, ...pipes)(...args);
6466
SessionWatermark(...args);
6567
}
6668
});
6769
};
6870

69-
// Using Nest's custom decorator so that we can pass pipes.
70-
const HttpSession = createParamDecorator(
71-
(_data: unknown, ctx: ExecutionContext) => {
72-
return ctx.switchToHttp().getRequest().session;
73-
},
74-
);
75-
7671
const SessionWatermark: ParameterDecorator = (target, key) =>
7772
Reflect.defineMetadata('SESSION_WATERMARK', true, target.constructor, key!);
7873

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { createMetadataDecorator } from '@seedcompany/nest';
2+
3+
export const LoggedIn = () => Anonymous(false);
4+
export const Anonymous = createMetadataDecorator({
5+
setter: (anonymous = true) => anonymous,
6+
types: ['class', 'method'],
7+
});

src/components/authentication/authentication.module.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import { forwardRef, Global, Module } from '@nestjs/common';
22
import { APP_INTERCEPTOR } from '@nestjs/core';
3+
import { SessionPipe } from '~/common/session';
34
import { splitDb } from '~/core';
45
import { AuthorizationModule } from '../authorization/authorization.module';
56
import { UserModule } from '../user/user.module';
67
import { AuthenticationGelRepository } from './authentication.gel.repository';
78
import { AuthenticationRepository } from './authentication.repository';
89
import { AuthenticationService } from './authentication.service';
910
import { CryptoService } from './crypto.service';
10-
import { GelCurrentUserProvider } from './current-user.provider';
1111
import {
1212
LoginExtraInfoResolver,
1313
RegisterExtraInfoResolver,
@@ -16,6 +16,7 @@ import {
1616
import { LoginResolver } from './login.resolver';
1717
import { PasswordResolver } from './password.resolver';
1818
import { RegisterResolver } from './register.resolver';
19+
import { SessionHost, SessionHostImpl } from './session.host';
1920
import { SessionInterceptor } from './session.interceptor';
2021
import { SessionResolver } from './session.resolver';
2122

@@ -39,10 +40,11 @@ import { SessionResolver } from './session.resolver';
3940
CryptoService,
4041
SessionInterceptor,
4142
{ provide: APP_INTERCEPTOR, useExisting: SessionInterceptor },
42-
GelCurrentUserProvider,
43-
{ provide: APP_INTERCEPTOR, useExisting: GelCurrentUserProvider },
43+
{ provide: SessionHost, useClass: SessionHostImpl },
44+
SessionPipe,
4445
],
4546
exports: [
47+
SessionHost,
4648
SessionInterceptor,
4749
AuthenticationService,
4850
'AUTHENTICATION',

src/components/authentication/authentication.service.ts

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import { DateTime } from 'luxon';
77
import type { Writable } from 'ts-essentials';
88
import {
99
DuplicateException,
10-
type GqlContextType,
1110
type ID,
1211
InputException,
1312
type Role,
@@ -16,7 +15,6 @@ import {
1615
UnauthenticatedException,
1716
UnauthorizedException,
1817
} from '~/common';
19-
import { sessionFromContext } from '~/common/session';
2018
import { ConfigService, ILogger, Logger } from '~/core';
2119
import { ForgotPassword } from '~/core/email/templates';
2220
import { disableAccessPolicies, Gel } from '~/core/gel';
@@ -28,6 +26,7 @@ import { AuthenticationRepository } from './authentication.repository';
2826
import { CryptoService } from './crypto.service';
2927
import type { LoginInput, RegisterInput, ResetPasswordInput } from './dto';
3028
import { NoSessionException } from './no-session.exception';
29+
import { SessionHost } from './session.host';
3130

3231
interface JwtPayload {
3332
iat: number;
@@ -44,6 +43,7 @@ export class AuthenticationService {
4443
private readonly repo: AuthenticationRepository,
4544
private readonly gel: Gel,
4645
private readonly agents: SystemAgentRepository,
46+
private readonly sessionHost: SessionHost,
4747
private readonly moduleRef: ModuleRef,
4848
) {}
4949

@@ -101,10 +101,10 @@ export class AuthenticationService {
101101
return userId;
102102
}
103103

104-
async updateSession(context: GqlContextType) {
105-
const prev = sessionFromContext(context);
104+
async refreshCurrentSession() {
105+
const prev = this.sessionHost.current;
106106
const newSession = await this.resumeSession(prev.token);
107-
context.session$.next(newSession);
107+
this.sessionHost.current$.next(newSession);
108108
return newSession;
109109
}
110110

@@ -169,10 +169,12 @@ export class AuthenticationService {
169169
: requesterSession;
170170

171171
if (impersonatee) {
172-
const p = this.privileges.for(requesterSession, AssignableRoles);
173-
const valid = impersonatee.roles.every((role) =>
174-
p.can('edit', withoutScope(role)),
175-
);
172+
const valid = this.sessionHost.withSession(requesterSession, () => {
173+
const p = this.privileges.for(AssignableRoles);
174+
return impersonatee.roles.every((role) =>
175+
p.can('edit', withoutScope(role)),
176+
);
177+
});
176178
if (!valid) {
177179
// Don't expose what the requester is unable to do as this could leak
178180
// private information.
@@ -230,6 +232,15 @@ export class AuthenticationService {
230232
return this.repo.waitForRootUserId();
231233
}
232234

235+
async asUser<R>(
236+
user: ID<'User'> | Session,
237+
fn: (session: Session) => Promise<R>,
238+
): Promise<R> {
239+
const session =
240+
typeof user === 'string' ? await this.sessionForUser(user) : user;
241+
return await this.sessionHost.withSession(session, () => fn(session));
242+
}
243+
233244
async sessionForUser(userId: ID): Promise<Session> {
234245
const roles = await this.repo.rolesForUser(userId);
235246
const session: Session = {

src/components/authentication/current-user.provider.ts

Lines changed: 0 additions & 85 deletions
This file was deleted.
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
11
export * from './authentication.service';
2+
export { SessionHost } from './session.host';
3+
export * from './anonymous.decorator';

src/components/authentication/login.resolver.ts

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,18 @@
11
import {
22
Args,
3-
Context,
43
Mutation,
54
Parent,
65
ResolveField,
76
Resolver,
87
} from '@nestjs/graphql';
98
import { stripIndent } from 'common-tags';
10-
import { AnonSession, type GqlContextType, type Session } from '~/common';
9+
import { AnonSession, type Session } from '~/common';
1110
import { Loader, type LoaderOf } from '~/core';
1211
import { Privileges } from '../authorization';
1312
import { Power } from '../authorization/dto';
1413
import { UserLoader } from '../user';
1514
import { User } from '../user/dto';
15+
import { Anonymous } from './anonymous.decorator';
1616
import { AuthenticationService } from './authentication.service';
1717
import { LoginInput, LoginOutput, LogoutOutput } from './dto';
1818

@@ -29,13 +29,13 @@ export class LoginResolver {
2929
@sensitive-secrets
3030
`,
3131
})
32+
@Anonymous()
3233
async login(
3334
@Args('input') input: LoginInput,
3435
@AnonSession() session: Session,
35-
@Context() context: GqlContextType,
3636
): Promise<LoginOutput> {
3737
const user = await this.authentication.login(input, session);
38-
await this.authentication.updateSession(context);
38+
await this.authentication.refreshCurrentSession();
3939
return { user };
4040
}
4141

@@ -45,12 +45,10 @@ export class LoginResolver {
4545
@sensitive-secrets
4646
`,
4747
})
48-
async logout(
49-
@AnonSession() session: Session,
50-
@Context() context: GqlContextType,
51-
): Promise<LogoutOutput> {
48+
@Anonymous()
49+
async logout(@AnonSession() session: Session): Promise<LogoutOutput> {
5250
await this.authentication.logout(session.token);
53-
await this.authentication.updateSession(context); // ensure session data is fresh
51+
await this.authentication.refreshCurrentSession(); // ensure session data is fresh
5452
return { success: true };
5553
}
5654

src/components/authentication/password.resolver.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Args, Mutation, Resolver } from '@nestjs/graphql';
22
import { stripIndent } from 'common-tags';
33
import { AnonSession, LoggedInSession, type Session } from '~/common';
4+
import { Anonymous } from './anonymous.decorator';
45
import { AuthenticationService } from './authentication.service';
56
import {
67
ChangePasswordArgs,
@@ -45,6 +46,7 @@ export class PasswordResolver {
4546
@sensitive-secrets
4647
`,
4748
})
49+
@Anonymous()
4850
async resetPassword(
4951
@Args('input') input: ResetPasswordInput,
5052
@AnonSession() session: Session,

0 commit comments

Comments
 (0)