Skip to content

Commit

Permalink
Merge pull request #112 from shapehq/develop
Browse files Browse the repository at this point in the history
Deploy to production
  • Loading branch information
simonbs authored Oct 31, 2023
2 parents cab3024 + e904303 commit 2c890ca
Show file tree
Hide file tree
Showing 15 changed files with 217 additions and 48 deletions.
49 changes: 49 additions & 0 deletions __test__/auth/SessionLockingAccessTokenService.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import SessionLockingAccessTokenService from "../../src/features/auth/domain/SessionLockingAccessTokenService"

test("It acquires a lock", async () => {
let didAcquireLock = false
const sut = new SessionLockingAccessTokenService({
async getUserId() {
return "foo"
}
}, {
makeMutex() {
return {
async acquire() {
didAcquireLock = true
},
async release() {}
}
}
}, {
async getAccessToken() {
return ""
}
})
await sut.getAccessToken()
expect(didAcquireLock).toBeTruthy()
})

test("It releases the acquired lock", async () => {
let didReleaseLock = false
const sut = new SessionLockingAccessTokenService({
async getUserId() {
return "foo"
}
}, {
makeMutex() {
return {
async acquire() {},
async release() {
didReleaseLock = true
}
}
}
}, {
async getAccessToken() {
return ""
}
})
await sut.getAccessToken()
expect(didReleaseLock).toBeTruthy()
})
15 changes: 15 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
"octokit": "^3.1.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"redis-semaphore": "^5.5.0",
"redoc": "^2.1.3",
"styled-components": "^6.1.0",
"swagger-ui-react": "^5.9.1",
Expand Down
4 changes: 4 additions & 0 deletions src/common/mutex/IMutex.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export default interface IMutex {
acquire(): Promise<void>
release(): Promise<void>
}
5 changes: 5 additions & 0 deletions src/common/mutex/IMutexFactory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import IMutex from "./IMutex"

export default interface IMutexFactory {
makeMutex(key: string): IMutex
}
32 changes: 32 additions & 0 deletions src/common/mutex/RedisMutexFactory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import Redis from "ioredis"
import IMutex from "./IMutex"
import IMutexFactory from "./IMutexFactory"
import { Mutex } from "redis-semaphore"

class RedisMutex implements IMutex {
private readonly mutex: Mutex

constructor(redis: Redis, key: string) {
this.mutex = new Mutex(redis, key)
}

async acquire(): Promise<void> {
return await this.mutex.acquire()
}

async release(): Promise<void> {
return await this.mutex.release()
}
}

export default class RedisMutexFactory implements IMutexFactory {
private readonly redis: Redis

constructor(url: string) {
this.redis = new Redis(url)
}

makeMutex(key: string): IMutex {
return new RedisMutex(this.redis, key)
}
}
16 changes: 16 additions & 0 deletions src/common/mutex/withMutex.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import IMutex from "./IMutex"

export default async function withMutex<T>(
mutex: IMutex,
f: () => Promise<T>
): Promise<T> {
await mutex.acquire()
try {
const value = await f()
await mutex.release()
return value
} catch(error) {
await mutex.release()
throw error
}
}
13 changes: 13 additions & 0 deletions src/common/session/Auth0Session.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { getSession } from "@auth0/nextjs-auth0"
import ISession from "./ISession"
import { UnauthorizedError } from "@/features/auth/domain/AuthError"

export default class Auth0Session implements ISession {
async getUserId(): Promise<string> {
const session = await getSession()
if (!session) {
throw new UnauthorizedError("User ID is unavailable because the user is not authenticated.")
}
return session.user.sub
}
}
4 changes: 4 additions & 0 deletions src/common/session/ISession.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export default interface ISession {
getUserId(): Promise<string>
}

36 changes: 0 additions & 36 deletions src/common/userData/Auth0SessionDataRepository.ts

This file was deleted.

28 changes: 28 additions & 0 deletions src/common/userData/SessionDataRepository.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import ISession from "../session/ISession"
import ISessionDataRepository from "@/common/userData/ISessionDataRepository"
import IUserDataRepository from "@/common/userData/IUserDataRepository"

export default class SessionDataRepository<T> implements ISessionDataRepository<T> {
private readonly session: ISession
private readonly repository: IUserDataRepository<T>

constructor(session: ISession, repository: IUserDataRepository<T>) {
this.session = session
this.repository = repository
}

async get(): Promise<T | null> {
const userId = await this.session.getUserId()
return await this.repository.get(userId)
}

async set(value: T): Promise<void> {
const userId = await this.session.getUserId()
return await this.repository.set(userId, value)
}

async delete(): Promise<void> {
const userId = await this.session.getUserId()
return await this.repository.delete(userId)
}
}
28 changes: 17 additions & 11 deletions src/composition.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
import AccessTokenService from "@/features/auth/domain/AccessTokenService"
import Auth0RefreshTokenReader from "@/features/auth/data/Auth0RefreshTokenReader"
import Auth0SessionDataRepository from "@/common/userData/Auth0SessionDataRepository"
import Auth0Session from "@/common/session/Auth0Session"
import CachingProjectDataSource from "@/features/projects/domain/CachingProjectDataSource"
import GitHubClient from "@/common/github/GitHubClient"
import GitHubOAuthTokenRefresher from "@/features/auth/data/GitHubOAuthTokenRefresher"
import GitHubProjectDataSource from "@/features/projects/data/GitHubProjectDataSource"
import InitialOAuthTokenService from "@/features/auth/domain/InitialOAuthTokenService"
import KeyValueUserDataRepository from "@/common//userData/KeyValueUserDataRepository"
import KeyValueUserDataRepository from "@/common/userData/KeyValueUserDataRepository"
import RedisMutexFactory from "@/common/mutex/RedisMutexFactory"
import RedisKeyValueStore from "@/common/keyValueStore/RedisKeyValueStore"
import SessionDataRepository from "@/common/userData/SessionDataRepository"
import SessionLockingAccessTokenService from "@/features/auth/domain/SessionLockingAccessTokenService"
import SessionOAuthTokenRepository from "@/features/auth/domain/SessionOAuthTokenRepository"
import SessionProjectRepository from "./features/projects/domain/SessionProjectRepository"
import SessionProjectRepository from "@/features/projects/domain/SessionProjectRepository"
import UserDataOAuthTokenRepository from "@/features/auth/domain/UserDataOAuthTokenRepository"
import authLogoutHandler from "@/common/authHandler/logout"

Expand All @@ -33,29 +36,32 @@ const oAuthTokenRepository = new KeyValueUserDataRepository(
)

export const sessionOAuthTokenRepository = new SessionOAuthTokenRepository(
new Auth0SessionDataRepository(oAuthTokenRepository)
new SessionDataRepository(new Auth0Session(), oAuthTokenRepository)
)

const gitHubOAuthTokenRefresher = new GitHubOAuthTokenRefresher({
clientId: GITHUB_CLIENT_ID,
clientSecret: GITHUB_CLIENT_SECRET
})

const accessTokenService = new AccessTokenService(
sessionOAuthTokenRepository,
gitHubOAuthTokenRefresher
)

export const gitHubClient = new GitHubClient({
appId: GITHUB_APP_ID,
clientId: GITHUB_CLIENT_ID,
clientSecret: GITHUB_CLIENT_SECRET,
privateKey: gitHubPrivateKey,
accessTokenReader: accessTokenService
accessTokenReader: new SessionLockingAccessTokenService(
new Auth0Session(),
new RedisMutexFactory(REDIS_URL),
new AccessTokenService(
sessionOAuthTokenRepository,
gitHubOAuthTokenRefresher
)
)
})

export const sessionProjectRepository = new SessionProjectRepository(
new Auth0SessionDataRepository(
new SessionDataRepository(
new Auth0Session(),
new KeyValueUserDataRepository(
new RedisKeyValueStore(REDIS_URL),
"projects"
Expand Down
3 changes: 2 additions & 1 deletion src/features/auth/domain/AccessTokenService.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import IAccessTokenService from "./IAccessTokenService"
import ISessionOAuthTokenRepository from "./ISessionOAuthTokenRepository"
import IOAuthTokenRefresher from "./IOAuthTokenRefresher"
import { UnauthorizedError } from "./AuthError"

export default class AccessTokenService {
export default class AccessTokenService implements IAccessTokenService {
private readonly tokenRepository: ISessionOAuthTokenRepository
private readonly tokenRefresher: IOAuthTokenRefresher
private readonly tokenExpirationThreshold = 5 * 60 * 1000
Expand Down
3 changes: 3 additions & 0 deletions src/features/auth/domain/IAccessTokenService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default interface IAccessTokenService {
getAccessToken(): Promise<string>
}
28 changes: 28 additions & 0 deletions src/features/auth/domain/SessionLockingAccessTokenService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import ISession from "@/common/session/ISession"
import IMutexFactory from "@/common/mutex/IMutexFactory"
import withMutex from "../../../common/mutex/withMutex"
import IAccessTokenService from "./IAccessTokenService"

export default class SessionLockingAccessTokenService implements IAccessTokenService {
private readonly session: ISession
private readonly mutexFactory: IMutexFactory
private readonly accessTokenService: IAccessTokenService

constructor(
session: ISession,
mutexFactory: IMutexFactory,
accessTokenService: IAccessTokenService
) {
this.session = session
this.mutexFactory = mutexFactory
this.accessTokenService = accessTokenService
}

async getAccessToken(): Promise<string> {
const userId = await this.session.getUserId()
const mutex = this.mutexFactory.makeMutex(`mutexAccessToken[${userId}]`)
return await withMutex(mutex, async () => {
return await this.accessTokenService.getAccessToken()
})
}
}

0 comments on commit 2c890ca

Please sign in to comment.