From a637d60b025f35f84384282e15aed94014614b76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=B8vring?= Date: Tue, 7 Nov 2023 12:47:30 +0100 Subject: [PATCH 01/42] Adds concept of guest users --- src/common/session/Auth0Session.ts | 18 ++++++++++++- src/common/session/ISession.ts | 2 +- .../CachingUserIdentityProviderReader.ts | 26 +++++++++++++++++++ .../IIsUserGuestReader.ts | 3 +++ .../IUserIdentityProviderReader.ts | 5 ++++ .../userIdentityProvider/IsUserGuestReader.ts | 16 ++++++++++++ .../UserIdentityProvider.ts | 6 +++++ 7 files changed, 74 insertions(+), 2 deletions(-) create mode 100644 src/features/auth/domain/userIdentityProvider/CachingUserIdentityProviderReader.ts create mode 100644 src/features/auth/domain/userIdentityProvider/IIsUserGuestReader.ts create mode 100644 src/features/auth/domain/userIdentityProvider/IUserIdentityProviderReader.ts create mode 100644 src/features/auth/domain/userIdentityProvider/IsUserGuestReader.ts create mode 100644 src/features/auth/domain/userIdentityProvider/UserIdentityProvider.ts diff --git a/src/common/session/Auth0Session.ts b/src/common/session/Auth0Session.ts index 0971235b..b675447b 100644 --- a/src/common/session/Auth0Session.ts +++ b/src/common/session/Auth0Session.ts @@ -1,8 +1,19 @@ import { getSession } from "@auth0/nextjs-auth0" import { UnauthorizedError } from "@/common/errors" import ISession from "./ISession" +import IIsUserGuestReader from "@/features/auth/domain/userIdentityProvider/IsUserGuestReader" + +export type Auth0SessionConfig = { + readonly isUserGuestReader: IIsUserGuestReader +} export default class Auth0Session implements ISession { + private readonly isUserGuestReader: IIsUserGuestReader + + constructor(config: Auth0SessionConfig) { + this.isUserGuestReader = config.isUserGuestReader + } + async getUserId(): Promise { const session = await getSession() if (!session) { @@ -10,4 +21,9 @@ export default class Auth0Session implements ISession { } return session.user.sub } -} \ No newline at end of file + + async getIsGuest(): Promise { + const userId = await this.getUserId() + return await this.isUserGuestReader.getIsUserGuest(userId) + } +} diff --git a/src/common/session/ISession.ts b/src/common/session/ISession.ts index 6481c29c..2496223f 100644 --- a/src/common/session/ISession.ts +++ b/src/common/session/ISession.ts @@ -1,4 +1,4 @@ export default interface ISession { getUserId(): Promise + getIsGuest(): Promise } - diff --git a/src/features/auth/domain/userIdentityProvider/CachingUserIdentityProviderReader.ts b/src/features/auth/domain/userIdentityProvider/CachingUserIdentityProviderReader.ts new file mode 100644 index 00000000..29f78760 --- /dev/null +++ b/src/features/auth/domain/userIdentityProvider/CachingUserIdentityProviderReader.ts @@ -0,0 +1,26 @@ +import IUserDataRepository from "@/common/userData/IUserDataRepository" +import IUserIdentityProviderReader from "./IUserIdentityProviderReader" +import UserIdentityProvider from "./UserIdentityProvider" + +type Repository = IUserDataRepository + +export default class CachingUserIdentityProviderReader implements IUserIdentityProviderReader { + private readonly repository: Repository + private readonly reader: IUserIdentityProviderReader + + constructor(repository: Repository, reader: IUserIdentityProviderReader) { + this.repository = repository + this.reader = reader + } + + async getUserIdentityProvider(userId: string): Promise { + const cachedValue = await this.repository.get(userId) + if (cachedValue) { + return cachedValue as UserIdentityProvider + } else { + const userIdentity = await this.reader.getUserIdentityProvider(userId) + await this.repository.set(userId, userIdentity.toString()) + return userIdentity + } + } +} diff --git a/src/features/auth/domain/userIdentityProvider/IIsUserGuestReader.ts b/src/features/auth/domain/userIdentityProvider/IIsUserGuestReader.ts new file mode 100644 index 00000000..da34aa0d --- /dev/null +++ b/src/features/auth/domain/userIdentityProvider/IIsUserGuestReader.ts @@ -0,0 +1,3 @@ +export default interface IIsUserGuestReader { + getIsUserGuest(userId: string): Promise +} \ No newline at end of file diff --git a/src/features/auth/domain/userIdentityProvider/IUserIdentityProviderReader.ts b/src/features/auth/domain/userIdentityProvider/IUserIdentityProviderReader.ts new file mode 100644 index 00000000..7b70ffec --- /dev/null +++ b/src/features/auth/domain/userIdentityProvider/IUserIdentityProviderReader.ts @@ -0,0 +1,5 @@ +import UserIdentityProvider from "./UserIdentityProvider" + +export default interface IUserIdentityProviderReader { + getUserIdentityProvider(userId: string): Promise +} diff --git a/src/features/auth/domain/userIdentityProvider/IsUserGuestReader.ts b/src/features/auth/domain/userIdentityProvider/IsUserGuestReader.ts new file mode 100644 index 00000000..d2e00648 --- /dev/null +++ b/src/features/auth/domain/userIdentityProvider/IsUserGuestReader.ts @@ -0,0 +1,16 @@ +import IIsUserGuestReader from "./IIsUserGuestReader" +import IUserIdentityProviderReader from "./IUserIdentityProviderReader" +import UserIdentityProvider from "./UserIdentityProvider" + +export default class IsUserGuestReader implements IIsUserGuestReader { + private readonly userIdentityProviderReader: IUserIdentityProviderReader + + constructor(userIdentityProviderReader: IUserIdentityProviderReader) { + this.userIdentityProviderReader = userIdentityProviderReader + } + + async getIsUserGuest(userId: string): Promise { + const userIdentityProvider = await this.userIdentityProviderReader.getUserIdentityProvider(userId) + return userIdentityProvider == UserIdentityProvider.USERNAME_PASSWORD + } +} \ No newline at end of file diff --git a/src/features/auth/domain/userIdentityProvider/UserIdentityProvider.ts b/src/features/auth/domain/userIdentityProvider/UserIdentityProvider.ts new file mode 100644 index 00000000..394cafa4 --- /dev/null +++ b/src/features/auth/domain/userIdentityProvider/UserIdentityProvider.ts @@ -0,0 +1,6 @@ +enum UserIdentityProvider { + GITHUB = "github", + USERNAME_PASSWORD = "username_password" +} + +export default UserIdentityProvider From 0a0b6ebbdc661dac63dac5c1612961a5e6530d36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=B8vring?= Date: Tue, 7 Nov 2023 12:50:25 +0100 Subject: [PATCH 02/42] Separates access token and credentials transferring --- ...redentialsTransferringLogInHandler.test.ts | 41 +++++- __test__/auth/GuestAccessTokenService.test.ts | 49 +++++++ .../auth/GuestCredentialsTransferrer.test.ts | 47 +++++++ ...test.ts => HostAccessTokenService.test.ts} | 8 +- ....ts => HostCredentialsTransferrer.test.ts} | 8 +- .../auth/SessionAccessTokenService.test.ts | 121 ++++++++++++++++++ src/composition.ts | 105 +++++++++++---- .../data/Auth0UserIdentityProviderReader.ts | 40 ++++++ ...GitHubInstallationAccessTokenDataSource.ts | 38 ++++++ .../accessToken/GuestAccessTokenService.ts | 49 +++++++ ...enService.ts => HostAccessTokenService.ts} | 8 +- .../accessToken/SessionAccessTokenService.ts | 42 ++++++ .../GuestCredentialsTransferrer.ts | 28 ++++ ...errer.ts => HostCredentialsTransferrer.ts} | 8 +- .../CredentialsTransferringLogInHandler.ts | 24 +++- 15 files changed, 565 insertions(+), 51 deletions(-) create mode 100644 __test__/auth/GuestAccessTokenService.test.ts create mode 100644 __test__/auth/GuestCredentialsTransferrer.test.ts rename __test__/auth/{AccessTokenService.test.ts => HostAccessTokenService.test.ts} (90%) rename __test__/auth/{CredentialsTransferrer.test.ts => HostCredentialsTransferrer.test.ts} (89%) create mode 100644 __test__/auth/SessionAccessTokenService.test.ts create mode 100644 src/features/auth/data/Auth0UserIdentityProviderReader.ts create mode 100644 src/features/auth/data/GitHubInstallationAccessTokenDataSource.ts create mode 100644 src/features/auth/domain/accessToken/GuestAccessTokenService.ts rename src/features/auth/domain/accessToken/{AccessTokenService.ts => HostAccessTokenService.ts} (87%) create mode 100644 src/features/auth/domain/accessToken/SessionAccessTokenService.ts create mode 100644 src/features/auth/domain/credentialsTransfer/GuestCredentialsTransferrer.ts rename src/features/auth/domain/credentialsTransfer/{CredentialsTransferrer.ts => HostCredentialsTransferrer.ts} (81%) diff --git a/__test__/auth/CredentialsTransferringLogInHandler.test.ts b/__test__/auth/CredentialsTransferringLogInHandler.test.ts index 90b106fe..ff33407c 100644 --- a/__test__/auth/CredentialsTransferringLogInHandler.test.ts +++ b/__test__/auth/CredentialsTransferringLogInHandler.test.ts @@ -1,12 +1,43 @@ import CredentialsTransferringLogInHandler from "../../src/features/auth/domain/logIn/CredentialsTransferringLogInHandler" -test("It transfers credentials", async () => { - let didTransferCredentials = false +test("It transfers credentials for guest", async () => { + let didTransferGuestCredentials = false const sut = new CredentialsTransferringLogInHandler({ - async transferCredentials() { - didTransferCredentials = true + isUserGuestReader: { + async getIsUserGuest() { + return true + } + }, + guestCredentialsTransferrer: { + async transferCredentials() { + didTransferGuestCredentials = true + } + }, + hostCredentialsTransferrer: { + async transferCredentials() {} } }) await sut.handleLogIn("1234") - expect(didTransferCredentials).toBeTruthy() + expect(didTransferGuestCredentials).toBeTruthy() +}) + +test("It transfers credentials for host", async () => { + let didTransferHostCredentials = false + const sut = new CredentialsTransferringLogInHandler({ + isUserGuestReader: { + async getIsUserGuest() { + return false + } + }, + guestCredentialsTransferrer: { + async transferCredentials() {} + }, + hostCredentialsTransferrer: { + async transferCredentials() { + didTransferHostCredentials = true + } + } + }) + await sut.handleLogIn("1234") + expect(didTransferHostCredentials).toBeTruthy() }) diff --git a/__test__/auth/GuestAccessTokenService.test.ts b/__test__/auth/GuestAccessTokenService.test.ts new file mode 100644 index 00000000..ea271ce3 --- /dev/null +++ b/__test__/auth/GuestAccessTokenService.test.ts @@ -0,0 +1,49 @@ +import GuestAccessTokenService from "../../src/features/auth/domain/accessToken/GuestAccessTokenService" + +test("It gets the access token for the user", async () => { + let readUserId: string | undefined + const sut = new GuestAccessTokenService({ + userIdReader: { + async getUserId() { + return "1234" + } + }, + repository: { + async get(userId) { + readUserId = userId + return "foo" + }, + async set() {} + }, + dataSource: { + async getAccessToken() { + return "foo" + } + } + }) + const accessToken = await sut.getAccessToken() + expect(readUserId).toBe("1234") + expect(accessToken).toBe("foo") +}) + +test("It throws an error when the access token is null", async () => { + const sut = new GuestAccessTokenService({ + userIdReader: { + async getUserId() { + return "1234" + } + }, + repository: { + async get() { + return null + }, + async set() {} + }, + dataSource: { + async getAccessToken() { + return "foo" + } + } + }) + expect(sut.getAccessToken()).rejects.toThrow() +}) diff --git a/__test__/auth/GuestCredentialsTransferrer.test.ts b/__test__/auth/GuestCredentialsTransferrer.test.ts new file mode 100644 index 00000000..6bc17fe0 --- /dev/null +++ b/__test__/auth/GuestCredentialsTransferrer.test.ts @@ -0,0 +1,47 @@ +import GuestCredentialsTransferrer from "../../src/features/auth/domain/credentialsTransfer/GuestCredentialsTransferrer" + +test("It fetches an access token for the specified user from the data source", async () => { + let didFetchAccessToken = false + const sut = new GuestCredentialsTransferrer({ + dataSource: { + async getAccessToken() { + didFetchAccessToken = true + return "foo" + } + }, + repository: { + async get() { + return "foo" + }, + async set() {}, + async delete() {}, + } + }) + await sut.transferCredentials("123") + expect(didFetchAccessToken).toBeTruthy() +}) + +test("It stores the feteched access token for the specified user", async () => { + let storedUserId: string | undefined + let storedAccessToken: string | undefined + const sut = new GuestCredentialsTransferrer({ + dataSource: { + async getAccessToken() { + return "foo" + } + }, + repository: { + async get() { + return "foo" + }, + async set(userId, accessToken) { + storedUserId = userId + storedAccessToken = accessToken + }, + async delete() {}, + } + }) + await sut.transferCredentials("123") + expect(storedUserId).toBe("123") + expect(storedAccessToken).toBe("foo") +}) diff --git a/__test__/auth/AccessTokenService.test.ts b/__test__/auth/HostAccessTokenService.test.ts similarity index 90% rename from __test__/auth/AccessTokenService.test.ts rename to __test__/auth/HostAccessTokenService.test.ts index 62a986c7..05babebe 100644 --- a/__test__/auth/AccessTokenService.test.ts +++ b/__test__/auth/HostAccessTokenService.test.ts @@ -1,9 +1,9 @@ -import AccessTokenService from "../../src/features/auth/domain/accessToken/AccessTokenService" +import HostAccessTokenService from "../../src/features/auth/domain/accessToken/HostAccessTokenService" import OAuthToken from "../../src/features/auth/domain/oAuthToken/OAuthToken" test("It gets the access token for the user", async () => { let readUserID: string | undefined - const sut = new AccessTokenService({ + const sut = new HostAccessTokenService({ userIdReader: { async getUserId() { return "1234" @@ -30,7 +30,7 @@ test("It gets the access token for the user", async () => { test("It refreshes OAuth using stored refresh token", async () => { let usedRefreshToken: string | undefined - const sut = new AccessTokenService({ + const sut = new HostAccessTokenService({ userIdReader: { async getUserId() { return "1234" @@ -57,7 +57,7 @@ test("It refreshes OAuth using stored refresh token", async () => { test("It stores the new OAuth token for the user", async () => { let storedUserId: string | undefined let storedOAuthToken: OAuthToken | undefined - const sut = new AccessTokenService({ + const sut = new HostAccessTokenService({ userIdReader: { async getUserId() { return "1234" diff --git a/__test__/auth/CredentialsTransferrer.test.ts b/__test__/auth/HostCredentialsTransferrer.test.ts similarity index 89% rename from __test__/auth/CredentialsTransferrer.test.ts rename to __test__/auth/HostCredentialsTransferrer.test.ts index 7723e8c1..7ea596bd 100644 --- a/__test__/auth/CredentialsTransferrer.test.ts +++ b/__test__/auth/HostCredentialsTransferrer.test.ts @@ -1,9 +1,9 @@ -import CredentialsTransferrer from "../../src/features/auth/domain/credentialsTransfer/CredentialsTransferrer" +import HostCredentialsTransferrer from "../../src/features/auth/domain/credentialsTransfer/HostCredentialsTransferrer" import OAuthToken from "../../src/features/auth/domain/oAuthToken/OAuthToken" test("It fetches refresh token for specified user", async () => { let fetchedUserId: string | undefined - const sut = new CredentialsTransferrer({ + const sut = new HostCredentialsTransferrer({ refreshTokenReader: { async getRefreshToken(userId) { fetchedUserId = userId @@ -29,7 +29,7 @@ test("It fetches refresh token for specified user", async () => { test("It refreshes the fetched refresh token", async () => { let refreshedRefreshToken: string | undefined - const sut = new CredentialsTransferrer({ + const sut = new HostCredentialsTransferrer({ refreshTokenReader: { async getRefreshToken() { return "helloworld" @@ -56,7 +56,7 @@ test("It refreshes the fetched refresh token", async () => { test("It stores the refreshed auth token for the user", async () => { let storedAuthToken: OAuthToken | undefined let storedUserId: string | undefined - const sut = new CredentialsTransferrer({ + const sut = new HostCredentialsTransferrer({ refreshTokenReader: { async getRefreshToken() { return "helloworld" diff --git a/__test__/auth/SessionAccessTokenService.test.ts b/__test__/auth/SessionAccessTokenService.test.ts new file mode 100644 index 00000000..07f5b905 --- /dev/null +++ b/__test__/auth/SessionAccessTokenService.test.ts @@ -0,0 +1,121 @@ +import SessionAccessTokenService from "../../src/features/auth/domain/accessToken/SessionAccessTokenService" + +test("It reads the access token for a guest user", async () => { + let didReadAccessToken = false + const sut = new SessionAccessTokenService({ + isGuestReader: { + async getIsGuest() { + return true + } + }, + guestAccessTokenService: { + async getAccessToken() { + didReadAccessToken = true + return "oldAccessToken" + }, + async refreshAccessToken() { + return "newAccessToken" + } + }, + hostAccessTokenService: { + async getAccessToken() { + return "oldAccessToken" + }, + async refreshAccessToken() { + return "newAccessToken" + } + } + }) + await sut.getAccessToken() + expect(didReadAccessToken).toBeTruthy() +}) + +test("It refreshes the access token for a guest user", async () => { + let didRefreshAccessToken = false + const sut = new SessionAccessTokenService({ + isGuestReader: { + async getIsGuest() { + return true + } + }, + guestAccessTokenService: { + async getAccessToken() { + return "oldAccessToken" + }, + async refreshAccessToken() { + didRefreshAccessToken = true + return "newAccessToken" + } + }, + hostAccessTokenService: { + async getAccessToken() { + return "oldAccessToken" + }, + async refreshAccessToken() { + return "newAccessToken" + } + } + }) + await sut.refreshAccessToken("oldAccessToken") + expect(didRefreshAccessToken).toBeTruthy() +}) + +test("It reads the access token for a host user", async () => { + let didReadAccessToken = false + const sut = new SessionAccessTokenService({ + isGuestReader: { + async getIsGuest() { + return false + } + }, + guestAccessTokenService: { + async getAccessToken() { + return "oldAccessToken" + }, + async refreshAccessToken() { + return "newAccessToken" + } + }, + hostAccessTokenService: { + async getAccessToken() { + didReadAccessToken = true + return "oldAccessToken" + }, + async refreshAccessToken() { + return "newAccessToken" + } + } + }) + await sut.getAccessToken() + expect(didReadAccessToken).toBeTruthy() +}) + +test("It refreshes the access token for a host user", async () => { + let didRefreshAccessToken = false + const sut = new SessionAccessTokenService({ + isGuestReader: { + async getIsGuest() { + return false + } + }, + guestAccessTokenService: { + async getAccessToken() { + return "oldAccessToken" + }, + async refreshAccessToken() { + return "newAccessToken" + } + }, + hostAccessTokenService: { + async getAccessToken() { + return "oldAccessToken" + }, + async refreshAccessToken() { + didRefreshAccessToken = true + return "newAccessToken" + } + } + }) + await sut.refreshAccessToken("oldAccessToken") + expect(didRefreshAccessToken).toBeTruthy() +}) diff --git a/src/composition.ts b/src/composition.ts index 916d5814..5d3f9c2d 100644 --- a/src/composition.ts +++ b/src/composition.ts @@ -1,25 +1,32 @@ import AccessTokenRefreshingGitHubClient from "@/common/github/AccessTokenRefreshingGitHubClient" -import AccessTokenService from "@/features/auth/domain/accessToken/AccessTokenService" import Auth0RefreshTokenReader from "@/features/auth/data/Auth0RefreshTokenReader" +import Auth0UserIdentityProviderReader from "./features/auth/data/Auth0UserIdentityProviderReader" import Auth0Session from "@/common/session/Auth0Session" import CachingProjectDataSource from "@/features/projects/domain/CachingProjectDataSource" +import CachingUserIdentityProviderReader from "./features/auth/domain/userIdentityProvider/CachingUserIdentityProviderReader" import CompositeLogOutHandler from "@/features/auth/domain/logOut/CompositeLogOutHandler" -import CredentialsTransferrer from "@/features/auth/domain/credentialsTransfer/CredentialsTransferrer" import CredentialsTransferringLogInHandler from "@/features/auth/domain/logIn/CredentialsTransferringLogInHandler" import ErrorIgnoringLogOutHandler from "@/features/auth/domain/logOut/ErrorIgnoringLogOutHandler" import GitHubClient from "@/common/github/GitHubClient" import GitHubOAuthTokenRefresher from "@/features/auth/data/GitHubOAuthTokenRefresher" import GitHubOrganizationSessionValidator from "@/common/session/GitHubOrganizationSessionValidator" import GitHubProjectDataSource from "@/features/projects/data/GitHubProjectDataSource" +import GitHubInstallationAccessTokenDataSource from "@/features/auth/data/GitHubInstallationAccessTokenDataSource" +import GuestAccessTokenService from "@/features/auth/domain/accessToken/GuestAccessTokenService" +import GuestCredentialsTransferrer from "@/features/auth/domain/credentialsTransfer/GuestCredentialsTransferrer" +import HostAccessTokenService from "@/features/auth/domain/accessToken/HostAccessTokenService" +import HostCredentialsTransferrer from "@/features/auth/domain/credentialsTransfer/HostCredentialsTransferrer" +import IsUserGuestReader from "@/features/auth/domain/userIdentityProvider/IsUserGuestReader" import KeyValueUserDataRepository from "@/common/userData/KeyValueUserDataRepository" import LockingAccessTokenService from "@/features/auth/domain/accessToken/LockingAccessTokenService" +import OAuthTokenRepository from "@/features/auth/domain/oAuthToken/OAuthTokenRepository" import OnlyStaleRefreshingAccessTokenService from "@/features/auth/domain/accessToken/OnlyStaleRefreshingAccessTokenService" import ProjectRepository from "@/features/projects/domain/ProjectRepository" import RedisKeyedMutexFactory from "@/common/mutex/RedisKeyedMutexFactory" import RedisKeyValueStore from "@/common/keyValueStore/RedisKeyValueStore" +import SessionAccessTokenService from "@/features/auth/domain/accessToken/SessionAccessTokenService" import SessionMutexFactory from "@/common/mutex/SessionMutexFactory" import SessionValidatingProjectDataSource from "@/features/projects/domain/SessionValidatingProjectDataSource" -import OAuthTokenRepository from "@/features/auth/domain/oAuthToken/OAuthTokenRepository" import UserDataCleanUpLogOutHandler from "@/features/auth/domain/logOut/UserDataCleanUpLogOutHandler" const { @@ -34,9 +41,34 @@ const { REDIS_URL } = process.env -const gitHubPrivateKey = Buffer.from(GITHUB_PRIVATE_KEY_BASE_64, "base64").toString("utf-8") +const auth0ManagementCredentials = { + domain: AUTH0_MANAGEMENT_DOMAIN, + clientId: AUTH0_MANAGEMENT_CLIENT_ID, + clientSecret: AUTH0_MANAGEMENT_CLIENT_SECRET +} + +const gitHubAppCredentials = { + appId: GITHUB_APP_ID, + clientId: GITHUB_CLIENT_ID, + clientSecret: GITHUB_CLIENT_SECRET, + privateKey: Buffer + .from(GITHUB_PRIVATE_KEY_BASE_64, "base64") + .toString("utf-8") +} + +const userIdentityProviderRepository = new KeyValueUserDataRepository( + new RedisKeyValueStore(REDIS_URL), + "userIdentityProvider" +) + +export const userIdentityProviderReader = new CachingUserIdentityProviderReader( + userIdentityProviderRepository, + new Auth0UserIdentityProviderReader(auth0ManagementCredentials) +) -export const session = new Auth0Session() +export const session = new Auth0Session({ + isUserGuestReader: new IsUserGuestReader(userIdentityProviderReader) +}) export const oAuthTokenRepository = new OAuthTokenRepository( new KeyValueUserDataRepository( @@ -45,6 +77,16 @@ export const oAuthTokenRepository = new OAuthTokenRepository( ) ) +const gitHubOAuthTokenRefresher = new GitHubOAuthTokenRefresher({ + clientId: gitHubAppCredentials.clientId, + clientSecret: gitHubAppCredentials.clientSecret +}) + +const accessTokenRepository = new KeyValueUserDataRepository( + new RedisKeyValueStore(REDIS_URL), + "accessToken" +) + const accessTokenService = new LockingAccessTokenService( new SessionMutexFactory( new RedisKeyedMutexFactory(REDIS_URL), @@ -52,12 +94,20 @@ const accessTokenService = new LockingAccessTokenService( "mutexAccessToken" ), new OnlyStaleRefreshingAccessTokenService( - new AccessTokenService({ - userIdReader: session, - repository: oAuthTokenRepository, - refresher: new GitHubOAuthTokenRefresher({ - clientId: GITHUB_CLIENT_ID, - clientSecret: GITHUB_CLIENT_SECRET + new SessionAccessTokenService({ + isGuestReader: session, + guestAccessTokenService: new GuestAccessTokenService({ + userIdReader: session, + repository: accessTokenRepository, + dataSource: new GitHubInstallationAccessTokenDataSource({ + ...gitHubAppCredentials, + organization: GITHUB_ORGANIZATION_NAME + }) + }), + hostAccessTokenService: new HostAccessTokenService({ + userIdReader: session, + repository: oAuthTokenRepository, + refresher: gitHubOAuthTokenRefresher }) }) ) @@ -66,10 +116,7 @@ const accessTokenService = new LockingAccessTokenService( export const gitHubClient = new AccessTokenRefreshingGitHubClient( accessTokenService, new GitHubClient({ - appId: GITHUB_APP_ID, - clientId: GITHUB_CLIENT_ID, - clientSecret: GITHUB_CLIENT_SECRET, - privateKey: gitHubPrivateKey, + ...gitHubAppCredentials, accessTokenReader: accessTokenService }) ) @@ -100,25 +147,31 @@ export const projectDataSource = new CachingProjectDataSource( projectRepository ) -export const logInHandler = new CredentialsTransferringLogInHandler( - new CredentialsTransferrer({ +export const logInHandler = new CredentialsTransferringLogInHandler({ + isUserGuestReader: new IsUserGuestReader( + userIdentityProviderReader + ), + guestCredentialsTransferrer: new GuestCredentialsTransferrer({ + repository: accessTokenRepository, + dataSource: new GitHubInstallationAccessTokenDataSource({ + ...gitHubAppCredentials, + organization: GITHUB_ORGANIZATION_NAME + }), + }), + hostCredentialsTransferrer: new HostCredentialsTransferrer({ refreshTokenReader: new Auth0RefreshTokenReader({ - domain: AUTH0_MANAGEMENT_DOMAIN, - clientId: AUTH0_MANAGEMENT_CLIENT_ID, - clientSecret: AUTH0_MANAGEMENT_CLIENT_SECRET, + ...auth0ManagementCredentials, connection: "github" }), - oAuthTokenRefresher: new GitHubOAuthTokenRefresher({ - clientId: GITHUB_CLIENT_ID, - clientSecret: GITHUB_CLIENT_SECRET - }), + oAuthTokenRefresher: gitHubOAuthTokenRefresher, oAuthTokenRepository: oAuthTokenRepository }) -) +}) export const logOutHandler = new ErrorIgnoringLogOutHandler( new CompositeLogOutHandler([ new UserDataCleanUpLogOutHandler(session, projectUserDataRepository), - new UserDataCleanUpLogOutHandler(session, oAuthTokenRepository) + new UserDataCleanUpLogOutHandler(session, oAuthTokenRepository), + new UserDataCleanUpLogOutHandler(session, userIdentityProviderRepository) ]) ) diff --git a/src/features/auth/data/Auth0UserIdentityProviderReader.ts b/src/features/auth/data/Auth0UserIdentityProviderReader.ts new file mode 100644 index 00000000..3c17f218 --- /dev/null +++ b/src/features/auth/data/Auth0UserIdentityProviderReader.ts @@ -0,0 +1,40 @@ +import { ManagementClient } from "auth0" +import UserIdentityProvider from "../domain/userIdentityProvider/UserIdentityProvider" +import IUserIdentityProviderReader from "../domain/userIdentityProvider/IUserIdentityProviderReader" +import { UnauthorizedError } from "@/common/errors" + +interface Auth0UserIdentityProviderReaderConfig { + readonly domain: string + readonly clientId: string + readonly clientSecret: string +} + +export default class Auth0UserIdentityProviderReader implements IUserIdentityProviderReader { + private readonly managementClient: ManagementClient + + constructor(config: Auth0UserIdentityProviderReaderConfig) { + this.managementClient = new ManagementClient({ + domain: config.domain, + clientId: config.clientId, + clientSecret: config.clientSecret + }) + } + + async getUserIdentityProvider(userId: string): Promise { + const response = await this.managementClient.users.get({ id: userId }) + const identities = response.data.identities + const gitHubIdentity = identities.find(e => { + return e.connection.toLowerCase() === "github" + }) + const usernamePasswordIdentity = identities.find(e => { + return e.connection.toLowerCase() === "username-password-authentication" + }) + if (gitHubIdentity) { + return UserIdentityProvider.GITHUB + } else if (usernamePasswordIdentity) { + return UserIdentityProvider.USERNAME_PASSWORD + } else { + throw new UnauthorizedError() + } + } +} diff --git a/src/features/auth/data/GitHubInstallationAccessTokenDataSource.ts b/src/features/auth/data/GitHubInstallationAccessTokenDataSource.ts new file mode 100644 index 00000000..e4bc7319 --- /dev/null +++ b/src/features/auth/data/GitHubInstallationAccessTokenDataSource.ts @@ -0,0 +1,38 @@ +import { Octokit } from "octokit" +import { createAppAuth } from "@octokit/auth-app" + +type GitHubInstallationAccessTokenRefresherConfig = { + readonly appId: string + readonly clientId: string + readonly clientSecret: string + readonly privateKey: string + readonly organization: string +} + +export default class GitHubInstallationAccessTokenRefresher { + private readonly config: GitHubInstallationAccessTokenRefresherConfig + + constructor(config: GitHubInstallationAccessTokenRefresherConfig) { + this.config = config + } + + async getAccessToken(): Promise { + const auth = createAppAuth({ + appId: this.config.appId, + clientId: this.config.clientId, + clientSecret: this.config.clientSecret, + privateKey: this.config.privateKey + }) + const appAuth = await auth({ type: "app" }) + const octokit = new Octokit({ auth: appAuth.token }) + const response = await octokit.rest.apps.getOrgInstallation({ + org: this.config.organization + }) + const installation = response.data + const installationAuth = await auth({ + type: "installation", + installationId: installation.id + }) + return installationAuth.token + } +} diff --git a/src/features/auth/domain/accessToken/GuestAccessTokenService.ts b/src/features/auth/domain/accessToken/GuestAccessTokenService.ts new file mode 100644 index 00000000..71a4ca46 --- /dev/null +++ b/src/features/auth/domain/accessToken/GuestAccessTokenService.ts @@ -0,0 +1,49 @@ +import { UnauthorizedError } from "../../../../common/errors" +import IAccessTokenService from "./IAccessTokenService" + +export interface IUserIDReader { + getUserId(): Promise +} + +export interface Repository { + get(userId: string): Promise + set(userId: string, token: string): Promise +} + +export interface DataSource { + getAccessToken(): Promise +} + +export type GuestAccessTokenServiceConfig = { + readonly userIdReader: IUserIDReader + readonly repository: Repository + readonly dataSource: DataSource +} + +export default class GuestAccessTokenService implements IAccessTokenService { + private readonly userIdReader: IUserIDReader + private readonly repository: Repository + private readonly dataSource: DataSource + + constructor(config: GuestAccessTokenServiceConfig) { + this.userIdReader = config.userIdReader + this.repository = config.repository + this.dataSource = config.dataSource + } + + async getAccessToken(): Promise { + const userId = await this.userIdReader.getUserId() + const accessToken = await this.repository.get(userId) + if (!accessToken) { + throw new UnauthorizedError(`No access token found for user with ID ${userId}`) + } + return accessToken + } + + async refreshAccessToken(_accessToken: string): Promise { + const userId = await this.userIdReader.getUserId() + const newAccessToken = await this.dataSource.getAccessToken() + await this.repository.set(userId, newAccessToken) + return newAccessToken + } +} \ No newline at end of file diff --git a/src/features/auth/domain/accessToken/AccessTokenService.ts b/src/features/auth/domain/accessToken/HostAccessTokenService.ts similarity index 87% rename from src/features/auth/domain/accessToken/AccessTokenService.ts rename to src/features/auth/domain/accessToken/HostAccessTokenService.ts index 729f40c3..7b400082 100644 --- a/src/features/auth/domain/accessToken/AccessTokenService.ts +++ b/src/features/auth/domain/accessToken/HostAccessTokenService.ts @@ -6,18 +6,18 @@ export interface IUserIDReader { getUserId(): Promise } -type AccessTokenServiceConfig = { +type HostAccessTokenServiceConfig = { readonly userIdReader: IUserIDReader readonly repository: IOAuthTokenRepository readonly refresher: IOAuthTokenRefresher } -export default class AccessTokenService implements IAccessTokenService { +export default class HostAccessTokenService implements IAccessTokenService { private readonly userIdReader: IUserIDReader private readonly repository: IOAuthTokenRepository private readonly refresher: IOAuthTokenRefresher - constructor(config: AccessTokenServiceConfig) { + constructor(config: HostAccessTokenServiceConfig) { this.userIdReader = config.userIdReader this.repository = config.repository this.refresher = config.refresher @@ -36,4 +36,4 @@ export default class AccessTokenService implements IAccessTokenService { await this.repository.set(userId, newOAuthToken) return newOAuthToken.accessToken } -} +} \ No newline at end of file diff --git a/src/features/auth/domain/accessToken/SessionAccessTokenService.ts b/src/features/auth/domain/accessToken/SessionAccessTokenService.ts new file mode 100644 index 00000000..e562520d --- /dev/null +++ b/src/features/auth/domain/accessToken/SessionAccessTokenService.ts @@ -0,0 +1,42 @@ +import IAccessTokenService from "./IAccessTokenService" + +export interface IIsGuestReader { + getIsGuest(): Promise +} + +interface SessionAccessTokenServiceConfig { + readonly isGuestReader: IIsGuestReader + readonly guestAccessTokenService: IAccessTokenService + readonly hostAccessTokenService: IAccessTokenService +} + +export default class SessionAccessTokenService implements IAccessTokenService { + private readonly isGuestReader: IIsGuestReader + private readonly guestAccessTokenService: IAccessTokenService + private readonly hostAccessTokenService: IAccessTokenService + + constructor(config: SessionAccessTokenServiceConfig) { + this.isGuestReader = config.isGuestReader + this.guestAccessTokenService = config.guestAccessTokenService + this.hostAccessTokenService = config.hostAccessTokenService + } + + async getAccessToken(): Promise { + const service = await this.getService() + return await service.getAccessToken() + } + + async refreshAccessToken(accessToken: string): Promise { + const service = await this.getService() + return await service.refreshAccessToken(accessToken) + } + + private async getService() { + const isGuest = await this.isGuestReader.getIsGuest() + if (isGuest) { + return this.guestAccessTokenService + } else { + return this.hostAccessTokenService + } + } +} \ No newline at end of file diff --git a/src/features/auth/domain/credentialsTransfer/GuestCredentialsTransferrer.ts b/src/features/auth/domain/credentialsTransfer/GuestCredentialsTransferrer.ts new file mode 100644 index 00000000..2e756856 --- /dev/null +++ b/src/features/auth/domain/credentialsTransfer/GuestCredentialsTransferrer.ts @@ -0,0 +1,28 @@ +import ICredentialsTransferrer from "./ICredentialsTransferrer" +import IUserDataRepository from "@/common/userData/IUserDataRepository" + +interface DataSource { + getAccessToken(): Promise +} + +type Repository = IUserDataRepository + +type GuestCredentialsTransferrerConfig = { + readonly dataSource: DataSource + readonly repository: Repository +} + +export default class GuestCredentialsTransferrer implements ICredentialsTransferrer { + private readonly dataSource: DataSource + private readonly repository: Repository + + constructor(config: GuestCredentialsTransferrerConfig) { + this.dataSource = config.dataSource + this.repository = config.repository + } + + async transferCredentials(userId: string): Promise { + const accessToken = await this.dataSource.getAccessToken() + await this.repository.set(userId, accessToken) + } +} diff --git a/src/features/auth/domain/credentialsTransfer/CredentialsTransferrer.ts b/src/features/auth/domain/credentialsTransfer/HostCredentialsTransferrer.ts similarity index 81% rename from src/features/auth/domain/credentialsTransfer/CredentialsTransferrer.ts rename to src/features/auth/domain/credentialsTransfer/HostCredentialsTransferrer.ts index c061a2cb..fd520e74 100644 --- a/src/features/auth/domain/credentialsTransfer/CredentialsTransferrer.ts +++ b/src/features/auth/domain/credentialsTransfer/HostCredentialsTransferrer.ts @@ -6,18 +6,18 @@ export interface IRefreshTokenReader { getRefreshToken(userId: string): Promise } -type CredentialsTransferrerConfig = { +type HostCredentialsTransferrerConfig = { readonly refreshTokenReader: IRefreshTokenReader readonly oAuthTokenRefresher: IOAuthTokenRefresher readonly oAuthTokenRepository: IOAuthTokenRepository } -export default class CredentialsTransferrer implements ICredentialsTransferrer { +export default class HostCredentialsTransferrer implements ICredentialsTransferrer { private readonly refreshTokenReader: IRefreshTokenReader private readonly oAuthTokenRefresher: IOAuthTokenRefresher private readonly oAuthTokenRepository: IOAuthTokenRepository - constructor(config: CredentialsTransferrerConfig) { + constructor(config: HostCredentialsTransferrerConfig) { this.refreshTokenReader = config.refreshTokenReader this.oAuthTokenRefresher = config.oAuthTokenRefresher this.oAuthTokenRepository = config.oAuthTokenRepository @@ -26,6 +26,6 @@ export default class CredentialsTransferrer implements ICredentialsTransferrer { async transferCredentials(userId: string): Promise { const refreshToken = await this.refreshTokenReader.getRefreshToken(userId) const authToken = await this.oAuthTokenRefresher.refreshOAuthToken(refreshToken) - await this.oAuthTokenRepository.set(userId, authToken) + this.oAuthTokenRepository.set(userId, authToken) } } diff --git a/src/features/auth/domain/logIn/CredentialsTransferringLogInHandler.ts b/src/features/auth/domain/logIn/CredentialsTransferringLogInHandler.ts index 618b2713..e89389f3 100644 --- a/src/features/auth/domain/logIn/CredentialsTransferringLogInHandler.ts +++ b/src/features/auth/domain/logIn/CredentialsTransferringLogInHandler.ts @@ -1,18 +1,34 @@ import ICredentialsTransferrer from "../credentialsTransfer/ICredentialsTransferrer" +import IIsUserGuestReader from "../userIdentityProvider/IIsUserGuestReader" import ILogInHandler from "./ILogInHandler" export interface IRefreshTokenReader { getRefreshToken(userId: string): Promise } +type CredentialsTransferringLogInHandlerConfig = { + readonly isUserGuestReader: IIsUserGuestReader + readonly guestCredentialsTransferrer: ICredentialsTransferrer + readonly hostCredentialsTransferrer: ICredentialsTransferrer +} + export default class CredentialsTransferringLogInHandler implements ILogInHandler { - private readonly credentialsTransferrer: ICredentialsTransferrer + private readonly isUserGuestReader: IIsUserGuestReader + private readonly guestCredentialsTransferrer: ICredentialsTransferrer + private readonly hostCredentialsTransferrer: ICredentialsTransferrer - constructor(credentialsTransferrer: ICredentialsTransferrer) { - this.credentialsTransferrer = credentialsTransferrer + constructor(config: CredentialsTransferringLogInHandlerConfig) { + this.isUserGuestReader = config.isUserGuestReader + this.guestCredentialsTransferrer = config.guestCredentialsTransferrer + this.hostCredentialsTransferrer = config.hostCredentialsTransferrer } async handleLogIn(userId: string): Promise { - await this.credentialsTransferrer.transferCredentials(userId) + const isGuest = await this.isUserGuestReader.getIsUserGuest(userId) + if (isGuest) { + this.guestCredentialsTransferrer.transferCredentials(userId) + } else { + this.hostCredentialsTransferrer.transferCredentials(userId) + } } } From c1df85af1ddf075d3b388f71f77819cfe236daab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=B8vring?= Date: Tue, 7 Nov 2023 12:50:50 +0100 Subject: [PATCH 03/42] Ensures guest sessions are always valid --- .../session/AlwaysValidSessionValidator.ts | 7 +++++ src/common/session/SessionValidator.ts | 29 +++++++++++++++++++ src/composition.ts | 14 ++++++--- 3 files changed, 46 insertions(+), 4 deletions(-) create mode 100644 src/common/session/AlwaysValidSessionValidator.ts create mode 100644 src/common/session/SessionValidator.ts diff --git a/src/common/session/AlwaysValidSessionValidator.ts b/src/common/session/AlwaysValidSessionValidator.ts new file mode 100644 index 00000000..3f55486a --- /dev/null +++ b/src/common/session/AlwaysValidSessionValidator.ts @@ -0,0 +1,7 @@ +import ISessionValidator from "./ISessionValidator" + +export default class AlwaysValidSessionValidator implements ISessionValidator { + async validateSession(): Promise { + return true + } +} \ No newline at end of file diff --git a/src/common/session/SessionValidator.ts b/src/common/session/SessionValidator.ts new file mode 100644 index 00000000..757701d3 --- /dev/null +++ b/src/common/session/SessionValidator.ts @@ -0,0 +1,29 @@ +import ISession from "./ISession" +import ISessionValidator from "./ISessionValidator" + +type SessionValidatorConfig = { + readonly session: ISession + readonly guestSessionValidator: ISessionValidator + readonly hostSessionValidator: ISessionValidator +} + +export default class SessionValidator implements ISessionValidator { + private readonly session: ISession + private readonly guestSessionValidator: ISessionValidator + private readonly hostSessionValidator: ISessionValidator + + constructor(config: SessionValidatorConfig) { + this.session = config.session + this.guestSessionValidator = config.guestSessionValidator + this.hostSessionValidator = config.hostSessionValidator + } + + async validateSession(): Promise { + const isGuest = await this.session.getIsGuest() + if (isGuest) { + return await this.guestSessionValidator.validateSession() + } else { + return await this.hostSessionValidator.validateSession() + } + } +} \ No newline at end of file diff --git a/src/composition.ts b/src/composition.ts index 5d3f9c2d..bc72dd85 100644 --- a/src/composition.ts +++ b/src/composition.ts @@ -1,4 +1,5 @@ import AccessTokenRefreshingGitHubClient from "@/common/github/AccessTokenRefreshingGitHubClient" +import AlwaysValidSessionValidator from "@/common/session/AlwaysValidSessionValidator" import Auth0RefreshTokenReader from "@/features/auth/data/Auth0RefreshTokenReader" import Auth0UserIdentityProviderReader from "./features/auth/data/Auth0UserIdentityProviderReader" import Auth0Session from "@/common/session/Auth0Session" @@ -27,6 +28,7 @@ import RedisKeyValueStore from "@/common/keyValueStore/RedisKeyValueStore" import SessionAccessTokenService from "@/features/auth/domain/accessToken/SessionAccessTokenService" import SessionMutexFactory from "@/common/mutex/SessionMutexFactory" import SessionValidatingProjectDataSource from "@/features/projects/domain/SessionValidatingProjectDataSource" +import SessionValidator from "@/common/session/SessionValidator" import UserDataCleanUpLogOutHandler from "@/features/auth/domain/logOut/UserDataCleanUpLogOutHandler" const { @@ -121,10 +123,14 @@ export const gitHubClient = new AccessTokenRefreshingGitHubClient( }) ) -export const sessionValidator = new GitHubOrganizationSessionValidator( - gitHubClient, - GITHUB_ORGANIZATION_NAME -) +export const sessionValidator = new SessionValidator({ + session, + guestSessionValidator: new AlwaysValidSessionValidator(), + hostSessionValidator: new GitHubOrganizationSessionValidator( + gitHubClient, + GITHUB_ORGANIZATION_NAME + ) +}) const projectUserDataRepository = new KeyValueUserDataRepository( new RedisKeyValueStore(REDIS_URL), From fcd481d340ec999f7934ffb6d23d1a7e0a76cc20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=B8vring?= Date: Tue, 7 Nov 2023 12:53:32 +0100 Subject: [PATCH 04/42] Replaces SessionOAuthTokenBarrier with SessionAccessTokenBarrier --- src/app/[...slug]/page.tsx | 6 +++--- src/app/page.tsx | 6 +++--- src/composition.ts | 4 ++-- ...OAuthTokenBarrier.tsx => SessionAccessTokenBarrier.tsx} | 7 +++---- 4 files changed, 11 insertions(+), 12 deletions(-) rename src/features/auth/view/{SessionOAuthTokenBarrier.tsx => SessionAccessTokenBarrier.tsx} (50%) diff --git a/src/app/[...slug]/page.tsx b/src/app/[...slug]/page.tsx index 2193779a..b0fabd5d 100644 --- a/src/app/[...slug]/page.tsx +++ b/src/app/[...slug]/page.tsx @@ -1,5 +1,5 @@ import { getProjectId, getSpecificationId, getVersionId } from "@/common/utils/url" -import SessionOAuthTokenBarrier from "@/features/auth/view/SessionOAuthTokenBarrier" +import SessionAccessTokenBarrier from "@/features/auth/view/SessionAccessTokenBarrier" import ProjectsPage from "@/features/projects/view/ProjectsPage" import { projectRepository } from "@/composition" @@ -8,14 +8,14 @@ type PageParams = { slug: string | string[] } export default async function Page({ params }: { params: PageParams }) { const url = getURL(params) return ( - + - + ) } diff --git a/src/app/page.tsx b/src/app/page.tsx index 9fcd36ad..55087f2f 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,11 +1,11 @@ -import SessionOAuthTokenBarrier from "@/features/auth/view/SessionOAuthTokenBarrier" +import SessionAccessTokenBarrier from "@/features/auth/view/SessionAccessTokenBarrier" import ProjectsPage from "@/features/projects/view/ProjectsPage" import { projectRepository } from "@/composition" export default async function Page() { return ( - + - + ) } diff --git a/src/composition.ts b/src/composition.ts index bc72dd85..f310f969 100644 --- a/src/composition.ts +++ b/src/composition.ts @@ -72,7 +72,7 @@ export const session = new Auth0Session({ isUserGuestReader: new IsUserGuestReader(userIdentityProviderReader) }) -export const oAuthTokenRepository = new OAuthTokenRepository( +const oAuthTokenRepository = new OAuthTokenRepository( new KeyValueUserDataRepository( new RedisKeyValueStore(REDIS_URL), "authToken" @@ -89,7 +89,7 @@ const accessTokenRepository = new KeyValueUserDataRepository( "accessToken" ) -const accessTokenService = new LockingAccessTokenService( +export const accessTokenService = new LockingAccessTokenService( new SessionMutexFactory( new RedisKeyedMutexFactory(REDIS_URL), session, diff --git a/src/features/auth/view/SessionOAuthTokenBarrier.tsx b/src/features/auth/view/SessionAccessTokenBarrier.tsx similarity index 50% rename from src/features/auth/view/SessionOAuthTokenBarrier.tsx rename to src/features/auth/view/SessionAccessTokenBarrier.tsx index 158ec381..d84ed7c7 100644 --- a/src/features/auth/view/SessionOAuthTokenBarrier.tsx +++ b/src/features/auth/view/SessionAccessTokenBarrier.tsx @@ -1,15 +1,14 @@ import { ReactNode } from "react" import { redirect } from "next/navigation" -import { session, oAuthTokenRepository } from "@/composition" +import { accessTokenService } from "@/composition" -export default async function SessionOAuthTokenBarrier({ +export default async function SessionAccessTokenBarrier({ children }: { children: ReactNode }) { try { - const userId = await session.getUserId() - await oAuthTokenRepository.get(userId) + await accessTokenService.getAccessToken() return <>{children} } catch { redirect("/api/auth/logout") From f08d6adb6f09e6fdb7c27560cad52883539d778a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=B8vring?= Date: Tue, 7 Nov 2023 12:57:29 +0100 Subject: [PATCH 05/42] Adds tests for SessionValidator --- .../common/session/SessionValidator.test.ts | 49 +++++++++++++++++++ src/common/session/SessionValidator.ts | 13 +++-- 2 files changed, 57 insertions(+), 5 deletions(-) create mode 100644 __test__/common/session/SessionValidator.test.ts diff --git a/__test__/common/session/SessionValidator.test.ts b/__test__/common/session/SessionValidator.test.ts new file mode 100644 index 00000000..8435eda6 --- /dev/null +++ b/__test__/common/session/SessionValidator.test.ts @@ -0,0 +1,49 @@ +import SessionValidator from "../../../src/common/session/SessionValidator" + +test("It validates a host user", async () => { + let didValidateHostUser = false + const sut = new SessionValidator({ + isGuestReader: { + async getIsGuest() { + return false + } + }, + guestSessionValidator: { + async validateSession() { + return true + }, + }, + hostSessionValidator: { + async validateSession() { + didValidateHostUser = true + return true + }, + } + }) + await sut.validateSession() + expect(didValidateHostUser).toBeTruthy() +}) + +test("It validates a guest user", async () => { + let didValidateGuestUser = false + const sut = new SessionValidator({ + isGuestReader: { + async getIsGuest() { + return true + } + }, + guestSessionValidator: { + async validateSession() { + didValidateGuestUser = true + return true + }, + }, + hostSessionValidator: { + async validateSession() { + return true + }, + } + }) + await sut.validateSession() + expect(didValidateGuestUser).toBeTruthy() +}) diff --git a/src/common/session/SessionValidator.ts b/src/common/session/SessionValidator.ts index 757701d3..4b3da8c9 100644 --- a/src/common/session/SessionValidator.ts +++ b/src/common/session/SessionValidator.ts @@ -1,25 +1,28 @@ -import ISession from "./ISession" import ISessionValidator from "./ISessionValidator" +interface IIsGuestReader { + getIsGuest(): Promise +} + type SessionValidatorConfig = { - readonly session: ISession + readonly isGuestReader: IIsGuestReader readonly guestSessionValidator: ISessionValidator readonly hostSessionValidator: ISessionValidator } export default class SessionValidator implements ISessionValidator { - private readonly session: ISession + private readonly isGuestReader: IIsGuestReader private readonly guestSessionValidator: ISessionValidator private readonly hostSessionValidator: ISessionValidator constructor(config: SessionValidatorConfig) { - this.session = config.session + this.isGuestReader = config.isGuestReader this.guestSessionValidator = config.guestSessionValidator this.hostSessionValidator = config.hostSessionValidator } async validateSession(): Promise { - const isGuest = await this.session.getIsGuest() + const isGuest = await this.isGuestReader.getIsGuest() if (isGuest) { return await this.guestSessionValidator.validateSession() } else { From 1f3091624dcedb3f7d4170a74e49348b1f3185f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=B8vring?= Date: Tue, 7 Nov 2023 13:03:08 +0100 Subject: [PATCH 06/42] Fixes object incorrectly passed --- src/composition.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/composition.ts b/src/composition.ts index f310f969..1d9ac680 100644 --- a/src/composition.ts +++ b/src/composition.ts @@ -124,7 +124,7 @@ export const gitHubClient = new AccessTokenRefreshingGitHubClient( ) export const sessionValidator = new SessionValidator({ - session, + isGuestReader: session, guestSessionValidator: new AlwaysValidSessionValidator(), hostSessionValidator: new GitHubOrganizationSessionValidator( gitHubClient, From 0f1966f1748165aa477e7533317c8595ee0b41eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=B8vring?= Date: Tue, 7 Nov 2023 13:04:52 +0100 Subject: [PATCH 07/42] Fixes credentials not transferred immediately --- .../domain/credentialsTransfer/HostCredentialsTransferrer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/features/auth/domain/credentialsTransfer/HostCredentialsTransferrer.ts b/src/features/auth/domain/credentialsTransfer/HostCredentialsTransferrer.ts index fd520e74..d66a0534 100644 --- a/src/features/auth/domain/credentialsTransfer/HostCredentialsTransferrer.ts +++ b/src/features/auth/domain/credentialsTransfer/HostCredentialsTransferrer.ts @@ -26,6 +26,6 @@ export default class HostCredentialsTransferrer implements ICredentialsTransferr async transferCredentials(userId: string): Promise { const refreshToken = await this.refreshTokenReader.getRefreshToken(userId) const authToken = await this.oAuthTokenRefresher.refreshOAuthToken(refreshToken) - this.oAuthTokenRepository.set(userId, authToken) + await this.oAuthTokenRepository.set(userId, authToken) } } From ab8a27a2611eb183b43de6c3993d03aa22a4e135 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=B8vring?= Date: Tue, 7 Nov 2023 13:07:27 +0100 Subject: [PATCH 08/42] Adds tests for IsUserGuestReader --- __test__/auth/IsUserGuestReader.test.ts | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 __test__/auth/IsUserGuestReader.test.ts diff --git a/__test__/auth/IsUserGuestReader.test.ts b/__test__/auth/IsUserGuestReader.test.ts new file mode 100644 index 00000000..5e121bc1 --- /dev/null +++ b/__test__/auth/IsUserGuestReader.test.ts @@ -0,0 +1,22 @@ +import IsUserGuestReader from "../../src/features/auth/domain/userIdentityProvider/IsUserGuestReader" +import UserIdentityProvider from "../../src/features/auth/domain/userIdentityProvider/UserIdentityProvider" + +test("It does not consider a user to be a guest if they are logged in with GitHub", async () => { + const sut = new IsUserGuestReader({ + async getUserIdentityProvider() { + return UserIdentityProvider.GITHUB + } + }) + const isGuest = await sut.getIsUserGuest("foo") + expect(isGuest).toBeFalsy() +}) + +test("It considers user a to be a guest if they are logged in with username and password", async () => { + const sut = new IsUserGuestReader({ + async getUserIdentityProvider() { + return UserIdentityProvider.USERNAME_PASSWORD + } + }) + const isGuest = await sut.getIsUserGuest("foo") + expect(isGuest).toBeTruthy() +}) From 8b4ff43a4a9520a8aa1fba2fd959abaeaa1f2aee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=B8vring?= Date: Tue, 7 Nov 2023 13:11:46 +0100 Subject: [PATCH 09/42] Adds tests for CachingUserIdentityProviderReader --- .../CachingUserIdentityProviderReader.test.ts | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 __test__/auth/CachingUserIdentityProviderReader.test.ts diff --git a/__test__/auth/CachingUserIdentityProviderReader.test.ts b/__test__/auth/CachingUserIdentityProviderReader.test.ts new file mode 100644 index 00000000..bc4a760e --- /dev/null +++ b/__test__/auth/CachingUserIdentityProviderReader.test.ts @@ -0,0 +1,60 @@ +import CachingUserIdentityProviderReader from "../../src/features/auth/domain/userIdentityProvider/CachingUserIdentityProviderReader" +import UserIdentityProvider from "../../src/features/auth/domain/userIdentityProvider/UserIdentityProvider" + +test("It fetches user identity provider if it is not cached", async () => { + let didFetchUserIdentityProvider = false + const sut = new CachingUserIdentityProviderReader({ + async get() { + return null + }, + async set() {}, + async delete() {} + }, { + async getUserIdentityProvider() { + didFetchUserIdentityProvider = true + return UserIdentityProvider.GITHUB + }, + }) + await sut.getUserIdentityProvider("foo") + expect(didFetchUserIdentityProvider).toBeTruthy() +}) + +test("It does not fetch user identity provider if it is cached", async () => { + let didFetchUserIdentityProvider = false + const sut = new CachingUserIdentityProviderReader({ + async get() { + return UserIdentityProvider.GITHUB + }, + async set() {}, + async delete() {} + }, { + async getUserIdentityProvider() { + didFetchUserIdentityProvider = true + return UserIdentityProvider.GITHUB + }, + }) + await sut.getUserIdentityProvider("foo") + expect(didFetchUserIdentityProvider).toBeFalsy() +}) + +test("It caches fetched user identity provider for user", async () => { + let cachedUserId: string | undefined + let cachedUserIdentityProvider: string | undefined + const sut = new CachingUserIdentityProviderReader({ + async get() { + return null + }, + async set(userId, userIdentityProvider) { + cachedUserId = userId + cachedUserIdentityProvider = userIdentityProvider + }, + async delete() {} + }, { + async getUserIdentityProvider() { + return UserIdentityProvider.USERNAME_PASSWORD + }, + }) + await sut.getUserIdentityProvider("1234") + expect(cachedUserId).toBe("1234") + expect(cachedUserIdentityProvider).toBe(UserIdentityProvider.USERNAME_PASSWORD.toString()) +}) From 55857bc86e1f68aa3b5f798f49e193b8c04dcf8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=B8vring?= Date: Tue, 7 Nov 2023 18:18:06 +0100 Subject: [PATCH 10/42] Adds missing await when transferring credentials --- .../auth/domain/logIn/CredentialsTransferringLogInHandler.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/features/auth/domain/logIn/CredentialsTransferringLogInHandler.ts b/src/features/auth/domain/logIn/CredentialsTransferringLogInHandler.ts index e89389f3..ae967eb5 100644 --- a/src/features/auth/domain/logIn/CredentialsTransferringLogInHandler.ts +++ b/src/features/auth/domain/logIn/CredentialsTransferringLogInHandler.ts @@ -26,9 +26,9 @@ export default class CredentialsTransferringLogInHandler implements ILogInHandle async handleLogIn(userId: string): Promise { const isGuest = await this.isUserGuestReader.getIsUserGuest(userId) if (isGuest) { - this.guestCredentialsTransferrer.transferCredentials(userId) + await this.guestCredentialsTransferrer.transferCredentials(userId) } else { - this.hostCredentialsTransferrer.transferCredentials(userId) + await this.hostCredentialsTransferrer.transferCredentials(userId) } } } From c17534900eda1196f7ab3805baf25e1125d407b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=B8vring?= Date: Tue, 7 Nov 2023 18:18:28 +0100 Subject: [PATCH 11/42] Creates guest access tokens with limited repository access --- .../CachingRepositoryAccessReaderConfig.ts | 90 +++++++++++++++++++ ...ositoryRestrictingAccessTokenDataSource.ts | 20 +++++ src/composition.ts | 36 +++++--- .../auth/data/Auth0RepositoryAccessReader.ts | 24 +++++ ...GitHubInstallationAccessTokenDataSource.ts | 5 +- .../accessToken/GuestAccessTokenService.ts | 4 +- .../GuestCredentialsTransferrer.ts | 4 +- .../CachingRepositoryAccessReaderConfig.ts | 60 +++++++++++++ ...ositoryRestrictingAccessTokenDataSource.ts | 27 ++++++ 9 files changed, 254 insertions(+), 16 deletions(-) create mode 100644 __test__/auth/CachingRepositoryAccessReaderConfig.ts create mode 100644 __test__/auth/RepositoryRestrictingAccessTokenDataSource.ts create mode 100644 src/features/auth/data/Auth0RepositoryAccessReader.ts create mode 100644 src/features/auth/domain/repositoryAccess/CachingRepositoryAccessReaderConfig.ts create mode 100644 src/features/auth/domain/repositoryAccess/RepositoryRestrictingAccessTokenDataSource.ts diff --git a/__test__/auth/CachingRepositoryAccessReaderConfig.ts b/__test__/auth/CachingRepositoryAccessReaderConfig.ts new file mode 100644 index 00000000..5715ce7f --- /dev/null +++ b/__test__/auth/CachingRepositoryAccessReaderConfig.ts @@ -0,0 +1,90 @@ +import CachingRepositoryAccessReaderConfig from "../../src/features/auth/domain/repositoryAccess/CachingRepositoryAccessReaderConfig" + +test("It fetches repository names for user if they are not cached", async () => { + let didFetchRepositoryNames = false + let requestedUserId: string | undefined + const sut = new CachingRepositoryAccessReaderConfig({ + repository: { + async get() { + return null + }, + async set() {}, + async delete() {} + }, + repositoryAccessReader: { + async getRepositoryNames(userId: string) { + didFetchRepositoryNames = true + requestedUserId = userId + return [] + } + } + }) + await sut.getRepositoryNames("1234") + expect(didFetchRepositoryNames).toBeTruthy() + expect(requestedUserId).toEqual("1234") +}) + +test("It does not fetch repository names if they are cached", async () => { + let didFetchRepositoryNames = false + const sut = new CachingRepositoryAccessReaderConfig({ + repository: { + async get() { + return "[\"foo\"]" + }, + async set() {}, + async delete() {} + }, + repositoryAccessReader: { + async getRepositoryNames() { + didFetchRepositoryNames = true + return [] + } + } + }) + await sut.getRepositoryNames("1234") + expect(didFetchRepositoryNames).toBeFalsy() +}) + +test("It caches fetched repository names for user", async () => { + let cachedUserId: string | undefined + let cachedRepositoryNames: string | undefined + const sut = new CachingRepositoryAccessReaderConfig({ + repository: { + async get() { + return null + }, + async set(userId, value) { + cachedUserId = userId + cachedRepositoryNames = value + }, + async delete() {} + }, + repositoryAccessReader: { + async getRepositoryNames() { + return ["foo"] + } + } + }) + await sut.getRepositoryNames("1234") + expect(cachedUserId).toEqual("1234") + expect(cachedRepositoryNames).toEqual("[\"foo\"]") +}) + +test("It decodes cached repository names", async () => { + const sut = new CachingRepositoryAccessReaderConfig({ + repository: { + async get() { + return "[\"foo\",\"bar\"]" + }, + async set() {}, + async delete() {} + }, + repositoryAccessReader: { + async getRepositoryNames() { + return [] + } + } + }) + const repositoryNames = await sut.getRepositoryNames("1234") + expect(repositoryNames).toEqual(["foo", "bar"]) +}) diff --git a/__test__/auth/RepositoryRestrictingAccessTokenDataSource.ts b/__test__/auth/RepositoryRestrictingAccessTokenDataSource.ts new file mode 100644 index 00000000..149a4487 --- /dev/null +++ b/__test__/auth/RepositoryRestrictingAccessTokenDataSource.ts @@ -0,0 +1,20 @@ +import RepositoryRestrictingAccessTokenDataSource from "../../src/features/auth/domain/repositoryAccess/RepositoryRestrictingAccessTokenDataSource" + +test("It limits access to the fetched repositories", async () => { + let restrictingRepositoryNames: string[] | undefined + const sut = new RepositoryRestrictingAccessTokenDataSource({ + repositoryAccessReader: { + async getRepositoryNames() { + return ["foo", "bar"] + } + }, + dataSource: { + async getAccessToken(repositoryNames) { + restrictingRepositoryNames = repositoryNames + return "secret" + }, + } + }) + await sut.getAccessToken("1234") + expect(restrictingRepositoryNames).toEqual(["foo", "bar"]) +}) diff --git a/src/composition.ts b/src/composition.ts index 51afc586..72bc3a1d 100644 --- a/src/composition.ts +++ b/src/composition.ts @@ -1,9 +1,11 @@ import AccessTokenRefreshingGitHubClient from "@/common/github/AccessTokenRefreshingGitHubClient" import AlwaysValidSessionValidator from "@/common/session/AlwaysValidSessionValidator" import Auth0RefreshTokenReader from "@/features/auth/data/Auth0RefreshTokenReader" -import Auth0UserIdentityProviderReader from "./features/auth/data/Auth0UserIdentityProviderReader" +import Auth0RepositoryAccessReader from "./features/auth/data/Auth0RepositoryAccessReader" import Auth0Session from "@/common/session/Auth0Session" +import Auth0UserIdentityProviderReader from "./features/auth/data/Auth0UserIdentityProviderReader" import CachingProjectDataSource from "@/features/projects/domain/CachingProjectDataSource" +import CachingRepositoryAccessReaderConfig from "./features/auth/domain/repositoryAccess/CachingRepositoryAccessReaderConfig" import CachingUserIdentityProviderReader from "./features/auth/domain/userIdentityProvider/CachingUserIdentityProviderReader" import CompositeLogOutHandler from "@/features/auth/domain/logOut/CompositeLogOutHandler" import CredentialsTransferringLogInHandler from "@/features/auth/domain/logIn/CredentialsTransferringLogInHandler" @@ -25,6 +27,7 @@ import OnlyStaleRefreshingAccessTokenService from "@/features/auth/domain/access import ProjectRepository from "@/features/projects/domain/ProjectRepository" import RedisKeyedMutexFactory from "@/common/mutex/RedisKeyedMutexFactory" import RedisKeyValueStore from "@/common/keyValueStore/RedisKeyValueStore" +import RepositoryRestrictingAccessTokenDataSource from "@/features/auth/domain/repositoryAccess/RepositoryRestrictingAccessTokenDataSource" import SessionAccessTokenService from "@/features/auth/domain/accessToken/SessionAccessTokenService" import SessionMutexFactory from "@/common/mutex/SessionMutexFactory" import SessionValidatingProjectDataSource from "@/features/projects/domain/SessionValidatingProjectDataSource" @@ -89,6 +92,24 @@ const accessTokenRepository = new KeyValueUserDataRepository( "accessToken" ) +const guestRepositoryAccessRepository = new KeyValueUserDataRepository( + new RedisKeyValueStore(REDIS_URL), + "guestRepositoryAccess" +) + +const guestAccessTokenDataSource = new RepositoryRestrictingAccessTokenDataSource({ + repositoryAccessReader: new CachingRepositoryAccessReaderConfig({ + repository: guestRepositoryAccessRepository, + repositoryAccessReader: new Auth0RepositoryAccessReader({ + ...auth0ManagementCredentials + }) + }), + dataSource: new GitHubInstallationAccessTokenDataSource({ + ...gitHubAppCredentials, + organization: GITHUB_ORGANIZATION_NAME + }) +}) + export const accessTokenService = new LockingAccessTokenService( new SessionMutexFactory( new RedisKeyedMutexFactory(REDIS_URL), @@ -101,10 +122,7 @@ export const accessTokenService = new LockingAccessTokenService( guestAccessTokenService: new GuestAccessTokenService({ userIdReader: session, repository: accessTokenRepository, - dataSource: new GitHubInstallationAccessTokenDataSource({ - ...gitHubAppCredentials, - organization: GITHUB_ORGANIZATION_NAME - }) + dataSource: guestAccessTokenDataSource }), hostAccessTokenService: new HostAccessTokenService({ userIdReader: session, @@ -161,10 +179,7 @@ export const logInHandler = new CredentialsTransferringLogInHandler({ ), guestCredentialsTransferrer: new GuestCredentialsTransferrer({ repository: accessTokenRepository, - dataSource: new GitHubInstallationAccessTokenDataSource({ - ...gitHubAppCredentials, - organization: GITHUB_ORGANIZATION_NAME - }), + dataSource: guestAccessTokenDataSource }), hostCredentialsTransferrer: new HostCredentialsTransferrer({ refreshTokenReader: new Auth0RefreshTokenReader({ @@ -180,6 +195,7 @@ export const logOutHandler = new ErrorIgnoringLogOutHandler( new CompositeLogOutHandler([ new UserDataCleanUpLogOutHandler(session, projectUserDataRepository), new UserDataCleanUpLogOutHandler(session, oAuthTokenRepository), - new UserDataCleanUpLogOutHandler(session, userIdentityProviderRepository) + new UserDataCleanUpLogOutHandler(session, userIdentityProviderRepository), + new UserDataCleanUpLogOutHandler(session, guestRepositoryAccessRepository) ]) ) diff --git a/src/features/auth/data/Auth0RepositoryAccessReader.ts b/src/features/auth/data/Auth0RepositoryAccessReader.ts new file mode 100644 index 00000000..1d923d61 --- /dev/null +++ b/src/features/auth/data/Auth0RepositoryAccessReader.ts @@ -0,0 +1,24 @@ +import { ManagementClient } from "auth0" + +type Auth0RepositoryAccessReaderConfig = { + readonly domain: string + readonly clientId: string + readonly clientSecret: string +} + +export default class Auth0RepositoryAccessReader { + private readonly managementClient: ManagementClient + + constructor(config: Auth0RepositoryAccessReaderConfig) { + this.managementClient = new ManagementClient({ + domain: config.domain, + clientId: config.clientId, + clientSecret: config.clientSecret + }) + } + + async getRepositoryNames(userId: string): Promise { + const response = await this.managementClient.users.getRoles({ id: userId }) + return response.data.map(e => e.name) + } +} diff --git a/src/features/auth/data/GitHubInstallationAccessTokenDataSource.ts b/src/features/auth/data/GitHubInstallationAccessTokenDataSource.ts index e4bc7319..0a157618 100644 --- a/src/features/auth/data/GitHubInstallationAccessTokenDataSource.ts +++ b/src/features/auth/data/GitHubInstallationAccessTokenDataSource.ts @@ -16,7 +16,7 @@ export default class GitHubInstallationAccessTokenRefresher { this.config = config } - async getAccessToken(): Promise { + async getAccessToken(repositoryNames: string[]): Promise { const auth = createAppAuth({ appId: this.config.appId, clientId: this.config.clientId, @@ -31,7 +31,8 @@ export default class GitHubInstallationAccessTokenRefresher { const installation = response.data const installationAuth = await auth({ type: "installation", - installationId: installation.id + installationId: installation.id, + repositoryNames: repositoryNames }) return installationAuth.token } diff --git a/src/features/auth/domain/accessToken/GuestAccessTokenService.ts b/src/features/auth/domain/accessToken/GuestAccessTokenService.ts index 71a4ca46..e0cd25dc 100644 --- a/src/features/auth/domain/accessToken/GuestAccessTokenService.ts +++ b/src/features/auth/domain/accessToken/GuestAccessTokenService.ts @@ -11,7 +11,7 @@ export interface Repository { } export interface DataSource { - getAccessToken(): Promise + getAccessToken(userId: string): Promise } export type GuestAccessTokenServiceConfig = { @@ -42,7 +42,7 @@ export default class GuestAccessTokenService implements IAccessTokenService { async refreshAccessToken(_accessToken: string): Promise { const userId = await this.userIdReader.getUserId() - const newAccessToken = await this.dataSource.getAccessToken() + const newAccessToken = await this.dataSource.getAccessToken(userId) await this.repository.set(userId, newAccessToken) return newAccessToken } diff --git a/src/features/auth/domain/credentialsTransfer/GuestCredentialsTransferrer.ts b/src/features/auth/domain/credentialsTransfer/GuestCredentialsTransferrer.ts index 2e756856..115024da 100644 --- a/src/features/auth/domain/credentialsTransfer/GuestCredentialsTransferrer.ts +++ b/src/features/auth/domain/credentialsTransfer/GuestCredentialsTransferrer.ts @@ -2,7 +2,7 @@ import ICredentialsTransferrer from "./ICredentialsTransferrer" import IUserDataRepository from "@/common/userData/IUserDataRepository" interface DataSource { - getAccessToken(): Promise + getAccessToken(userId: string): Promise } type Repository = IUserDataRepository @@ -22,7 +22,7 @@ export default class GuestCredentialsTransferrer implements ICredentialsTransfer } async transferCredentials(userId: string): Promise { - const accessToken = await this.dataSource.getAccessToken() + const accessToken = await this.dataSource.getAccessToken(userId) await this.repository.set(userId, accessToken) } } diff --git a/src/features/auth/domain/repositoryAccess/CachingRepositoryAccessReaderConfig.ts b/src/features/auth/domain/repositoryAccess/CachingRepositoryAccessReaderConfig.ts new file mode 100644 index 00000000..e0dcd881 --- /dev/null +++ b/src/features/auth/domain/repositoryAccess/CachingRepositoryAccessReaderConfig.ts @@ -0,0 +1,60 @@ +import { z } from "zod" +import ZodJSONCoder from "../../../../common/utils/ZodJSONCoder" +import IUserDataRepository from "@/common/userData/IUserDataRepository" + +export const RepositoryNamesContainerSchema = z.string().array() + +interface IRepositoryAccessReader { + getRepositoryNames(userId: string): Promise +} + +type CachingRepositoryAccessReaderConfig = { + readonly repository: IRepositoryNameRepository + readonly repositoryAccessReader: IRepositoryAccessReader +} + +type IRepositoryNameRepository = IUserDataRepository + +export default class CachingRepositoryAccessReader { + private readonly repository: IRepositoryNameRepository + private readonly repositoryAccessReader: IRepositoryAccessReader + + constructor(config: CachingRepositoryAccessReaderConfig) { + this.repository = config.repository + this.repositoryAccessReader = config.repositoryAccessReader + } + + async getRepositoryNames(userId: string): Promise { + const cachedValue = await this.getCachedRepositoryNames(userId) + if (cachedValue) { + return cachedValue + } + return await this.refreshRepositoryNames(userId) + } + + private async getCachedRepositoryNames(userId: string): Promise { + const str = await this.repository.get(userId) + if (!str) { + return null + } + try { + return ZodJSONCoder.decode(RepositoryNamesContainerSchema, str) + /* eslint-disable-next-line no-empty */ + } catch (error: any) { + console.error(error) + return null + } + } + + private async refreshRepositoryNames(userId: string): Promise { + const repositoryNames = await this.repositoryAccessReader.getRepositoryNames(userId) + try { + const str = ZodJSONCoder.encode(RepositoryNamesContainerSchema, repositoryNames) + await this.repository.set(userId, str) + /* eslint-disable-next-line no-empty */ + } catch (error: any) { + console.error(error) + } + return repositoryNames + } +} diff --git a/src/features/auth/domain/repositoryAccess/RepositoryRestrictingAccessTokenDataSource.ts b/src/features/auth/domain/repositoryAccess/RepositoryRestrictingAccessTokenDataSource.ts new file mode 100644 index 00000000..0bb4b4d2 --- /dev/null +++ b/src/features/auth/domain/repositoryAccess/RepositoryRestrictingAccessTokenDataSource.ts @@ -0,0 +1,27 @@ +interface IRepositoryAccessReader { + getRepositoryNames(userId: string): Promise +} + +interface IAccessTokenDataSource { + getAccessToken(repositoryNames: string[]): Promise +} + +type RepositoryRestrictingAccessTokenDataSourceConfig = { + readonly repositoryAccessReader: IRepositoryAccessReader + readonly dataSource: IAccessTokenDataSource +} + +export default class RepositoryRestrictingAccessTokenDataSource { + private readonly repositoryAccessReader: IRepositoryAccessReader + private readonly dataSource: IAccessTokenDataSource + + constructor(config: RepositoryRestrictingAccessTokenDataSourceConfig) { + this.repositoryAccessReader = config.repositoryAccessReader + this.dataSource = config.dataSource + } + + async getAccessToken(userId: string): Promise { + const repositoryNames = await this.repositoryAccessReader.getRepositoryNames(userId) + return await this.dataSource.getAccessToken(repositoryNames) + } +} From 8778d0327ad0a36bee351817cd8ecce214fdcd23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=B8vring?= Date: Tue, 7 Nov 2023 18:39:18 +0100 Subject: [PATCH 12/42] Adds CompositeLogInHandler --- __test__/auth/CompositeLogInHandler.test.ts | 24 +++++++++++++++++++ .../domain/logIn/CompositeLogInHandler.ts | 14 +++++++++++ 2 files changed, 38 insertions(+) create mode 100644 __test__/auth/CompositeLogInHandler.test.ts create mode 100644 src/features/auth/domain/logIn/CompositeLogInHandler.ts diff --git a/__test__/auth/CompositeLogInHandler.test.ts b/__test__/auth/CompositeLogInHandler.test.ts new file mode 100644 index 00000000..7438eef0 --- /dev/null +++ b/__test__/auth/CompositeLogInHandler.test.ts @@ -0,0 +1,24 @@ +import CompositeLogInHandler from "../../src/features/auth/domain/logIn/CompositeLogInHandler" + +test("It invokes all log in handlers for user", async () => { + let userId1: string | undefined + let userId2: string | undefined + let userId3: string | undefined + const sut = new CompositeLogInHandler([{ + async handleLogIn(userId) { + userId1 = userId + } + }, { + async handleLogIn(userId) { + userId2 = userId + } + }, { + async handleLogIn(userId) { + userId3 = userId + } + }]) + await sut.handleLogIn("1234") + expect(userId1).toEqual("1234") + expect(userId2).toEqual("1234") + expect(userId3).toEqual("1234") +}) diff --git a/src/features/auth/domain/logIn/CompositeLogInHandler.ts b/src/features/auth/domain/logIn/CompositeLogInHandler.ts new file mode 100644 index 00000000..169be4c2 --- /dev/null +++ b/src/features/auth/domain/logIn/CompositeLogInHandler.ts @@ -0,0 +1,14 @@ +import ILogInHandler from "./ILogInHandler" + +export default class CompositeLogInHandler implements ILogInHandler { + private readonly handlers: ILogInHandler[] + + constructor(handlers: ILogInHandler[]) { + this.handlers = handlers + } + + async handleLogIn(userId: string): Promise { + const promises = this.handlers.map(e => e.handleLogIn(userId)) + await Promise.all(promises) + } +} From f0f894ffdc78b4d897d54db3f72ac198894a2228 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=B8vring?= Date: Tue, 7 Nov 2023 18:39:28 +0100 Subject: [PATCH 13/42] Resets has_pending_invitation flag --- .../RemoveInvitedFlagLogInHandler.test.ts | 18 +++++++++ src/composition.ts | 40 +++++++++++-------- .../auth/data/Auth0MetadataUpdater.ts | 28 +++++++++++++ .../logIn/RemoveInvitedFlagLogInHandler.ts | 20 ++++++++++ 4 files changed, 90 insertions(+), 16 deletions(-) create mode 100644 __test__/auth/RemoveInvitedFlagLogInHandler.test.ts create mode 100644 src/features/auth/data/Auth0MetadataUpdater.ts create mode 100644 src/features/auth/domain/logIn/RemoveInvitedFlagLogInHandler.ts diff --git a/__test__/auth/RemoveInvitedFlagLogInHandler.test.ts b/__test__/auth/RemoveInvitedFlagLogInHandler.test.ts new file mode 100644 index 00000000..ef8dc3b4 --- /dev/null +++ b/__test__/auth/RemoveInvitedFlagLogInHandler.test.ts @@ -0,0 +1,18 @@ +import RemoveInvitedFlagLogInHandler from "../../src/features/auth/domain/logIn/RemoveInvitedFlagLogInHandler" + +test("It removes invited flag from specified user", async () => { + let updatedUserId: string | undefined + /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ + let updatedMetadata: {[key: string]: any} | undefined + const sut = new RemoveInvitedFlagLogInHandler({ + async updateMetadata(userId, metadata) { + updatedUserId = userId + updatedMetadata = metadata + } + }) + await sut.handleLogIn("1234") + expect(updatedUserId).toEqual("1234") + expect(updatedMetadata).toEqual({ + has_pending_invitation: false + }) +}) diff --git a/src/composition.ts b/src/composition.ts index 72bc3a1d..5f57a0f6 100644 --- a/src/composition.ts +++ b/src/composition.ts @@ -1,5 +1,6 @@ import AccessTokenRefreshingGitHubClient from "@/common/github/AccessTokenRefreshingGitHubClient" import AlwaysValidSessionValidator from "@/common/session/AlwaysValidSessionValidator" +import Auth0MetadataUpdater from "./features/auth/data/Auth0MetadataUpdater" import Auth0RefreshTokenReader from "@/features/auth/data/Auth0RefreshTokenReader" import Auth0RepositoryAccessReader from "./features/auth/data/Auth0RepositoryAccessReader" import Auth0Session from "@/common/session/Auth0Session" @@ -7,6 +8,7 @@ import Auth0UserIdentityProviderReader from "./features/auth/data/Auth0UserIdent import CachingProjectDataSource from "@/features/projects/domain/CachingProjectDataSource" import CachingRepositoryAccessReaderConfig from "./features/auth/domain/repositoryAccess/CachingRepositoryAccessReaderConfig" import CachingUserIdentityProviderReader from "./features/auth/domain/userIdentityProvider/CachingUserIdentityProviderReader" +import CompositeLogInHandler from "@/features/auth/domain/logIn/CompositeLogInHandler" import CompositeLogOutHandler from "@/features/auth/domain/logOut/CompositeLogOutHandler" import CredentialsTransferringLogInHandler from "@/features/auth/domain/logIn/CredentialsTransferringLogInHandler" import ErrorIgnoringLogOutHandler from "@/features/auth/domain/logOut/ErrorIgnoringLogOutHandler" @@ -27,6 +29,7 @@ import OnlyStaleRefreshingAccessTokenService from "@/features/auth/domain/access import ProjectRepository from "@/features/projects/domain/ProjectRepository" import RedisKeyedMutexFactory from "@/common/mutex/RedisKeyedMutexFactory" import RedisKeyValueStore from "@/common/keyValueStore/RedisKeyValueStore" +import RemoveInvitedFlagLoginHandler from "./features/auth/domain/logIn/RemoveInvitedFlagLogInHandler" import RepositoryRestrictingAccessTokenDataSource from "@/features/auth/domain/repositoryAccess/RepositoryRestrictingAccessTokenDataSource" import SessionAccessTokenService from "@/features/auth/domain/accessToken/SessionAccessTokenService" import SessionMutexFactory from "@/common/mutex/SessionMutexFactory" @@ -173,23 +176,28 @@ export const projectDataSource = new CachingProjectDataSource( projectRepository ) -export const logInHandler = new CredentialsTransferringLogInHandler({ - isUserGuestReader: new IsUserGuestReader( - userIdentityProviderReader - ), - guestCredentialsTransferrer: new GuestCredentialsTransferrer({ - repository: accessTokenRepository, - dataSource: guestAccessTokenDataSource - }), - hostCredentialsTransferrer: new HostCredentialsTransferrer({ - refreshTokenReader: new Auth0RefreshTokenReader({ - ...auth0ManagementCredentials, - connection: "github" +export const logInHandler = new CompositeLogInHandler([ + new CredentialsTransferringLogInHandler({ + isUserGuestReader: new IsUserGuestReader( + userIdentityProviderReader + ), + guestCredentialsTransferrer: new GuestCredentialsTransferrer({ + repository: accessTokenRepository, + dataSource: guestAccessTokenDataSource }), - oAuthTokenRefresher: gitHubOAuthTokenRefresher, - oAuthTokenRepository: oAuthTokenRepository - }) -}) + hostCredentialsTransferrer: new HostCredentialsTransferrer({ + refreshTokenReader: new Auth0RefreshTokenReader({ + ...auth0ManagementCredentials, + connection: "github" + }), + oAuthTokenRefresher: gitHubOAuthTokenRefresher, + oAuthTokenRepository: oAuthTokenRepository + }) + }), + new RemoveInvitedFlagLoginHandler( + new Auth0MetadataUpdater({ ...auth0ManagementCredentials }) + ) +]) export const logOutHandler = new ErrorIgnoringLogOutHandler( new CompositeLogOutHandler([ diff --git a/src/features/auth/data/Auth0MetadataUpdater.ts b/src/features/auth/data/Auth0MetadataUpdater.ts new file mode 100644 index 00000000..88ffb524 --- /dev/null +++ b/src/features/auth/data/Auth0MetadataUpdater.ts @@ -0,0 +1,28 @@ +import { ManagementClient } from "auth0" + +type Auth0MetadataUpdaterConfig = { + readonly domain: string + readonly clientId: string + readonly clientSecret: string +} + +export default class Auth0MetadataUpdater { + private readonly managementClient: ManagementClient + + constructor(config: Auth0MetadataUpdaterConfig) { + this.managementClient = new ManagementClient({ + domain: config.domain, + clientId: config.clientId, + clientSecret: config.clientSecret + }) + } + + /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ + async updateMetadata(userId: string, metadata: {[key: string]: any}): Promise { + await this.managementClient.users.update({ + id: userId + }, { + app_metadata: metadata + }) + } +} diff --git a/src/features/auth/domain/logIn/RemoveInvitedFlagLogInHandler.ts b/src/features/auth/domain/logIn/RemoveInvitedFlagLogInHandler.ts new file mode 100644 index 00000000..d2de0220 --- /dev/null +++ b/src/features/auth/domain/logIn/RemoveInvitedFlagLogInHandler.ts @@ -0,0 +1,20 @@ +import ILogInHandler from "./ILogInHandler" + +export interface IMetadataUpdater { + /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ + updateMetadata(userId: string, metadata: {[key: string]: any}): Promise +} + +export default class RemoveInvitedFlagLoginHandler implements ILogInHandler { + private readonly metadataUpdater: IMetadataUpdater + + constructor(metadataUpdater: IMetadataUpdater) { + this.metadataUpdater = metadataUpdater + } + + async handleLogIn(userId: string): Promise { + await this.metadataUpdater.updateMetadata(userId, { + has_pending_invitation: false + }) + } +} From df9fa79a71b9626a4668c1ecc2bb8ec96b3d6653 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=B8vring?= Date: Tue, 7 Nov 2023 18:43:54 +0100 Subject: [PATCH 14/42] Logs callback error --- src/app/api/auth/[auth0]/route.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/app/api/auth/[auth0]/route.ts b/src/app/api/auth/[auth0]/route.ts index aeaf8b83..2023b606 100644 --- a/src/app/api/auth/[auth0]/route.ts +++ b/src/app/api/auth/[auth0]/route.ts @@ -16,7 +16,8 @@ const afterCallback: AfterCallbackAppRoute = async (_req, session) => { return session } -const onError: AppRouterOnError = async () => { +const onError: AppRouterOnError = async (_req, error) => { + console.error(error) const url = new URL(SHAPE_DOCS_BASE_URL + "/api/auth/forceLogout") return NextResponse.redirect(url) } From 553a421ab99da5fa06cc378cc432b39ecd919067 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=B8vring?= Date: Tue, 7 Nov 2023 19:18:28 +0100 Subject: [PATCH 15/42] Deletes access token --- src/composition.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/composition.ts b/src/composition.ts index 5f57a0f6..4b3f007a 100644 --- a/src/composition.ts +++ b/src/composition.ts @@ -202,8 +202,9 @@ export const logInHandler = new CompositeLogInHandler([ export const logOutHandler = new ErrorIgnoringLogOutHandler( new CompositeLogOutHandler([ new UserDataCleanUpLogOutHandler(session, projectUserDataRepository), - new UserDataCleanUpLogOutHandler(session, oAuthTokenRepository), new UserDataCleanUpLogOutHandler(session, userIdentityProviderRepository), - new UserDataCleanUpLogOutHandler(session, guestRepositoryAccessRepository) + new UserDataCleanUpLogOutHandler(session, guestRepositoryAccessRepository), + new UserDataCleanUpLogOutHandler(session, oAuthTokenRepository), + new UserDataCleanUpLogOutHandler(session, accessTokenRepository) ]) ) From f4ad1900d7c02a9e11c68fbf425a4ba22d4297e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=B8vring?= Date: Tue, 7 Nov 2023 19:35:04 +0100 Subject: [PATCH 16/42] Improves error handling --- ...GitHubInstallationAccessTokenDataSource.ts | 23 ++++++++++++++----- 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/src/features/auth/data/GitHubInstallationAccessTokenDataSource.ts b/src/features/auth/data/GitHubInstallationAccessTokenDataSource.ts index 0a157618..7a958619 100644 --- a/src/features/auth/data/GitHubInstallationAccessTokenDataSource.ts +++ b/src/features/auth/data/GitHubInstallationAccessTokenDataSource.ts @@ -29,11 +29,22 @@ export default class GitHubInstallationAccessTokenRefresher { org: this.config.organization }) const installation = response.data - const installationAuth = await auth({ - type: "installation", - installationId: installation.id, - repositoryNames: repositoryNames - }) - return installationAuth.token + try { + const installationAuth = await auth({ + type: "installation", + installationId: installation.id, + repositoryNames: repositoryNames + }) + return installationAuth.token + /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ + } catch (error: any) { + if (error.status && error.status == 422) { + // One or more of the repositories do not exist. We log the error + // and create an access token with access to know repositories. + console.error("Cannot log in user as one or more repositories do not exist: " + repositoryNames.join(", ")) + console.error(error) + } + throw error + } } } From 96b83051405091304375d7faee2db7c41c5e3970 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=B8vring?= Date: Tue, 7 Nov 2023 19:35:15 +0100 Subject: [PATCH 17/42] Fetch guest access tokens on demand --- .../auth/domain/accessToken/GuestAccessTokenService.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/features/auth/domain/accessToken/GuestAccessTokenService.ts b/src/features/auth/domain/accessToken/GuestAccessTokenService.ts index e0cd25dc..701051eb 100644 --- a/src/features/auth/domain/accessToken/GuestAccessTokenService.ts +++ b/src/features/auth/domain/accessToken/GuestAccessTokenService.ts @@ -35,12 +35,17 @@ export default class GuestAccessTokenService implements IAccessTokenService { const userId = await this.userIdReader.getUserId() const accessToken = await this.repository.get(userId) if (!accessToken) { - throw new UnauthorizedError(`No access token found for user with ID ${userId}`) + // We fetch the access token for guests on demand. + return await this.getNewAccessToken() } return accessToken } async refreshAccessToken(_accessToken: string): Promise { + return await this.getNewAccessToken() + } + + private async getNewAccessToken(): Promise { const userId = await this.userIdReader.getUserId() const newAccessToken = await this.dataSource.getAccessToken(userId) await this.repository.set(userId, newAccessToken) From a0d79612807529a65cc6b9248fa43a3d81c0d346 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=B8vring?= Date: Tue, 7 Nov 2023 19:35:52 +0100 Subject: [PATCH 18/42] Removes GuestCredentialsTransferrer --- src/composition.ts | 7 ++--- .../GuestCredentialsTransferrer.ts | 28 ------------------- .../NullObjectCredentialsTransferrer.ts | 5 ++++ 3 files changed, 7 insertions(+), 33 deletions(-) delete mode 100644 src/features/auth/domain/credentialsTransfer/GuestCredentialsTransferrer.ts create mode 100644 src/features/auth/domain/credentialsTransfer/NullObjectCredentialsTransferrer.ts diff --git a/src/composition.ts b/src/composition.ts index 4b3f007a..9f06b816 100644 --- a/src/composition.ts +++ b/src/composition.ts @@ -18,7 +18,7 @@ import GitHubOrganizationSessionValidator from "@/common/session/GitHubOrganizat import GitHubProjectDataSource from "@/features/projects/data/GitHubProjectDataSource" import GitHubInstallationAccessTokenDataSource from "@/features/auth/data/GitHubInstallationAccessTokenDataSource" import GuestAccessTokenService from "@/features/auth/domain/accessToken/GuestAccessTokenService" -import GuestCredentialsTransferrer from "@/features/auth/domain/credentialsTransfer/GuestCredentialsTransferrer" +import NullObjectCredentialsTransferrer from "@/features/auth/domain/credentialsTransfer/NullObjectCredentialsTransferrer" import HostAccessTokenService from "@/features/auth/domain/accessToken/HostAccessTokenService" import HostCredentialsTransferrer from "@/features/auth/domain/credentialsTransfer/HostCredentialsTransferrer" import IsUserGuestReader from "@/features/auth/domain/userIdentityProvider/IsUserGuestReader" @@ -181,10 +181,7 @@ export const logInHandler = new CompositeLogInHandler([ isUserGuestReader: new IsUserGuestReader( userIdentityProviderReader ), - guestCredentialsTransferrer: new GuestCredentialsTransferrer({ - repository: accessTokenRepository, - dataSource: guestAccessTokenDataSource - }), + guestCredentialsTransferrer: new NullObjectCredentialsTransferrer(), hostCredentialsTransferrer: new HostCredentialsTransferrer({ refreshTokenReader: new Auth0RefreshTokenReader({ ...auth0ManagementCredentials, diff --git a/src/features/auth/domain/credentialsTransfer/GuestCredentialsTransferrer.ts b/src/features/auth/domain/credentialsTransfer/GuestCredentialsTransferrer.ts deleted file mode 100644 index 115024da..00000000 --- a/src/features/auth/domain/credentialsTransfer/GuestCredentialsTransferrer.ts +++ /dev/null @@ -1,28 +0,0 @@ -import ICredentialsTransferrer from "./ICredentialsTransferrer" -import IUserDataRepository from "@/common/userData/IUserDataRepository" - -interface DataSource { - getAccessToken(userId: string): Promise -} - -type Repository = IUserDataRepository - -type GuestCredentialsTransferrerConfig = { - readonly dataSource: DataSource - readonly repository: Repository -} - -export default class GuestCredentialsTransferrer implements ICredentialsTransferrer { - private readonly dataSource: DataSource - private readonly repository: Repository - - constructor(config: GuestCredentialsTransferrerConfig) { - this.dataSource = config.dataSource - this.repository = config.repository - } - - async transferCredentials(userId: string): Promise { - const accessToken = await this.dataSource.getAccessToken(userId) - await this.repository.set(userId, accessToken) - } -} diff --git a/src/features/auth/domain/credentialsTransfer/NullObjectCredentialsTransferrer.ts b/src/features/auth/domain/credentialsTransfer/NullObjectCredentialsTransferrer.ts new file mode 100644 index 00000000..2b02e044 --- /dev/null +++ b/src/features/auth/domain/credentialsTransfer/NullObjectCredentialsTransferrer.ts @@ -0,0 +1,5 @@ +import ICredentialsTransferrer from "./ICredentialsTransferrer" + +export default class NullObjectCredentialsTransferrer implements ICredentialsTransferrer { + async transferCredentials(_userId: string): Promise {} +} From 87e458000bc6a31a3e841199e8b8918ab67647c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=B8vring?= Date: Tue, 7 Nov 2023 19:36:31 +0100 Subject: [PATCH 19/42] Only validates access token for host users --- src/features/auth/view/SessionAccessTokenBarrier.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/features/auth/view/SessionAccessTokenBarrier.tsx b/src/features/auth/view/SessionAccessTokenBarrier.tsx index d84ed7c7..58996582 100644 --- a/src/features/auth/view/SessionAccessTokenBarrier.tsx +++ b/src/features/auth/view/SessionAccessTokenBarrier.tsx @@ -1,6 +1,6 @@ import { ReactNode } from "react" import { redirect } from "next/navigation" -import { accessTokenService } from "@/composition" +import { session, accessTokenService } from "@/composition" export default async function SessionAccessTokenBarrier({ children @@ -8,7 +8,10 @@ export default async function SessionAccessTokenBarrier({ children: ReactNode }) { try { - await accessTokenService.getAccessToken() + const isGuest = await session.getIsGuest() + if (!isGuest) { + await accessTokenService.getAccessToken() + } return <>{children} } catch { redirect("/api/auth/logout") From 8b2911ab01ac4ff3ab2c0f81e4571588f3663fe1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=B8vring?= Date: Tue, 7 Nov 2023 19:36:54 +0100 Subject: [PATCH 20/42] Returns no projects when access token does not exist --- src/composition.ts | 12 ++++--- .../domain/ForgivingProjectDataSource.ts | 34 +++++++++++++++++++ 2 files changed, 42 insertions(+), 4 deletions(-) create mode 100644 src/features/projects/domain/ForgivingProjectDataSource.ts diff --git a/src/composition.ts b/src/composition.ts index 9f06b816..e4734f8f 100644 --- a/src/composition.ts +++ b/src/composition.ts @@ -12,6 +12,7 @@ import CompositeLogInHandler from "@/features/auth/domain/logIn/CompositeLogInHa import CompositeLogOutHandler from "@/features/auth/domain/logOut/CompositeLogOutHandler" import CredentialsTransferringLogInHandler from "@/features/auth/domain/logIn/CredentialsTransferringLogInHandler" import ErrorIgnoringLogOutHandler from "@/features/auth/domain/logOut/ErrorIgnoringLogOutHandler" +import ForgivingProjectDataSource from "./features/projects/domain/ForgivingProjectDataSource" import GitHubClient from "@/common/github/GitHubClient" import GitHubOAuthTokenRefresher from "@/features/auth/data/GitHubOAuthTokenRefresher" import GitHubOrganizationSessionValidator from "@/common/session/GitHubOrganizationSessionValidator" @@ -168,10 +169,13 @@ export const projectRepository = new ProjectRepository( export const projectDataSource = new CachingProjectDataSource( new SessionValidatingProjectDataSource( sessionValidator, - new GitHubProjectDataSource( - userGitHubClient, - GITHUB_ORGANIZATION_NAME - ) + new ForgivingProjectDataSource({ + accessTokenReader: accessTokenService, + projectDataSource: new GitHubProjectDataSource( + userGitHubClient, + GITHUB_ORGANIZATION_NAME + ) + }) ), projectRepository ) diff --git a/src/features/projects/domain/ForgivingProjectDataSource.ts b/src/features/projects/domain/ForgivingProjectDataSource.ts new file mode 100644 index 00000000..64191e1e --- /dev/null +++ b/src/features/projects/domain/ForgivingProjectDataSource.ts @@ -0,0 +1,34 @@ +import Project from "./Project" +import IProjectDataSource from "./IProjectDataSource" + +interface IAccessTokenReader { + getAccessToken(): Promise +} + +type ForgivingProjectDataSourceConfig = { + readonly accessTokenReader: IAccessTokenReader + readonly projectDataSource: IProjectDataSource +} + +export default class ForgivingProjectDataSource implements IProjectDataSource { + private readonly accessTokenReader: IAccessTokenReader + private readonly projectDataSource: IProjectDataSource + + constructor(config: ForgivingProjectDataSourceConfig) { + this.accessTokenReader = config.accessTokenReader + this.projectDataSource = config.projectDataSource + } + + async getProjects(): Promise { + try { + await this.accessTokenReader.getAccessToken() + } catch { + // If we cannot get an access token for, we show an empty list + // of projects. It is common for guest users that we cannot get + // an access token because they have been incorrectly configured + // to have access to non-existing repositories. + return [] + } + return this.projectDataSource.getProjects() + } +} From 8093c89f9c6d8d284585a130c653f0ba3dfb70b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=B8vring?= Date: Tue, 7 Nov 2023 20:03:19 +0100 Subject: [PATCH 21/42] Simplifies imports --- src/common/github/index.ts | 2 + src/common/index.ts | 5 ++ src/common/keyValueStore/index.ts | 1 + src/common/mutex/index.ts | 2 + src/common/session/index.ts | 4 + src/common/userData/index.ts | 1 + src/composition.ts | 90 ++++++++++--------- src/features/auth/data/index.ts | 6 ++ src/features/auth/domain/accessToken/index.ts | 5 ++ .../auth/domain/credentialsTransfer/index.ts | 2 + src/features/auth/domain/index.ts | 7 ++ .../logIn/RemoveInvitedFlagLogInHandler.ts | 2 +- src/features/auth/domain/logIn/index.ts | 3 + src/features/auth/domain/logOut/index.ts | 3 + src/features/auth/domain/oAuthToken/index.ts | 1 + .../auth/domain/repositoryAccess/index.ts | 2 + .../auth/domain/userIdentityProvider/index.ts | 2 + src/features/projects/data/index.ts | 1 + src/features/projects/domain/index.ts | 4 + 19 files changed, 102 insertions(+), 41 deletions(-) create mode 100644 src/common/github/index.ts create mode 100644 src/common/index.ts create mode 100644 src/common/keyValueStore/index.ts create mode 100644 src/common/mutex/index.ts create mode 100644 src/common/session/index.ts create mode 100644 src/common/userData/index.ts create mode 100644 src/features/auth/data/index.ts create mode 100644 src/features/auth/domain/accessToken/index.ts create mode 100644 src/features/auth/domain/credentialsTransfer/index.ts create mode 100644 src/features/auth/domain/index.ts create mode 100644 src/features/auth/domain/logIn/index.ts create mode 100644 src/features/auth/domain/logOut/index.ts create mode 100644 src/features/auth/domain/oAuthToken/index.ts create mode 100644 src/features/auth/domain/repositoryAccess/index.ts create mode 100644 src/features/auth/domain/userIdentityProvider/index.ts create mode 100644 src/features/projects/data/index.ts create mode 100644 src/features/projects/domain/index.ts diff --git a/src/common/github/index.ts b/src/common/github/index.ts new file mode 100644 index 00000000..86c418d6 --- /dev/null +++ b/src/common/github/index.ts @@ -0,0 +1,2 @@ +export { default as AccessTokenRefreshingGitHubClient } from "./AccessTokenRefreshingGitHubClient" +export { default as GitHubClient } from "./GitHubClient" diff --git a/src/common/index.ts b/src/common/index.ts new file mode 100644 index 00000000..ca794e57 --- /dev/null +++ b/src/common/index.ts @@ -0,0 +1,5 @@ +export * from "./github" +export * from "./keyValueStore" +export * from "./mutex" +export * from "./session" +export * from "./userData" diff --git a/src/common/keyValueStore/index.ts b/src/common/keyValueStore/index.ts new file mode 100644 index 00000000..2d035899 --- /dev/null +++ b/src/common/keyValueStore/index.ts @@ -0,0 +1 @@ +export { default as RedisKeyValueStore } from "./RedisKeyValueStore" diff --git a/src/common/mutex/index.ts b/src/common/mutex/index.ts new file mode 100644 index 00000000..139d3e2b --- /dev/null +++ b/src/common/mutex/index.ts @@ -0,0 +1,2 @@ +export { default as RedisKeyedMutexFactory } from "./RedisKeyedMutexFactory" +export { default as SessionMutexFactory } from "./SessionMutexFactory" diff --git a/src/common/session/index.ts b/src/common/session/index.ts new file mode 100644 index 00000000..5831d41b --- /dev/null +++ b/src/common/session/index.ts @@ -0,0 +1,4 @@ +export { default as AlwaysValidSessionValidator } from "./AlwaysValidSessionValidator" +export { default as Auth0Session } from "./Auth0Session" +export { default as GitHubOrganizationSessionValidator } from "./GitHubOrganizationSessionValidator" +export { default as SessionValidator } from "./SessionValidator" diff --git a/src/common/userData/index.ts b/src/common/userData/index.ts new file mode 100644 index 00000000..78c446f8 --- /dev/null +++ b/src/common/userData/index.ts @@ -0,0 +1 @@ +export { default as KeyValueUserDataRepository } from "./KeyValueUserDataRepository" diff --git a/src/composition.ts b/src/composition.ts index e4734f8f..9953e0a0 100644 --- a/src/composition.ts +++ b/src/composition.ts @@ -1,42 +1,52 @@ -import AccessTokenRefreshingGitHubClient from "@/common/github/AccessTokenRefreshingGitHubClient" -import AlwaysValidSessionValidator from "@/common/session/AlwaysValidSessionValidator" -import Auth0MetadataUpdater from "./features/auth/data/Auth0MetadataUpdater" -import Auth0RefreshTokenReader from "@/features/auth/data/Auth0RefreshTokenReader" -import Auth0RepositoryAccessReader from "./features/auth/data/Auth0RepositoryAccessReader" -import Auth0Session from "@/common/session/Auth0Session" -import Auth0UserIdentityProviderReader from "./features/auth/data/Auth0UserIdentityProviderReader" -import CachingProjectDataSource from "@/features/projects/domain/CachingProjectDataSource" -import CachingRepositoryAccessReaderConfig from "./features/auth/domain/repositoryAccess/CachingRepositoryAccessReaderConfig" -import CachingUserIdentityProviderReader from "./features/auth/domain/userIdentityProvider/CachingUserIdentityProviderReader" -import CompositeLogInHandler from "@/features/auth/domain/logIn/CompositeLogInHandler" -import CompositeLogOutHandler from "@/features/auth/domain/logOut/CompositeLogOutHandler" -import CredentialsTransferringLogInHandler from "@/features/auth/domain/logIn/CredentialsTransferringLogInHandler" -import ErrorIgnoringLogOutHandler from "@/features/auth/domain/logOut/ErrorIgnoringLogOutHandler" -import ForgivingProjectDataSource from "./features/projects/domain/ForgivingProjectDataSource" -import GitHubClient from "@/common/github/GitHubClient" -import GitHubOAuthTokenRefresher from "@/features/auth/data/GitHubOAuthTokenRefresher" -import GitHubOrganizationSessionValidator from "@/common/session/GitHubOrganizationSessionValidator" -import GitHubProjectDataSource from "@/features/projects/data/GitHubProjectDataSource" -import GitHubInstallationAccessTokenDataSource from "@/features/auth/data/GitHubInstallationAccessTokenDataSource" -import GuestAccessTokenService from "@/features/auth/domain/accessToken/GuestAccessTokenService" -import NullObjectCredentialsTransferrer from "@/features/auth/domain/credentialsTransfer/NullObjectCredentialsTransferrer" -import HostAccessTokenService from "@/features/auth/domain/accessToken/HostAccessTokenService" -import HostCredentialsTransferrer from "@/features/auth/domain/credentialsTransfer/HostCredentialsTransferrer" -import IsUserGuestReader from "@/features/auth/domain/userIdentityProvider/IsUserGuestReader" -import KeyValueUserDataRepository from "@/common/userData/KeyValueUserDataRepository" -import LockingAccessTokenService from "@/features/auth/domain/accessToken/LockingAccessTokenService" -import OAuthTokenRepository from "@/features/auth/domain/oAuthToken/OAuthTokenRepository" -import OnlyStaleRefreshingAccessTokenService from "@/features/auth/domain/accessToken/OnlyStaleRefreshingAccessTokenService" -import ProjectRepository from "@/features/projects/domain/ProjectRepository" -import RedisKeyedMutexFactory from "@/common/mutex/RedisKeyedMutexFactory" -import RedisKeyValueStore from "@/common/keyValueStore/RedisKeyValueStore" -import RemoveInvitedFlagLoginHandler from "./features/auth/domain/logIn/RemoveInvitedFlagLogInHandler" -import RepositoryRestrictingAccessTokenDataSource from "@/features/auth/domain/repositoryAccess/RepositoryRestrictingAccessTokenDataSource" -import SessionAccessTokenService from "@/features/auth/domain/accessToken/SessionAccessTokenService" -import SessionMutexFactory from "@/common/mutex/SessionMutexFactory" -import SessionValidatingProjectDataSource from "@/features/projects/domain/SessionValidatingProjectDataSource" -import SessionValidator from "@/common/session/SessionValidator" -import UserDataCleanUpLogOutHandler from "@/features/auth/domain/logOut/UserDataCleanUpLogOutHandler" +import { + AccessTokenRefreshingGitHubClient, + AlwaysValidSessionValidator, + Auth0Session, + GitHubClient, + GitHubOrganizationSessionValidator, + KeyValueUserDataRepository, + RedisKeyedMutexFactory, + RedisKeyValueStore, + SessionMutexFactory, + SessionValidator +} from "@/common" +import { + GitHubProjectDataSource +} from "@/features/projects/data" +import { + CachingProjectDataSource, + ForgivingProjectDataSource, + ProjectRepository, + SessionValidatingProjectDataSource +} from "@/features/projects/domain" +import { + GitHubOAuthTokenRefresher, + GitHubInstallationAccessTokenDataSource, + Auth0MetadataUpdater, + Auth0RefreshTokenReader, + Auth0RepositoryAccessReader, + Auth0UserIdentityProviderReader +} from "@/features/auth/data" +import { + CachingRepositoryAccessReaderConfig, + CachingUserIdentityProviderReader, + CompositeLogInHandler, + CompositeLogOutHandler, + CredentialsTransferringLogInHandler, + ErrorIgnoringLogOutHandler, + GuestAccessTokenService, + NullObjectCredentialsTransferrer, + HostAccessTokenService, + HostCredentialsTransferrer, + IsUserGuestReader, + LockingAccessTokenService, + OAuthTokenRepository, + OnlyStaleRefreshingAccessTokenService, + RemoveInvitedFlagLogInHandler, + RepositoryRestrictingAccessTokenDataSource, + SessionAccessTokenService, + UserDataCleanUpLogOutHandler +} from "@/features/auth/domain" const { AUTH0_MANAGEMENT_DOMAIN, @@ -195,7 +205,7 @@ export const logInHandler = new CompositeLogInHandler([ oAuthTokenRepository: oAuthTokenRepository }) }), - new RemoveInvitedFlagLoginHandler( + new RemoveInvitedFlagLogInHandler( new Auth0MetadataUpdater({ ...auth0ManagementCredentials }) ) ]) diff --git a/src/features/auth/data/index.ts b/src/features/auth/data/index.ts new file mode 100644 index 00000000..8090466b --- /dev/null +++ b/src/features/auth/data/index.ts @@ -0,0 +1,6 @@ +export { default as Auth0MetadataUpdater } from "./Auth0MetadataUpdater" +export { default as Auth0RefreshTokenReader } from "./Auth0RefreshTokenReader" +export { default as Auth0RepositoryAccessReader } from "./Auth0RepositoryAccessReader" +export { default as Auth0UserIdentityProviderReader } from "./Auth0UserIdentityProviderReader" +export { default as GitHubInstallationAccessTokenDataSource } from "./GitHubInstallationAccessTokenDataSource" +export { default as GitHubOAuthTokenRefresher } from "./GitHubOAuthTokenRefresher" diff --git a/src/features/auth/domain/accessToken/index.ts b/src/features/auth/domain/accessToken/index.ts new file mode 100644 index 00000000..814fdf3d --- /dev/null +++ b/src/features/auth/domain/accessToken/index.ts @@ -0,0 +1,5 @@ +export { default as GuestAccessTokenService } from "./GuestAccessTokenService" +export { default as HostAccessTokenService } from "./HostAccessTokenService" +export { default as LockingAccessTokenService } from "./LockingAccessTokenService" +export { default as OnlyStaleRefreshingAccessTokenService } from "./OnlyStaleRefreshingAccessTokenService" +export { default as SessionAccessTokenService } from "./SessionAccessTokenService" diff --git a/src/features/auth/domain/credentialsTransfer/index.ts b/src/features/auth/domain/credentialsTransfer/index.ts new file mode 100644 index 00000000..3961d607 --- /dev/null +++ b/src/features/auth/domain/credentialsTransfer/index.ts @@ -0,0 +1,2 @@ +export { default as HostCredentialsTransferrer } from "./HostCredentialsTransferrer" +export { default as NullObjectCredentialsTransferrer } from "./NullObjectCredentialsTransferrer" diff --git a/src/features/auth/domain/index.ts b/src/features/auth/domain/index.ts new file mode 100644 index 00000000..63cbe382 --- /dev/null +++ b/src/features/auth/domain/index.ts @@ -0,0 +1,7 @@ +export * from "./accessToken" +export * from "./credentialsTransfer" +export * from "./logIn" +export * from "./logOut" +export * from "./oAuthToken" +export * from "./repositoryAccess" +export * from "./userIdentityProvider" diff --git a/src/features/auth/domain/logIn/RemoveInvitedFlagLogInHandler.ts b/src/features/auth/domain/logIn/RemoveInvitedFlagLogInHandler.ts index d2de0220..c1565e9d 100644 --- a/src/features/auth/domain/logIn/RemoveInvitedFlagLogInHandler.ts +++ b/src/features/auth/domain/logIn/RemoveInvitedFlagLogInHandler.ts @@ -5,7 +5,7 @@ export interface IMetadataUpdater { updateMetadata(userId: string, metadata: {[key: string]: any}): Promise } -export default class RemoveInvitedFlagLoginHandler implements ILogInHandler { +export default class RemoveInvitedFlagLogInHandler implements ILogInHandler { private readonly metadataUpdater: IMetadataUpdater constructor(metadataUpdater: IMetadataUpdater) { diff --git a/src/features/auth/domain/logIn/index.ts b/src/features/auth/domain/logIn/index.ts new file mode 100644 index 00000000..ec98a5e6 --- /dev/null +++ b/src/features/auth/domain/logIn/index.ts @@ -0,0 +1,3 @@ +export { default as CompositeLogInHandler } from "./CompositeLogInHandler" +export { default as CredentialsTransferringLogInHandler } from "./CredentialsTransferringLogInHandler" +export { default as RemoveInvitedFlagLogInHandler } from "./RemoveInvitedFlagLogInHandler" diff --git a/src/features/auth/domain/logOut/index.ts b/src/features/auth/domain/logOut/index.ts new file mode 100644 index 00000000..28840ac6 --- /dev/null +++ b/src/features/auth/domain/logOut/index.ts @@ -0,0 +1,3 @@ +export { default as CompositeLogOutHandler } from "./CompositeLogOutHandler" +export { default as ErrorIgnoringLogOutHandler } from "./ErrorIgnoringLogOutHandler" +export { default as UserDataCleanUpLogOutHandler } from "./UserDataCleanUpLogOutHandler" diff --git a/src/features/auth/domain/oAuthToken/index.ts b/src/features/auth/domain/oAuthToken/index.ts new file mode 100644 index 00000000..7d79ca92 --- /dev/null +++ b/src/features/auth/domain/oAuthToken/index.ts @@ -0,0 +1 @@ +export { default as OAuthTokenRepository } from "./OAuthTokenRepository" diff --git a/src/features/auth/domain/repositoryAccess/index.ts b/src/features/auth/domain/repositoryAccess/index.ts new file mode 100644 index 00000000..3f3e42b7 --- /dev/null +++ b/src/features/auth/domain/repositoryAccess/index.ts @@ -0,0 +1,2 @@ +export { default as CachingRepositoryAccessReaderConfig } from "./CachingRepositoryAccessReaderConfig" +export { default as RepositoryRestrictingAccessTokenDataSource } from "./RepositoryRestrictingAccessTokenDataSource" diff --git a/src/features/auth/domain/userIdentityProvider/index.ts b/src/features/auth/domain/userIdentityProvider/index.ts new file mode 100644 index 00000000..a94ce5d2 --- /dev/null +++ b/src/features/auth/domain/userIdentityProvider/index.ts @@ -0,0 +1,2 @@ +export { default as CachingUserIdentityProviderReader } from "./CachingUserIdentityProviderReader" +export { default as IsUserGuestReader } from "./IsUserGuestReader" diff --git a/src/features/projects/data/index.ts b/src/features/projects/data/index.ts new file mode 100644 index 00000000..62d1e9d2 --- /dev/null +++ b/src/features/projects/data/index.ts @@ -0,0 +1 @@ +export { default as GitHubProjectDataSource } from "./GitHubProjectDataSource" diff --git a/src/features/projects/domain/index.ts b/src/features/projects/domain/index.ts new file mode 100644 index 00000000..c74d9975 --- /dev/null +++ b/src/features/projects/domain/index.ts @@ -0,0 +1,4 @@ +export { default as CachingProjectDataSource } from "./CachingProjectDataSource" +export { default as ForgivingProjectDataSource } from "./ForgivingProjectDataSource" +export { default as ProjectRepository } from "./ProjectRepository" +export { default as SessionValidatingProjectDataSource } from "./SessionValidatingProjectDataSource" From 92f9f705ca17e38d04f87546a5e257e938125405 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=B8vring?= Date: Tue, 7 Nov 2023 20:07:29 +0100 Subject: [PATCH 22/42] Removes irrelevant tests --- .../auth/GuestCredentialsTransferrer.test.ts | 47 ------------------- 1 file changed, 47 deletions(-) delete mode 100644 __test__/auth/GuestCredentialsTransferrer.test.ts diff --git a/__test__/auth/GuestCredentialsTransferrer.test.ts b/__test__/auth/GuestCredentialsTransferrer.test.ts deleted file mode 100644 index 6bc17fe0..00000000 --- a/__test__/auth/GuestCredentialsTransferrer.test.ts +++ /dev/null @@ -1,47 +0,0 @@ -import GuestCredentialsTransferrer from "../../src/features/auth/domain/credentialsTransfer/GuestCredentialsTransferrer" - -test("It fetches an access token for the specified user from the data source", async () => { - let didFetchAccessToken = false - const sut = new GuestCredentialsTransferrer({ - dataSource: { - async getAccessToken() { - didFetchAccessToken = true - return "foo" - } - }, - repository: { - async get() { - return "foo" - }, - async set() {}, - async delete() {}, - } - }) - await sut.transferCredentials("123") - expect(didFetchAccessToken).toBeTruthy() -}) - -test("It stores the feteched access token for the specified user", async () => { - let storedUserId: string | undefined - let storedAccessToken: string | undefined - const sut = new GuestCredentialsTransferrer({ - dataSource: { - async getAccessToken() { - return "foo" - } - }, - repository: { - async get() { - return "foo" - }, - async set(userId, accessToken) { - storedUserId = userId - storedAccessToken = accessToken - }, - async delete() {}, - } - }) - await sut.transferCredentials("123") - expect(storedUserId).toBe("123") - expect(storedAccessToken).toBe("foo") -}) From 231dac4695be16cf7580bea8a9379d74c074da1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=B8vring?= Date: Tue, 7 Nov 2023 20:08:32 +0100 Subject: [PATCH 23/42] F --- __test__/auth/GuestAccessTokenService.test.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/__test__/auth/GuestAccessTokenService.test.ts b/__test__/auth/GuestAccessTokenService.test.ts index ea271ce3..0bac0224 100644 --- a/__test__/auth/GuestAccessTokenService.test.ts +++ b/__test__/auth/GuestAccessTokenService.test.ts @@ -26,7 +26,8 @@ test("It gets the access token for the user", async () => { expect(accessToken).toBe("foo") }) -test("It throws an error when the access token is null", async () => { +test("It refreshes access token on demand when there is no cached access token", async () => { + let didRefreshAccessToken = false const sut = new GuestAccessTokenService({ userIdReader: { async getUserId() { @@ -41,9 +42,11 @@ test("It throws an error when the access token is null", async () => { }, dataSource: { async getAccessToken() { + didRefreshAccessToken = true return "foo" } } }) - expect(sut.getAccessToken()).rejects.toThrow() + await sut.getAccessToken() + expect(didRefreshAccessToken).toBeTruthy() }) From fa19fc0637dd031236bba5ae60542a1399881ada Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=B8vring?= Date: Tue, 7 Nov 2023 20:09:09 +0100 Subject: [PATCH 24/42] Fixes linting warnings --- .../auth/domain/accessToken/GuestAccessTokenService.ts | 1 - .../repositoryAccess/CachingRepositoryAccessReaderConfig.ts | 6 ++---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/features/auth/domain/accessToken/GuestAccessTokenService.ts b/src/features/auth/domain/accessToken/GuestAccessTokenService.ts index 701051eb..a30c26f3 100644 --- a/src/features/auth/domain/accessToken/GuestAccessTokenService.ts +++ b/src/features/auth/domain/accessToken/GuestAccessTokenService.ts @@ -1,4 +1,3 @@ -import { UnauthorizedError } from "../../../../common/errors" import IAccessTokenService from "./IAccessTokenService" export interface IUserIDReader { diff --git a/src/features/auth/domain/repositoryAccess/CachingRepositoryAccessReaderConfig.ts b/src/features/auth/domain/repositoryAccess/CachingRepositoryAccessReaderConfig.ts index e0dcd881..11574d07 100644 --- a/src/features/auth/domain/repositoryAccess/CachingRepositoryAccessReaderConfig.ts +++ b/src/features/auth/domain/repositoryAccess/CachingRepositoryAccessReaderConfig.ts @@ -39,8 +39,7 @@ export default class CachingRepositoryAccessReader { } try { return ZodJSONCoder.decode(RepositoryNamesContainerSchema, str) - /* eslint-disable-next-line no-empty */ - } catch (error: any) { + } catch (error: unknown) { console.error(error) return null } @@ -51,8 +50,7 @@ export default class CachingRepositoryAccessReader { try { const str = ZodJSONCoder.encode(RepositoryNamesContainerSchema, repositoryNames) await this.repository.set(userId, str) - /* eslint-disable-next-line no-empty */ - } catch (error: any) { + } catch (error: unknown) { console.error(error) } return repositoryNames From 52a79505a24a6f4ae33616ee4b37eec87b8d8bac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=B8vring?= Date: Tue, 7 Nov 2023 20:46:04 +0100 Subject: [PATCH 25/42] Improves imports --- .../CachingRepositoryAccessReaderConfig.ts | 2 +- .../CachingUserIdentityProviderReader.test.ts | 4 ++-- __test__/auth/CompositeLogInHandler.test.ts | 2 +- __test__/auth/CompositeLogOutHandler.test.ts | 2 +- ...redentialsTransferringLogInHandler.test.ts | 2 +- .../auth/ErrorIgnoringLogOutHandler.test.ts | 2 +- __test__/auth/GuestAccessTokenService.test.ts | 2 +- __test__/auth/HostAccessTokenService.test.ts | 4 ++-- .../auth/HostCredentialsTransferrer.test.ts | 4 ++-- __test__/auth/IsUserGuestReader.test.ts | 4 ++-- .../auth/LockingAccessTokenService.test.ts | 2 +- __test__/auth/OAuthTokenRepository.test.ts | 2 +- ...yStaleRefreshingAccessTokenService.test.ts | 2 +- .../RemoveInvitedFlagLogInHandler.test.ts | 2 +- ...ositoryRestrictingAccessTokenDataSource.ts | 2 +- .../auth/SessionAccessTokenService.test.ts | 2 +- .../auth/UserDataCleanUpLogOutHandler.test.ts | 2 +- .../AccessTokenRefreshingGitHubClient.test.ts | 2 +- ...GitHubOrganizationSessionValidator.test.ts | 2 +- .../common/session/SessionValidator.test.ts | 2 +- .../KeyValueUserDataRepository.test.ts | 2 +- __test__/common/utils/ZodJSONCoder.test.ts | 2 +- ...entCheckingPullRequestEventHandler.test.ts | 2 +- __test__/hooks/GitHubCommentFactory.test.ts | 2 +- ...PostCommentPullRequestEventHandler.test.ts | 2 +- ...ameCheckingPullRequestEventHandler.test.ts | 2 +- .../projects/CachingProjectDataSource.test.ts | 4 ++-- __test__/projects/ProjectConfigParser.test.ts | 2 +- ...SessionValidatingProjectDataSource.test.ts | 2 +- __test__/projects/getSelection.test.ts | 2 +- __test__/projects/projectNavigator.test.ts | 2 +- __test__/projects/updateWindowTitle.test.ts | 2 +- src/app/[...slug]/page.tsx | 2 +- src/app/api/user/projects/route.ts | 2 +- src/app/layout.tsx | 4 ++-- src/common/errors/client/ErrorHandler.tsx | 2 +- src/common/github/index.ts | 1 + src/common/index.ts | 2 ++ src/common/mutex/SessionMutexFactory.ts | 2 +- src/common/mutex/index.ts | 3 ++- src/common/session/Auth0Session.ts | 2 +- src/common/session/index.ts | 2 ++ src/common/userData/index.ts | 1 + src/common/utils/index.ts | 4 ++++ .../auth/data/Auth0RefreshTokenReader.ts | 2 +- .../data/Auth0UserIdentityProviderReader.ts | 2 +- .../auth/data/GitHubOAuthTokenRefresher.ts | 2 +- .../accessToken/LockingAccessTokenService.ts | 2 +- .../domain/oAuthToken/OAuthTokenRepository.ts | 4 +--- src/features/auth/domain/oAuthToken/index.ts | 1 + .../CachingRepositoryAccessReaderConfig.ts | 3 +-- .../CachingUserIdentityProviderReader.ts | 2 +- .../auth/domain/userIdentityProvider/index.ts | 1 + .../GitHubPullRequestCommentRepository.ts | 2 +- src/features/hooks/domain/index.ts | 4 ++++ .../projects/data/GitHubProjectDataSource.ts | 2 +- src/features/projects/data/useProjects.ts | 2 +- .../projects/domain/ProjectRepository.ts | 20 ++++++++++--------- .../SessionValidatingProjectDataSource.ts | 3 +-- src/features/projects/domain/index.ts | 5 +++++ 60 files changed, 89 insertions(+), 69 deletions(-) create mode 100644 src/common/utils/index.ts create mode 100644 src/features/hooks/domain/index.ts diff --git a/__test__/auth/CachingRepositoryAccessReaderConfig.ts b/__test__/auth/CachingRepositoryAccessReaderConfig.ts index 5715ce7f..3ee2aa6a 100644 --- a/__test__/auth/CachingRepositoryAccessReaderConfig.ts +++ b/__test__/auth/CachingRepositoryAccessReaderConfig.ts @@ -1,4 +1,4 @@ -import CachingRepositoryAccessReaderConfig from "../../src/features/auth/domain/repositoryAccess/CachingRepositoryAccessReaderConfig" +import { CachingRepositoryAccessReaderConfig } from "../../src/features/auth/domain" test("It fetches repository names for user if they are not cached", async () => { let didFetchRepositoryNames = false diff --git a/__test__/auth/CachingUserIdentityProviderReader.test.ts b/__test__/auth/CachingUserIdentityProviderReader.test.ts index bc4a760e..60515704 100644 --- a/__test__/auth/CachingUserIdentityProviderReader.test.ts +++ b/__test__/auth/CachingUserIdentityProviderReader.test.ts @@ -1,5 +1,5 @@ -import CachingUserIdentityProviderReader from "../../src/features/auth/domain/userIdentityProvider/CachingUserIdentityProviderReader" -import UserIdentityProvider from "../../src/features/auth/domain/userIdentityProvider/UserIdentityProvider" +import { CachingUserIdentityProviderReader } from "../../src/features/auth/domain" +import { UserIdentityProvider } from "../../src/features/auth/domain" test("It fetches user identity provider if it is not cached", async () => { let didFetchUserIdentityProvider = false diff --git a/__test__/auth/CompositeLogInHandler.test.ts b/__test__/auth/CompositeLogInHandler.test.ts index 7438eef0..c342ffbb 100644 --- a/__test__/auth/CompositeLogInHandler.test.ts +++ b/__test__/auth/CompositeLogInHandler.test.ts @@ -1,4 +1,4 @@ -import CompositeLogInHandler from "../../src/features/auth/domain/logIn/CompositeLogInHandler" +import { CompositeLogInHandler } from "../../src/features/auth/domain" test("It invokes all log in handlers for user", async () => { let userId1: string | undefined diff --git a/__test__/auth/CompositeLogOutHandler.test.ts b/__test__/auth/CompositeLogOutHandler.test.ts index 9618723c..f95757bf 100644 --- a/__test__/auth/CompositeLogOutHandler.test.ts +++ b/__test__/auth/CompositeLogOutHandler.test.ts @@ -1,4 +1,4 @@ -import CompositeLogOutHandler from "../../src/features/auth/domain/logOut/CompositeLogOutHandler" +import { CompositeLogOutHandler } from "../../src/features/auth/domain" test("It invokes all log out handlers", async () => { let didCallLogOutHandler1 = false diff --git a/__test__/auth/CredentialsTransferringLogInHandler.test.ts b/__test__/auth/CredentialsTransferringLogInHandler.test.ts index ff33407c..f1061b40 100644 --- a/__test__/auth/CredentialsTransferringLogInHandler.test.ts +++ b/__test__/auth/CredentialsTransferringLogInHandler.test.ts @@ -1,4 +1,4 @@ -import CredentialsTransferringLogInHandler from "../../src/features/auth/domain/logIn/CredentialsTransferringLogInHandler" +import { CredentialsTransferringLogInHandler } from "../../src/features/auth/domain" test("It transfers credentials for guest", async () => { let didTransferGuestCredentials = false diff --git a/__test__/auth/ErrorIgnoringLogOutHandler.test.ts b/__test__/auth/ErrorIgnoringLogOutHandler.test.ts index 5feae77c..a7bacdd0 100644 --- a/__test__/auth/ErrorIgnoringLogOutHandler.test.ts +++ b/__test__/auth/ErrorIgnoringLogOutHandler.test.ts @@ -1,4 +1,4 @@ -import ErrorIgnoringLogOutHandler from "../../src/features/auth/domain/logOut/ErrorIgnoringLogOutHandler" +import { ErrorIgnoringLogOutHandler } from "../../src/features/auth/domain" test("It ignores errors", async () => { const sut = new ErrorIgnoringLogOutHandler({ diff --git a/__test__/auth/GuestAccessTokenService.test.ts b/__test__/auth/GuestAccessTokenService.test.ts index 0bac0224..708a630d 100644 --- a/__test__/auth/GuestAccessTokenService.test.ts +++ b/__test__/auth/GuestAccessTokenService.test.ts @@ -1,4 +1,4 @@ -import GuestAccessTokenService from "../../src/features/auth/domain/accessToken/GuestAccessTokenService" +import { GuestAccessTokenService } from "../../src/features/auth/domain" test("It gets the access token for the user", async () => { let readUserId: string | undefined diff --git a/__test__/auth/HostAccessTokenService.test.ts b/__test__/auth/HostAccessTokenService.test.ts index 05babebe..79fcaf72 100644 --- a/__test__/auth/HostAccessTokenService.test.ts +++ b/__test__/auth/HostAccessTokenService.test.ts @@ -1,5 +1,5 @@ -import HostAccessTokenService from "../../src/features/auth/domain/accessToken/HostAccessTokenService" -import OAuthToken from "../../src/features/auth/domain/oAuthToken/OAuthToken" +import { HostAccessTokenService } from "../../src/features/auth/domain" +import { OAuthToken } from "../../src/features/auth/domain" test("It gets the access token for the user", async () => { let readUserID: string | undefined diff --git a/__test__/auth/HostCredentialsTransferrer.test.ts b/__test__/auth/HostCredentialsTransferrer.test.ts index 7ea596bd..a5988aa8 100644 --- a/__test__/auth/HostCredentialsTransferrer.test.ts +++ b/__test__/auth/HostCredentialsTransferrer.test.ts @@ -1,5 +1,5 @@ -import HostCredentialsTransferrer from "../../src/features/auth/domain/credentialsTransfer/HostCredentialsTransferrer" -import OAuthToken from "../../src/features/auth/domain/oAuthToken/OAuthToken" +import { HostCredentialsTransferrer } from "../../src/features/auth/domain" +import { OAuthToken } from "../../src/features/auth/domain" test("It fetches refresh token for specified user", async () => { let fetchedUserId: string | undefined diff --git a/__test__/auth/IsUserGuestReader.test.ts b/__test__/auth/IsUserGuestReader.test.ts index 5e121bc1..2d604818 100644 --- a/__test__/auth/IsUserGuestReader.test.ts +++ b/__test__/auth/IsUserGuestReader.test.ts @@ -1,5 +1,5 @@ -import IsUserGuestReader from "../../src/features/auth/domain/userIdentityProvider/IsUserGuestReader" -import UserIdentityProvider from "../../src/features/auth/domain/userIdentityProvider/UserIdentityProvider" +import { IsUserGuestReader } from "../../src/features/auth/domain" +import { UserIdentityProvider } from "../../src/features/auth/domain" test("It does not consider a user to be a guest if they are logged in with GitHub", async () => { const sut = new IsUserGuestReader({ diff --git a/__test__/auth/LockingAccessTokenService.test.ts b/__test__/auth/LockingAccessTokenService.test.ts index 28dad317..30996afb 100644 --- a/__test__/auth/LockingAccessTokenService.test.ts +++ b/__test__/auth/LockingAccessTokenService.test.ts @@ -1,4 +1,4 @@ -import LockingAccessTokenService from "../../src/features/auth/domain/accessToken/LockingAccessTokenService" +import { LockingAccessTokenService } from "../../src/features/auth/domain" test("It reads access token", async () => { let didReadAccessToken = false diff --git a/__test__/auth/OAuthTokenRepository.test.ts b/__test__/auth/OAuthTokenRepository.test.ts index 3894338e..b5786016 100644 --- a/__test__/auth/OAuthTokenRepository.test.ts +++ b/__test__/auth/OAuthTokenRepository.test.ts @@ -1,4 +1,4 @@ -import OAuthTokenRepository from "../../src/features/auth/domain/oAuthToken/OAuthTokenRepository" +import { OAuthTokenRepository } from "../../src/features/auth/domain" test("It reads the auth token for the specified user", async () => { let readUserId: string | undefined diff --git a/__test__/auth/OnlyStaleRefreshingAccessTokenService.test.ts b/__test__/auth/OnlyStaleRefreshingAccessTokenService.test.ts index 016d224d..f6dedfda 100644 --- a/__test__/auth/OnlyStaleRefreshingAccessTokenService.test.ts +++ b/__test__/auth/OnlyStaleRefreshingAccessTokenService.test.ts @@ -1,4 +1,4 @@ -import OnlyStaleRefreshingAccessTokenService from "../../src/features/auth/domain/accessToken/OnlyStaleRefreshingAccessTokenService" +import { OnlyStaleRefreshingAccessTokenService } from "../../src/features/auth/domain" test("It refreshes the access token when the input access token is equal to the stored access token", async () => { let didRefreshAccessToken = false diff --git a/__test__/auth/RemoveInvitedFlagLogInHandler.test.ts b/__test__/auth/RemoveInvitedFlagLogInHandler.test.ts index ef8dc3b4..e4a276cd 100644 --- a/__test__/auth/RemoveInvitedFlagLogInHandler.test.ts +++ b/__test__/auth/RemoveInvitedFlagLogInHandler.test.ts @@ -1,4 +1,4 @@ -import RemoveInvitedFlagLogInHandler from "../../src/features/auth/domain/logIn/RemoveInvitedFlagLogInHandler" +import { RemoveInvitedFlagLogInHandler } from "../../src/features/auth/domain" test("It removes invited flag from specified user", async () => { let updatedUserId: string | undefined diff --git a/__test__/auth/RepositoryRestrictingAccessTokenDataSource.ts b/__test__/auth/RepositoryRestrictingAccessTokenDataSource.ts index 149a4487..c4a26be7 100644 --- a/__test__/auth/RepositoryRestrictingAccessTokenDataSource.ts +++ b/__test__/auth/RepositoryRestrictingAccessTokenDataSource.ts @@ -1,4 +1,4 @@ -import RepositoryRestrictingAccessTokenDataSource from "../../src/features/auth/domain/repositoryAccess/RepositoryRestrictingAccessTokenDataSource" +import { RepositoryRestrictingAccessTokenDataSource } from "../../src/features/auth/domain" test("It limits access to the fetched repositories", async () => { let restrictingRepositoryNames: string[] | undefined diff --git a/__test__/auth/SessionAccessTokenService.test.ts b/__test__/auth/SessionAccessTokenService.test.ts index 07f5b905..14920da2 100644 --- a/__test__/auth/SessionAccessTokenService.test.ts +++ b/__test__/auth/SessionAccessTokenService.test.ts @@ -1,4 +1,4 @@ -import SessionAccessTokenService from "../../src/features/auth/domain/accessToken/SessionAccessTokenService" +import { SessionAccessTokenService } from "../../src/features/auth/domain" test("It reads the access token for a guest user", async () => { let didReadAccessToken = false diff --git a/__test__/auth/UserDataCleanUpLogOutHandler.test.ts b/__test__/auth/UserDataCleanUpLogOutHandler.test.ts index c8db0461..6cbfc03d 100644 --- a/__test__/auth/UserDataCleanUpLogOutHandler.test.ts +++ b/__test__/auth/UserDataCleanUpLogOutHandler.test.ts @@ -1,4 +1,4 @@ -import UserDataCleanUpLogOutHandler from "../../src/features/auth/domain/logOut/UserDataCleanUpLogOutHandler" +import { UserDataCleanUpLogOutHandler } from "../../src/features/auth/domain" test("It deletes data for the read user ID", async () => { let deletedUserId: string | undefined diff --git a/__test__/common/github/AccessTokenRefreshingGitHubClient.test.ts b/__test__/common/github/AccessTokenRefreshingGitHubClient.test.ts index 59b667b4..0345ea9c 100644 --- a/__test__/common/github/AccessTokenRefreshingGitHubClient.test.ts +++ b/__test__/common/github/AccessTokenRefreshingGitHubClient.test.ts @@ -1,4 +1,4 @@ -import AccessTokenRefreshingGitHubClient from "../../../src/common/github/AccessTokenRefreshingGitHubClient" +import { AccessTokenRefreshingGitHubClient } from "../../../src/common" import { GraphQLQueryRequest, GetRepositoryContentRequest, diff --git a/__test__/common/session/GitHubOrganizationSessionValidator.test.ts b/__test__/common/session/GitHubOrganizationSessionValidator.test.ts index 105039de..5e530047 100644 --- a/__test__/common/session/GitHubOrganizationSessionValidator.test.ts +++ b/__test__/common/session/GitHubOrganizationSessionValidator.test.ts @@ -1,7 +1,7 @@ import { GetOrganizationMembershipStatusRequest } from "../../../src/common/github/IGitHubClient" -import GitHubOrganizationSessionValidator from "../../../src/common/session/GitHubOrganizationSessionValidator" +import { GitHubOrganizationSessionValidator } from "../../../src/common" test("It requests organization membership status for the specified organization", async () => { let queriedOrganizationName: string | undefined diff --git a/__test__/common/session/SessionValidator.test.ts b/__test__/common/session/SessionValidator.test.ts index 8435eda6..057bd6cd 100644 --- a/__test__/common/session/SessionValidator.test.ts +++ b/__test__/common/session/SessionValidator.test.ts @@ -1,4 +1,4 @@ -import SessionValidator from "../../../src/common/session/SessionValidator" +import { SessionValidator } from "../../../src/common" test("It validates a host user", async () => { let didValidateHostUser = false diff --git a/__test__/common/userData/KeyValueUserDataRepository.test.ts b/__test__/common/userData/KeyValueUserDataRepository.test.ts index dbf6eef6..869403b4 100644 --- a/__test__/common/userData/KeyValueUserDataRepository.test.ts +++ b/__test__/common/userData/KeyValueUserDataRepository.test.ts @@ -1,4 +1,4 @@ -import KeyValueUserDataRepository from "../../../src/common/userData/KeyValueUserDataRepository" +import { KeyValueUserDataRepository } from "../../../src/common" test("It reads the expected key", async () => { let readKey: string | undefined diff --git a/__test__/common/utils/ZodJSONCoder.test.ts b/__test__/common/utils/ZodJSONCoder.test.ts index 2958b80e..6335bbd9 100644 --- a/__test__/common/utils/ZodJSONCoder.test.ts +++ b/__test__/common/utils/ZodJSONCoder.test.ts @@ -1,5 +1,5 @@ import { z } from "zod" -import ZodJSONCoder from "../../../src/common/utils/ZodJSONCoder" +import { ZodJSONCoder } from "../../../src/common" const SampleAuthTokenSchema = z.object({ accessToken: z.string(), diff --git a/__test__/hooks/ExistingCommentCheckingPullRequestEventHandler.test.ts b/__test__/hooks/ExistingCommentCheckingPullRequestEventHandler.test.ts index 69c90250..ff56d18f 100644 --- a/__test__/hooks/ExistingCommentCheckingPullRequestEventHandler.test.ts +++ b/__test__/hooks/ExistingCommentCheckingPullRequestEventHandler.test.ts @@ -1,4 +1,4 @@ -import ExistingCommentCheckingPullRequestEventHandler from "../../src/features/hooks/domain/ExistingCommentCheckingPullRequestEventHandler" +import { ExistingCommentCheckingPullRequestEventHandler } from "../../src/features/hooks/domain" test("It fetches comments from the repository", async () => { let didFetchComments = false diff --git a/__test__/hooks/GitHubCommentFactory.test.ts b/__test__/hooks/GitHubCommentFactory.test.ts index 8347ab03..4857c717 100644 --- a/__test__/hooks/GitHubCommentFactory.test.ts +++ b/__test__/hooks/GitHubCommentFactory.test.ts @@ -1,4 +1,4 @@ -import GitHubCommentFactory from "../../src/features/hooks/domain/GitHubCommentFactory" +import { GitHubCommentFactory } from "../../src/features/hooks/domain" test("It includes a link to the documentation", async () => { const text = GitHubCommentFactory.makeDocumentationPreviewReadyComment( diff --git a/__test__/hooks/PostCommentPullRequestEventHandler.test.ts b/__test__/hooks/PostCommentPullRequestEventHandler.test.ts index 5bc4fb3c..82fb91f4 100644 --- a/__test__/hooks/PostCommentPullRequestEventHandler.test.ts +++ b/__test__/hooks/PostCommentPullRequestEventHandler.test.ts @@ -1,4 +1,4 @@ -import PostCommentPullRequestEventHandler from "../../src/features/hooks/domain/PostCommentPullRequestEventHandler" +import { PostCommentPullRequestEventHandler } from "../../src/features/hooks/domain" test("It adds a comment to the repository", async () => { let didAddComment = false diff --git a/__test__/hooks/RepositoryNameCheckingPullRequestEventHandler.test.ts b/__test__/hooks/RepositoryNameCheckingPullRequestEventHandler.test.ts index 25aab3f7..bfb42db3 100644 --- a/__test__/hooks/RepositoryNameCheckingPullRequestEventHandler.test.ts +++ b/__test__/hooks/RepositoryNameCheckingPullRequestEventHandler.test.ts @@ -1,4 +1,4 @@ -import RepositoryNameCheckingPullRequestEventHandler from "../../src/features/hooks/domain/RepositoryNameCheckingPullRequestEventHandler" +import { RepositoryNameCheckingPullRequestEventHandler } from "../../src/features/hooks/domain" test("It does not call event handler when repository name does not have \"-openapi\" suffix", async () => { let didCallEventHandler = false diff --git a/__test__/projects/CachingProjectDataSource.test.ts b/__test__/projects/CachingProjectDataSource.test.ts index 1a0f2f76..4d39bb09 100644 --- a/__test__/projects/CachingProjectDataSource.test.ts +++ b/__test__/projects/CachingProjectDataSource.test.ts @@ -1,5 +1,5 @@ -import Project from "../../src/features/projects/domain/Project" -import CachingProjectDataSource from "../../src/features/projects/domain/CachingProjectDataSource" +import { Project } from "../../src/features/projects/domain" +import { CachingProjectDataSource } from "../../src/features/projects/domain" test("It caches projects read from the data source", async () => { const projects: Project[] = [{ diff --git a/__test__/projects/ProjectConfigParser.test.ts b/__test__/projects/ProjectConfigParser.test.ts index ba5c5fdc..4cb0ee0e 100644 --- a/__test__/projects/ProjectConfigParser.test.ts +++ b/__test__/projects/ProjectConfigParser.test.ts @@ -1,4 +1,4 @@ -import ProjectConfigParser from "../../src/features/projects/domain/ProjectConfigParser" +import { ProjectConfigParser } from "../../src/features/projects/domain" test("It parses an empty string", async () => { const sut = new ProjectConfigParser() diff --git a/__test__/projects/SessionValidatingProjectDataSource.test.ts b/__test__/projects/SessionValidatingProjectDataSource.test.ts index f0f96dc7..7050cb78 100644 --- a/__test__/projects/SessionValidatingProjectDataSource.test.ts +++ b/__test__/projects/SessionValidatingProjectDataSource.test.ts @@ -1,4 +1,4 @@ -import SessionValidatingProjectDataSource from "../../src/features/projects/domain/SessionValidatingProjectDataSource" +import { SessionValidatingProjectDataSource } from "../../src/features/projects/domain" test("It validates the session", async () => { let didValidateSession = false diff --git a/__test__/projects/getSelection.test.ts b/__test__/projects/getSelection.test.ts index 25c116de..b2b54c5f 100644 --- a/__test__/projects/getSelection.test.ts +++ b/__test__/projects/getSelection.test.ts @@ -1,4 +1,4 @@ -import getSelection from "../../src/features/projects/domain/getSelection" +import { getSelection } from "../../src/features/projects/domain" test("It selects the first project when there is only one project", () => { const sut = getSelection({ diff --git a/__test__/projects/projectNavigator.test.ts b/__test__/projects/projectNavigator.test.ts index a4f69daa..06c30ac1 100644 --- a/__test__/projects/projectNavigator.test.ts +++ b/__test__/projects/projectNavigator.test.ts @@ -1,4 +1,4 @@ -import projectNavigator from "../../src/features/projects/domain/projectNavigator" +import { projectNavigator } from "../../src/features/projects/domain" test("It navigates to the correct path", async () => { let pushedPath: string | undefined diff --git a/__test__/projects/updateWindowTitle.test.ts b/__test__/projects/updateWindowTitle.test.ts index 8fe93ef1..1f7692b2 100644 --- a/__test__/projects/updateWindowTitle.test.ts +++ b/__test__/projects/updateWindowTitle.test.ts @@ -1,4 +1,4 @@ -import updateWindowTitle from "../../src/features/projects/domain/updateWindowTitle" +import { updateWindowTitle } from "../../src/features/projects/domain" test("It uses default title when there is no selection", async () => { const store: { title: string } = { title: "" } diff --git a/src/app/[...slug]/page.tsx b/src/app/[...slug]/page.tsx index b0fabd5d..cfcb585e 100644 --- a/src/app/[...slug]/page.tsx +++ b/src/app/[...slug]/page.tsx @@ -1,4 +1,4 @@ -import { getProjectId, getSpecificationId, getVersionId } from "@/common/utils/url" +import { getProjectId, getSpecificationId, getVersionId } from "../../common" import SessionAccessTokenBarrier from "@/features/auth/view/SessionAccessTokenBarrier" import ProjectsPage from "@/features/projects/view/ProjectsPage" import { projectRepository } from "@/composition" diff --git a/src/app/api/user/projects/route.ts b/src/app/api/user/projects/route.ts index 799f13f6..64a96f68 100644 --- a/src/app/api/user/projects/route.ts +++ b/src/app/api/user/projects/route.ts @@ -1,6 +1,6 @@ import { NextResponse } from "next/server" import { projectDataSource } from "@/composition" -import { UnauthorizedError, InvalidSessionError } from "@/common/errors" +import { UnauthorizedError, InvalidSessionError } from "../../../../common" export async function GET() { try { diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 1760b426..ff68262e 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -3,8 +3,8 @@ import type { Metadata } from "next" import { UserProvider } from "@auth0/nextjs-auth0/client" import { config as fontAwesomeConfig } from "@fortawesome/fontawesome-svg-core" import { CssBaseline } from "@mui/material" -import ThemeRegistry from "@/common/theme/ThemeRegistry" -import ErrorHandler from "@/common/errors/client/ErrorHandler" +import { ThemeRegistry } from "../common" +import ErrorHandler from "../common/errors/client/ErrorHandler" import "@fortawesome/fontawesome-svg-core/styles.css" fontAwesomeConfig.autoAddCss = false diff --git a/src/common/errors/client/ErrorHandler.tsx b/src/common/errors/client/ErrorHandler.tsx index 20ba19a3..61260578 100644 --- a/src/common/errors/client/ErrorHandler.tsx +++ b/src/common/errors/client/ErrorHandler.tsx @@ -1,7 +1,7 @@ "use client" import { SWRConfig } from "swr" -import { FetcherError } from "@/common/utils/fetcher" +import { FetcherError } from "@/common" export default function ErrorHandler({ children diff --git a/src/common/github/index.ts b/src/common/github/index.ts index 86c418d6..da9c21f3 100644 --- a/src/common/github/index.ts +++ b/src/common/github/index.ts @@ -1,2 +1,3 @@ export { default as AccessTokenRefreshingGitHubClient } from "./AccessTokenRefreshingGitHubClient" export { default as GitHubClient } from "./GitHubClient" +export type { default as IGitHubClient } from "./IGitHubClient" diff --git a/src/common/index.ts b/src/common/index.ts index ca794e57..c7086817 100644 --- a/src/common/index.ts +++ b/src/common/index.ts @@ -1,5 +1,7 @@ +export * from "./errors" export * from "./github" export * from "./keyValueStore" export * from "./mutex" export * from "./session" export * from "./userData" +export * from "./utils" diff --git a/src/common/mutex/SessionMutexFactory.ts b/src/common/mutex/SessionMutexFactory.ts index 72554464..c84b02de 100644 --- a/src/common/mutex/SessionMutexFactory.ts +++ b/src/common/mutex/SessionMutexFactory.ts @@ -1,7 +1,7 @@ import IKeyedMutexFactory from "./IKeyedMutexFactory" import IMutex from "./IMutex" import IMutexFactory from "./IMutexFactory" -import ISession from "@/common/session/ISession" +import { ISession } from "@/common" export default class SessionMutexFactory implements IMutexFactory { private readonly mutexFactory: IKeyedMutexFactory diff --git a/src/common/mutex/index.ts b/src/common/mutex/index.ts index 139d3e2b..d787ed40 100644 --- a/src/common/mutex/index.ts +++ b/src/common/mutex/index.ts @@ -1,2 +1,3 @@ +export type { default as IMutexFactory } from "./IMutexFactory" export { default as RedisKeyedMutexFactory } from "./RedisKeyedMutexFactory" -export { default as SessionMutexFactory } from "./SessionMutexFactory" +export { default as SessionMutexFactory } from "./SessionMutexFactory" \ No newline at end of file diff --git a/src/common/session/Auth0Session.ts b/src/common/session/Auth0Session.ts index b675447b..10c9e02c 100644 --- a/src/common/session/Auth0Session.ts +++ b/src/common/session/Auth0Session.ts @@ -1,5 +1,5 @@ import { getSession } from "@auth0/nextjs-auth0" -import { UnauthorizedError } from "@/common/errors" +import { UnauthorizedError } from "../../common" import ISession from "./ISession" import IIsUserGuestReader from "@/features/auth/domain/userIdentityProvider/IsUserGuestReader" diff --git a/src/common/session/index.ts b/src/common/session/index.ts index 5831d41b..8d21bb27 100644 --- a/src/common/session/index.ts +++ b/src/common/session/index.ts @@ -1,4 +1,6 @@ export { default as AlwaysValidSessionValidator } from "./AlwaysValidSessionValidator" export { default as Auth0Session } from "./Auth0Session" export { default as GitHubOrganizationSessionValidator } from "./GitHubOrganizationSessionValidator" +export type { default as ISession } from "./ISession" +export type { default as ISessionValidator } from "./ISessionValidator" export { default as SessionValidator } from "./SessionValidator" diff --git a/src/common/userData/index.ts b/src/common/userData/index.ts index 78c446f8..9d4d35fc 100644 --- a/src/common/userData/index.ts +++ b/src/common/userData/index.ts @@ -1 +1,2 @@ +export type { default as IUserDataRepository } from "./IUserDataRepository" export { default as KeyValueUserDataRepository } from "./KeyValueUserDataRepository" diff --git a/src/common/utils/index.ts b/src/common/utils/index.ts new file mode 100644 index 00000000..ff2de037 --- /dev/null +++ b/src/common/utils/index.ts @@ -0,0 +1,4 @@ +export * from "./fetcher" +export { default as fetcher } from "./fetcher" +export * from "./url" +export { default as ZodJSONCoder } from "./ZodJSONCoder" \ No newline at end of file diff --git a/src/features/auth/data/Auth0RefreshTokenReader.ts b/src/features/auth/data/Auth0RefreshTokenReader.ts index 642aebc2..cafe3edc 100644 --- a/src/features/auth/data/Auth0RefreshTokenReader.ts +++ b/src/features/auth/data/Auth0RefreshTokenReader.ts @@ -1,5 +1,5 @@ import { ManagementClient } from "auth0" -import { UnauthorizedError } from "@/common/errors" +import { UnauthorizedError } from "@/common" interface Auth0RefreshTokenReaderConfig { readonly domain: string diff --git a/src/features/auth/data/Auth0UserIdentityProviderReader.ts b/src/features/auth/data/Auth0UserIdentityProviderReader.ts index 3c17f218..dfd56b98 100644 --- a/src/features/auth/data/Auth0UserIdentityProviderReader.ts +++ b/src/features/auth/data/Auth0UserIdentityProviderReader.ts @@ -1,7 +1,7 @@ import { ManagementClient } from "auth0" import UserIdentityProvider from "../domain/userIdentityProvider/UserIdentityProvider" import IUserIdentityProviderReader from "../domain/userIdentityProvider/IUserIdentityProviderReader" -import { UnauthorizedError } from "@/common/errors" +import { UnauthorizedError } from "@/common" interface Auth0UserIdentityProviderReaderConfig { readonly domain: string diff --git a/src/features/auth/data/GitHubOAuthTokenRefresher.ts b/src/features/auth/data/GitHubOAuthTokenRefresher.ts index 255e8d13..6683a14f 100644 --- a/src/features/auth/data/GitHubOAuthTokenRefresher.ts +++ b/src/features/auth/data/GitHubOAuthTokenRefresher.ts @@ -1,4 +1,4 @@ -import { UnauthorizedError } from "@/common/errors" +import { UnauthorizedError } from "@/common" import OAuthToken from "../domain/oAuthToken/OAuthToken" import IOAuthTokenRefresher from "../domain/oAuthToken/IOAuthTokenRefresher" diff --git a/src/features/auth/domain/accessToken/LockingAccessTokenService.ts b/src/features/auth/domain/accessToken/LockingAccessTokenService.ts index cda75ecf..5717bdcc 100644 --- a/src/features/auth/domain/accessToken/LockingAccessTokenService.ts +++ b/src/features/auth/domain/accessToken/LockingAccessTokenService.ts @@ -1,4 +1,4 @@ -import IMutexFactory from "@/common/mutex/IMutexFactory" +import { IMutexFactory } from "@/common" import IAccessTokenService from "./IAccessTokenService" import withMutex from "../../../../common/mutex/withMutex" diff --git a/src/features/auth/domain/oAuthToken/OAuthTokenRepository.ts b/src/features/auth/domain/oAuthToken/OAuthTokenRepository.ts index a28bc29a..2a11b700 100644 --- a/src/features/auth/domain/oAuthToken/OAuthTokenRepository.ts +++ b/src/features/auth/domain/oAuthToken/OAuthTokenRepository.ts @@ -1,6 +1,4 @@ -import ZodJSONCoder from "../../../../common/utils/ZodJSONCoder" -import IUserDataRepository from "@/common/userData/IUserDataRepository" -import { UnauthorizedError } from "../../../../common/errors" +import { IUserDataRepository, UnauthorizedError, ZodJSONCoder } from "../../../../common" import IOAuthTokenRepository from "./IOAuthTokenRepository" import OAuthToken, { OAuthTokenSchema } from "./OAuthToken" diff --git a/src/features/auth/domain/oAuthToken/index.ts b/src/features/auth/domain/oAuthToken/index.ts index 7d79ca92..af000370 100644 --- a/src/features/auth/domain/oAuthToken/index.ts +++ b/src/features/auth/domain/oAuthToken/index.ts @@ -1 +1,2 @@ +export type { default as OAuthToken } from "./OAuthToken" export { default as OAuthTokenRepository } from "./OAuthTokenRepository" diff --git a/src/features/auth/domain/repositoryAccess/CachingRepositoryAccessReaderConfig.ts b/src/features/auth/domain/repositoryAccess/CachingRepositoryAccessReaderConfig.ts index 11574d07..15e4a717 100644 --- a/src/features/auth/domain/repositoryAccess/CachingRepositoryAccessReaderConfig.ts +++ b/src/features/auth/domain/repositoryAccess/CachingRepositoryAccessReaderConfig.ts @@ -1,6 +1,5 @@ import { z } from "zod" -import ZodJSONCoder from "../../../../common/utils/ZodJSONCoder" -import IUserDataRepository from "@/common/userData/IUserDataRepository" +import { ZodJSONCoder, IUserDataRepository } from "../../../../common" export const RepositoryNamesContainerSchema = z.string().array() diff --git a/src/features/auth/domain/userIdentityProvider/CachingUserIdentityProviderReader.ts b/src/features/auth/domain/userIdentityProvider/CachingUserIdentityProviderReader.ts index 29f78760..9d1fff8c 100644 --- a/src/features/auth/domain/userIdentityProvider/CachingUserIdentityProviderReader.ts +++ b/src/features/auth/domain/userIdentityProvider/CachingUserIdentityProviderReader.ts @@ -1,4 +1,4 @@ -import IUserDataRepository from "@/common/userData/IUserDataRepository" +import { IUserDataRepository } from "@/common" import IUserIdentityProviderReader from "./IUserIdentityProviderReader" import UserIdentityProvider from "./UserIdentityProvider" diff --git a/src/features/auth/domain/userIdentityProvider/index.ts b/src/features/auth/domain/userIdentityProvider/index.ts index a94ce5d2..74b56b57 100644 --- a/src/features/auth/domain/userIdentityProvider/index.ts +++ b/src/features/auth/domain/userIdentityProvider/index.ts @@ -1,2 +1,3 @@ export { default as CachingUserIdentityProviderReader } from "./CachingUserIdentityProviderReader" export { default as IsUserGuestReader } from "./IsUserGuestReader" +export { default as UserIdentityProvider } from "./UserIdentityProvider" diff --git a/src/features/hooks/data/GitHubPullRequestCommentRepository.ts b/src/features/hooks/data/GitHubPullRequestCommentRepository.ts index 59b60d62..3e074d59 100644 --- a/src/features/hooks/data/GitHubPullRequestCommentRepository.ts +++ b/src/features/hooks/data/GitHubPullRequestCommentRepository.ts @@ -1,4 +1,4 @@ -import IGitHubClient from "@/common/github/IGitHubClient" +import { IGitHubClient } from "@/common" import IPullRequestCommentRepository, { GetPullRequestCommentsOperation, AddPullRequestCommentOperation, diff --git a/src/features/hooks/domain/index.ts b/src/features/hooks/domain/index.ts new file mode 100644 index 00000000..4e205a7c --- /dev/null +++ b/src/features/hooks/domain/index.ts @@ -0,0 +1,4 @@ +export { default as ExistingCommentCheckingPullRequestEventHandler } from "./ExistingCommentCheckingPullRequestEventHandler" +export { default as GitHubCommentFactory } from "./GitHubCommentFactory" +export { default as PostCommentPullRequestEventHandler } from "./PostCommentPullRequestEventHandler" +export { default as RepositoryNameCheckingPullRequestEventHandler } from "./RepositoryNameCheckingPullRequestEventHandler" diff --git a/src/features/projects/data/GitHubProjectDataSource.ts b/src/features/projects/data/GitHubProjectDataSource.ts index 60db94d4..40244891 100644 --- a/src/features/projects/data/GitHubProjectDataSource.ts +++ b/src/features/projects/data/GitHubProjectDataSource.ts @@ -1,4 +1,4 @@ -import IGitHubClient from "@/common/github/IGitHubClient" +import { IGitHubClient } from "@/common" import Project from "../domain/Project" import IProjectConfig from "../domain/IProjectConfig" import IProjectDataSource from "../domain/IProjectDataSource" diff --git a/src/features/projects/data/useProjects.ts b/src/features/projects/data/useProjects.ts index 5e1455cb..fe430389 100644 --- a/src/features/projects/data/useProjects.ts +++ b/src/features/projects/data/useProjects.ts @@ -1,5 +1,5 @@ import useSWR from "swr" -import fetcher from "@/common/utils/fetcher" +import { fetcher } from "@/common" import Project from "../domain/Project" type ProjectContainer = { projects: Project[] } diff --git a/src/features/projects/domain/ProjectRepository.ts b/src/features/projects/domain/ProjectRepository.ts index 356c4a53..45a494d2 100644 --- a/src/features/projects/domain/ProjectRepository.ts +++ b/src/features/projects/domain/ProjectRepository.ts @@ -1,22 +1,24 @@ -import ZodJSONCoder from "@/common/utils/ZodJSONCoder" -import ISession from "@/common/session/ISession" -import IUserDataRepository from "@/common/userData/IUserDataRepository" +import { IUserDataRepository, ZodJSONCoder } from "../../../common" import IProjectRepository from "./IProjectRepository" import Project, { ProjectSchema } from "./Project" +interface IUserIDReader { + getUserId(): Promise +} + type Repository = IUserDataRepository export default class ProjectRepository implements IProjectRepository { - private readonly session: ISession + private readonly userIDReader: IUserIDReader private readonly repository: Repository - constructor(session: ISession, repository: Repository) { - this.session = session + constructor(userIDReader: IUserIDReader, repository: Repository) { + this.userIDReader = userIDReader this.repository = repository } async get(): Promise { - const userId = await this.session.getUserId() + const userId = await this.userIDReader.getUserId() const string = await this.repository.get(userId) if (!string) { return undefined @@ -25,13 +27,13 @@ export default class ProjectRepository implements IProjectRepository { } async set(projects: Project[]): Promise { - const userId = await this.session.getUserId() + const userId = await this.userIDReader.getUserId() const string = ZodJSONCoder.encode(ProjectSchema.array(), projects) await this.repository.set(userId, string) } async delete(): Promise { - const userId = await this.session.getUserId() + const userId = await this.userIDReader.getUserId() await this.repository.delete(userId) } } diff --git a/src/features/projects/domain/SessionValidatingProjectDataSource.ts b/src/features/projects/domain/SessionValidatingProjectDataSource.ts index 942d0859..b7f82b3f 100644 --- a/src/features/projects/domain/SessionValidatingProjectDataSource.ts +++ b/src/features/projects/domain/SessionValidatingProjectDataSource.ts @@ -1,5 +1,4 @@ -import { InvalidSessionError } from "../../../common/errors" -import ISessionValidator from "@/common/session/ISessionValidator" +import { ISessionValidator, InvalidSessionError } from "../../../common" import IProjectDataSource from "../domain/IProjectDataSource" import Project from "../domain/Project" diff --git a/src/features/projects/domain/index.ts b/src/features/projects/domain/index.ts index c74d9975..d6ff6211 100644 --- a/src/features/projects/domain/index.ts +++ b/src/features/projects/domain/index.ts @@ -1,4 +1,9 @@ export { default as CachingProjectDataSource } from "./CachingProjectDataSource" export { default as ForgivingProjectDataSource } from "./ForgivingProjectDataSource" +export { default as getSelection } from "./getSelection" +export type { default as Project } from "./Project" +export { default as ProjectConfigParser } from "./ProjectConfigParser" +export { default as projectNavigator } from "./projectNavigator" export { default as ProjectRepository } from "./ProjectRepository" export { default as SessionValidatingProjectDataSource } from "./SessionValidatingProjectDataSource" +export { default as updateWindowTitle } from "./updateWindowTitle" From 29f9144fb6e11aee3933338c19562ed6593f380a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=B8vring?= Date: Tue, 7 Nov 2023 22:40:46 +0100 Subject: [PATCH 26/42] Hides edit button when logged in as guest --- src/features/projects/view/ProjectsPage.tsx | 3 +++ src/features/projects/view/client/ProjectsPage.tsx | 3 +++ src/features/projects/view/toolbar/TrailingToolbarItem.tsx | 4 +++- 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/features/projects/view/ProjectsPage.tsx b/src/features/projects/view/ProjectsPage.tsx index d01941f6..231525c7 100644 --- a/src/features/projects/view/ProjectsPage.tsx +++ b/src/features/projects/view/ProjectsPage.tsx @@ -1,3 +1,4 @@ +import { session } from "@/composition" import { ProjectRepository } from "../domain" import ClientProjectsPage from "./client/ProjectsPage" @@ -12,9 +13,11 @@ export default async function ProjectsPage({ versionId?: string specificationId?: string }) { + const isGuest = await session.getIsGuest() const projects = await projectRepository.get() return ( - {specification.editURL && + {showEditButton && specification.editURL && Date: Tue, 7 Nov 2023 22:45:14 +0100 Subject: [PATCH 27/42] Disables link to repository ofr guests --- src/features/projects/view/ProjectsPage.tsx | 2 +- src/features/projects/view/client/ProjectsPage.tsx | 6 +++--- .../projects/view/toolbar/TrailingToolbarItem.tsx | 12 ++++++++---- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/features/projects/view/ProjectsPage.tsx b/src/features/projects/view/ProjectsPage.tsx index 231525c7..5e5118d4 100644 --- a/src/features/projects/view/ProjectsPage.tsx +++ b/src/features/projects/view/ProjectsPage.tsx @@ -17,7 +17,7 @@ export default async function ProjectsPage({ const projects = await projectRepository.get() return ( - + / - {showEditButton && specification.editURL && + {enableGitHubLinks && specification.editURL && Date: Tue, 7 Nov 2023 22:45:19 +0100 Subject: [PATCH 28/42] Fixes margin --- src/features/projects/view/toolbar/TrailingToolbarItem.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/features/projects/view/toolbar/TrailingToolbarItem.tsx b/src/features/projects/view/toolbar/TrailingToolbarItem.tsx index a35ffd24..18a4ffe1 100644 --- a/src/features/projects/view/toolbar/TrailingToolbarItem.tsx +++ b/src/features/projects/view/toolbar/TrailingToolbarItem.tsx @@ -95,7 +95,7 @@ const ProjectName = ({ ) } else { return ( - + {text} ) From 8986a8e5c0c9b91514d72097d6d94d94d4bcdda7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=B8vring?= Date: Wed, 8 Nov 2023 07:36:30 +0100 Subject: [PATCH 29/42] Renames SessionAccessTokenService to AccessTokenService --- ...TokenService.test.ts => AccessTokenService.test.ts} | 10 +++++----- src/composition.ts | 4 ++-- ...sionAccessTokenService.ts => AccessTokenService.ts} | 6 +++--- src/features/auth/domain/accessToken/index.ts | 2 +- 4 files changed, 11 insertions(+), 11 deletions(-) rename __test__/auth/{SessionAccessTokenService.test.ts => AccessTokenService.test.ts} (91%) rename src/features/auth/domain/accessToken/{SessionAccessTokenService.ts => AccessTokenService.ts} (86%) diff --git a/__test__/auth/SessionAccessTokenService.test.ts b/__test__/auth/AccessTokenService.test.ts similarity index 91% rename from __test__/auth/SessionAccessTokenService.test.ts rename to __test__/auth/AccessTokenService.test.ts index 14920da2..9c0024c5 100644 --- a/__test__/auth/SessionAccessTokenService.test.ts +++ b/__test__/auth/AccessTokenService.test.ts @@ -1,8 +1,8 @@ -import { SessionAccessTokenService } from "../../src/features/auth/domain" +import { AccessTokenService } from "../../src/features/auth/domain" test("It reads the access token for a guest user", async () => { let didReadAccessToken = false - const sut = new SessionAccessTokenService({ + const sut = new AccessTokenService({ isGuestReader: { async getIsGuest() { return true @@ -32,7 +32,7 @@ test("It reads the access token for a guest user", async () => { test("It refreshes the access token for a guest user", async () => { let didRefreshAccessToken = false - const sut = new SessionAccessTokenService({ + const sut = new AccessTokenService({ isGuestReader: { async getIsGuest() { return true @@ -62,7 +62,7 @@ test("It refreshes the access token for a guest user", async () => { test("It reads the access token for a host user", async () => { let didReadAccessToken = false - const sut = new SessionAccessTokenService({ + const sut = new AccessTokenService({ isGuestReader: { async getIsGuest() { return false @@ -92,7 +92,7 @@ test("It reads the access token for a host user", async () => { test("It refreshes the access token for a host user", async () => { let didRefreshAccessToken = false - const sut = new SessionAccessTokenService({ + const sut = new AccessTokenService({ isGuestReader: { async getIsGuest() { return false diff --git a/src/composition.ts b/src/composition.ts index b5d54e3d..1cf69ef1 100644 --- a/src/composition.ts +++ b/src/composition.ts @@ -28,6 +28,7 @@ import { Auth0UserIdentityProviderReader } from "@/features/auth/data" import { + AccessTokenService, CachingRepositoryAccessReaderConfig, CachingUserIdentityProviderReader, CompositeLogInHandler, @@ -44,7 +45,6 @@ import { OnlyStaleRefreshingAccessTokenService, RemoveInvitedFlagLogInHandler, RepositoryRestrictingAccessTokenDataSource, - SessionAccessTokenService, UserDataCleanUpLogOutHandler } from "@/features/auth/domain" @@ -131,7 +131,7 @@ export const accessTokenService = new LockingAccessTokenService( "mutexAccessToken" ), new OnlyStaleRefreshingAccessTokenService( - new SessionAccessTokenService({ + new AccessTokenService({ isGuestReader: session, guestAccessTokenService: new GuestAccessTokenService({ userIdReader: session, diff --git a/src/features/auth/domain/accessToken/SessionAccessTokenService.ts b/src/features/auth/domain/accessToken/AccessTokenService.ts similarity index 86% rename from src/features/auth/domain/accessToken/SessionAccessTokenService.ts rename to src/features/auth/domain/accessToken/AccessTokenService.ts index e562520d..095aef31 100644 --- a/src/features/auth/domain/accessToken/SessionAccessTokenService.ts +++ b/src/features/auth/domain/accessToken/AccessTokenService.ts @@ -4,18 +4,18 @@ export interface IIsGuestReader { getIsGuest(): Promise } -interface SessionAccessTokenServiceConfig { +interface AccessTokenServiceConfig { readonly isGuestReader: IIsGuestReader readonly guestAccessTokenService: IAccessTokenService readonly hostAccessTokenService: IAccessTokenService } -export default class SessionAccessTokenService implements IAccessTokenService { +export default class AccessTokenService implements IAccessTokenService { private readonly isGuestReader: IIsGuestReader private readonly guestAccessTokenService: IAccessTokenService private readonly hostAccessTokenService: IAccessTokenService - constructor(config: SessionAccessTokenServiceConfig) { + constructor(config: AccessTokenServiceConfig) { this.isGuestReader = config.isGuestReader this.guestAccessTokenService = config.guestAccessTokenService this.hostAccessTokenService = config.hostAccessTokenService diff --git a/src/features/auth/domain/accessToken/index.ts b/src/features/auth/domain/accessToken/index.ts index 814fdf3d..fd3cb104 100644 --- a/src/features/auth/domain/accessToken/index.ts +++ b/src/features/auth/domain/accessToken/index.ts @@ -2,4 +2,4 @@ export { default as GuestAccessTokenService } from "./GuestAccessTokenService" export { default as HostAccessTokenService } from "./HostAccessTokenService" export { default as LockingAccessTokenService } from "./LockingAccessTokenService" export { default as OnlyStaleRefreshingAccessTokenService } from "./OnlyStaleRefreshingAccessTokenService" -export { default as SessionAccessTokenService } from "./SessionAccessTokenService" +export { default as AccessTokenService } from "./AccessTokenService" From 3d531429d5e845e26d9be72797c6d8c0123792df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=B8vring?= Date: Wed, 8 Nov 2023 08:01:38 +0100 Subject: [PATCH 30/42] Clean up imports --- src/app/layout.tsx | 4 ++-- src/common/errors/client/ErrorHandler.tsx | 2 +- src/common/mutex/SessionMutexFactory.ts | 2 +- .../domain/oAuthToken/OAuthTokenRepository.ts | 4 +--- .../data/GitHubPullRequestCommentRepository.ts | 2 +- .../projects/data/GitHubProjectDataSource.ts | 2 +- .../projects/domain/ProjectRepository.ts | 18 +++++++++++------- 7 files changed, 18 insertions(+), 16 deletions(-) diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 1760b426..48ad9fee 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -3,8 +3,8 @@ import type { Metadata } from "next" import { UserProvider } from "@auth0/nextjs-auth0/client" import { config as fontAwesomeConfig } from "@fortawesome/fontawesome-svg-core" import { CssBaseline } from "@mui/material" -import ThemeRegistry from "@/common/theme/ThemeRegistry" -import ErrorHandler from "@/common/errors/client/ErrorHandler" +import ThemeRegistry from "../common/theme/ThemeRegistry" +import ErrorHandler from "../common/errors/client/ErrorHandler" import "@fortawesome/fontawesome-svg-core/styles.css" fontAwesomeConfig.autoAddCss = false diff --git a/src/common/errors/client/ErrorHandler.tsx b/src/common/errors/client/ErrorHandler.tsx index 20ba19a3..61260578 100644 --- a/src/common/errors/client/ErrorHandler.tsx +++ b/src/common/errors/client/ErrorHandler.tsx @@ -1,7 +1,7 @@ "use client" import { SWRConfig } from "swr" -import { FetcherError } from "@/common/utils/fetcher" +import { FetcherError } from "@/common" export default function ErrorHandler({ children diff --git a/src/common/mutex/SessionMutexFactory.ts b/src/common/mutex/SessionMutexFactory.ts index 72554464..c84b02de 100644 --- a/src/common/mutex/SessionMutexFactory.ts +++ b/src/common/mutex/SessionMutexFactory.ts @@ -1,7 +1,7 @@ import IKeyedMutexFactory from "./IKeyedMutexFactory" import IMutex from "./IMutex" import IMutexFactory from "./IMutexFactory" -import ISession from "@/common/session/ISession" +import { ISession } from "@/common" export default class SessionMutexFactory implements IMutexFactory { private readonly mutexFactory: IKeyedMutexFactory diff --git a/src/features/auth/domain/oAuthToken/OAuthTokenRepository.ts b/src/features/auth/domain/oAuthToken/OAuthTokenRepository.ts index a28bc29a..2a11b700 100644 --- a/src/features/auth/domain/oAuthToken/OAuthTokenRepository.ts +++ b/src/features/auth/domain/oAuthToken/OAuthTokenRepository.ts @@ -1,6 +1,4 @@ -import ZodJSONCoder from "../../../../common/utils/ZodJSONCoder" -import IUserDataRepository from "@/common/userData/IUserDataRepository" -import { UnauthorizedError } from "../../../../common/errors" +import { IUserDataRepository, UnauthorizedError, ZodJSONCoder } from "../../../../common" import IOAuthTokenRepository from "./IOAuthTokenRepository" import OAuthToken, { OAuthTokenSchema } from "./OAuthToken" diff --git a/src/features/hooks/data/GitHubPullRequestCommentRepository.ts b/src/features/hooks/data/GitHubPullRequestCommentRepository.ts index 59b60d62..3e074d59 100644 --- a/src/features/hooks/data/GitHubPullRequestCommentRepository.ts +++ b/src/features/hooks/data/GitHubPullRequestCommentRepository.ts @@ -1,4 +1,4 @@ -import IGitHubClient from "@/common/github/IGitHubClient" +import { IGitHubClient } from "@/common" import IPullRequestCommentRepository, { GetPullRequestCommentsOperation, AddPullRequestCommentOperation, diff --git a/src/features/projects/data/GitHubProjectDataSource.ts b/src/features/projects/data/GitHubProjectDataSource.ts index a66a6f27..f001549e 100644 --- a/src/features/projects/data/GitHubProjectDataSource.ts +++ b/src/features/projects/data/GitHubProjectDataSource.ts @@ -1,4 +1,4 @@ -import IGitHubClient from "@/common/github/IGitHubClient" +import { IGitHubClient } from "@/common" import { Project, IProjectConfig, diff --git a/src/features/projects/domain/ProjectRepository.ts b/src/features/projects/domain/ProjectRepository.ts index 1fb96b0c..45a494d2 100644 --- a/src/features/projects/domain/ProjectRepository.ts +++ b/src/features/projects/domain/ProjectRepository.ts @@ -1,20 +1,24 @@ -import { ZodJSONCoder, ISession, IUserDataRepository } from "../../../common" +import { IUserDataRepository, ZodJSONCoder } from "../../../common" import IProjectRepository from "./IProjectRepository" import Project, { ProjectSchema } from "./Project" +interface IUserIDReader { + getUserId(): Promise +} + type Repository = IUserDataRepository export default class ProjectRepository implements IProjectRepository { - private readonly session: ISession + private readonly userIDReader: IUserIDReader private readonly repository: Repository - constructor(session: ISession, repository: Repository) { - this.session = session + constructor(userIDReader: IUserIDReader, repository: Repository) { + this.userIDReader = userIDReader this.repository = repository } async get(): Promise { - const userId = await this.session.getUserId() + const userId = await this.userIDReader.getUserId() const string = await this.repository.get(userId) if (!string) { return undefined @@ -23,13 +27,13 @@ export default class ProjectRepository implements IProjectRepository { } async set(projects: Project[]): Promise { - const userId = await this.session.getUserId() + const userId = await this.userIDReader.getUserId() const string = ZodJSONCoder.encode(ProjectSchema.array(), projects) await this.repository.set(userId, string) } async delete(): Promise { - const userId = await this.session.getUserId() + const userId = await this.userIDReader.getUserId() await this.repository.delete(userId) } } From 9436fb95434e2bf65ae2a116b644f018a4824168 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=B8vring?= Date: Wed, 8 Nov 2023 08:03:07 +0100 Subject: [PATCH 31/42] Logs authentication errors --- src/app/api/auth/[auth0]/route.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/app/api/auth/[auth0]/route.ts b/src/app/api/auth/[auth0]/route.ts index aeaf8b83..2023b606 100644 --- a/src/app/api/auth/[auth0]/route.ts +++ b/src/app/api/auth/[auth0]/route.ts @@ -16,7 +16,8 @@ const afterCallback: AfterCallbackAppRoute = async (_req, session) => { return session } -const onError: AppRouterOnError = async () => { +const onError: AppRouterOnError = async (_req, error) => { + console.error(error) const url = new URL(SHAPE_DOCS_BASE_URL + "/api/auth/forceLogout") return NextResponse.redirect(url) } From 11d6c1f0e45f8f60098d203ea0310021c4938a1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=B8vring?= Date: Wed, 8 Nov 2023 09:31:14 +0100 Subject: [PATCH 32/42] Removes logic resetting has_pending_invitation --- __test__/auth/CompositeLogInHandler.test.ts | 24 ------------- .../RemoveInvitedFlagLogInHandler.test.ts | 18 ---------- src/composition.ts | 36 ++++++++----------- .../auth/data/Auth0MetadataUpdater.ts | 28 --------------- src/features/auth/data/index.ts | 1 - .../domain/logIn/CompositeLogInHandler.ts | 14 -------- .../logIn/RemoveInvitedFlagLogInHandler.ts | 20 ----------- src/features/auth/domain/logIn/index.ts | 2 -- 8 files changed, 14 insertions(+), 129 deletions(-) delete mode 100644 __test__/auth/CompositeLogInHandler.test.ts delete mode 100644 __test__/auth/RemoveInvitedFlagLogInHandler.test.ts delete mode 100644 src/features/auth/data/Auth0MetadataUpdater.ts delete mode 100644 src/features/auth/domain/logIn/CompositeLogInHandler.ts delete mode 100644 src/features/auth/domain/logIn/RemoveInvitedFlagLogInHandler.ts diff --git a/__test__/auth/CompositeLogInHandler.test.ts b/__test__/auth/CompositeLogInHandler.test.ts deleted file mode 100644 index c342ffbb..00000000 --- a/__test__/auth/CompositeLogInHandler.test.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { CompositeLogInHandler } from "../../src/features/auth/domain" - -test("It invokes all log in handlers for user", async () => { - let userId1: string | undefined - let userId2: string | undefined - let userId3: string | undefined - const sut = new CompositeLogInHandler([{ - async handleLogIn(userId) { - userId1 = userId - } - }, { - async handleLogIn(userId) { - userId2 = userId - } - }, { - async handleLogIn(userId) { - userId3 = userId - } - }]) - await sut.handleLogIn("1234") - expect(userId1).toEqual("1234") - expect(userId2).toEqual("1234") - expect(userId3).toEqual("1234") -}) diff --git a/__test__/auth/RemoveInvitedFlagLogInHandler.test.ts b/__test__/auth/RemoveInvitedFlagLogInHandler.test.ts deleted file mode 100644 index e4a276cd..00000000 --- a/__test__/auth/RemoveInvitedFlagLogInHandler.test.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { RemoveInvitedFlagLogInHandler } from "../../src/features/auth/domain" - -test("It removes invited flag from specified user", async () => { - let updatedUserId: string | undefined - /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ - let updatedMetadata: {[key: string]: any} | undefined - const sut = new RemoveInvitedFlagLogInHandler({ - async updateMetadata(userId, metadata) { - updatedUserId = userId - updatedMetadata = metadata - } - }) - await sut.handleLogIn("1234") - expect(updatedUserId).toEqual("1234") - expect(updatedMetadata).toEqual({ - has_pending_invitation: false - }) -}) diff --git a/src/composition.ts b/src/composition.ts index 46dce15b..b5432314 100644 --- a/src/composition.ts +++ b/src/composition.ts @@ -22,7 +22,6 @@ import { import { GitHubOAuthTokenRefresher, GitHubInstallationAccessTokenDataSource, - Auth0MetadataUpdater, Auth0RefreshTokenReader, Auth0RepositoryAccessReader, Auth0UserIdentityProviderReader @@ -31,7 +30,6 @@ import { AccessTokenService, CachingRepositoryAccessReaderConfig, CachingUserIdentityProviderReader, - CompositeLogInHandler, CompositeLogOutHandler, CredentialsTransferringLogInHandler, ErrorIgnoringLogOutHandler, @@ -43,7 +41,6 @@ import { LockingAccessTokenService, OAuthTokenRepository, OnlyStaleRefreshingAccessTokenService, - RemoveInvitedFlagLogInHandler, RepositoryRestrictingAccessTokenDataSource, UserDataCleanUpLogOutHandler } from "@/features/auth/domain" @@ -190,25 +187,20 @@ export const projectDataSource = new CachingProjectDataSource( projectRepository ) -export const logInHandler = new CompositeLogInHandler([ - new CredentialsTransferringLogInHandler({ - isUserGuestReader: new IsUserGuestReader( - userIdentityProviderReader - ), - guestCredentialsTransferrer: new NullObjectCredentialsTransferrer(), - hostCredentialsTransferrer: new HostCredentialsTransferrer({ - refreshTokenReader: new Auth0RefreshTokenReader({ - ...auth0ManagementCredentials, - connection: "github" - }), - oAuthTokenRefresher: gitHubOAuthTokenRefresher, - oAuthTokenRepository: oAuthTokenRepository - }) - }), - new RemoveInvitedFlagLogInHandler( - new Auth0MetadataUpdater({ ...auth0ManagementCredentials }) - ) -]) +export const logInHandler = new CredentialsTransferringLogInHandler({ + isUserGuestReader: new IsUserGuestReader( + userIdentityProviderReader + ), + guestCredentialsTransferrer: new NullObjectCredentialsTransferrer(), + hostCredentialsTransferrer: new HostCredentialsTransferrer({ + refreshTokenReader: new Auth0RefreshTokenReader({ + ...auth0ManagementCredentials, + connection: "github" + }), + oAuthTokenRefresher: gitHubOAuthTokenRefresher, + oAuthTokenRepository: oAuthTokenRepository + }) +}) export const logOutHandler = new ErrorIgnoringLogOutHandler( new CompositeLogOutHandler([ diff --git a/src/features/auth/data/Auth0MetadataUpdater.ts b/src/features/auth/data/Auth0MetadataUpdater.ts deleted file mode 100644 index 88ffb524..00000000 --- a/src/features/auth/data/Auth0MetadataUpdater.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { ManagementClient } from "auth0" - -type Auth0MetadataUpdaterConfig = { - readonly domain: string - readonly clientId: string - readonly clientSecret: string -} - -export default class Auth0MetadataUpdater { - private readonly managementClient: ManagementClient - - constructor(config: Auth0MetadataUpdaterConfig) { - this.managementClient = new ManagementClient({ - domain: config.domain, - clientId: config.clientId, - clientSecret: config.clientSecret - }) - } - - /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ - async updateMetadata(userId: string, metadata: {[key: string]: any}): Promise { - await this.managementClient.users.update({ - id: userId - }, { - app_metadata: metadata - }) - } -} diff --git a/src/features/auth/data/index.ts b/src/features/auth/data/index.ts index 8090466b..5db47a9f 100644 --- a/src/features/auth/data/index.ts +++ b/src/features/auth/data/index.ts @@ -1,4 +1,3 @@ -export { default as Auth0MetadataUpdater } from "./Auth0MetadataUpdater" export { default as Auth0RefreshTokenReader } from "./Auth0RefreshTokenReader" export { default as Auth0RepositoryAccessReader } from "./Auth0RepositoryAccessReader" export { default as Auth0UserIdentityProviderReader } from "./Auth0UserIdentityProviderReader" diff --git a/src/features/auth/domain/logIn/CompositeLogInHandler.ts b/src/features/auth/domain/logIn/CompositeLogInHandler.ts deleted file mode 100644 index 169be4c2..00000000 --- a/src/features/auth/domain/logIn/CompositeLogInHandler.ts +++ /dev/null @@ -1,14 +0,0 @@ -import ILogInHandler from "./ILogInHandler" - -export default class CompositeLogInHandler implements ILogInHandler { - private readonly handlers: ILogInHandler[] - - constructor(handlers: ILogInHandler[]) { - this.handlers = handlers - } - - async handleLogIn(userId: string): Promise { - const promises = this.handlers.map(e => e.handleLogIn(userId)) - await Promise.all(promises) - } -} diff --git a/src/features/auth/domain/logIn/RemoveInvitedFlagLogInHandler.ts b/src/features/auth/domain/logIn/RemoveInvitedFlagLogInHandler.ts deleted file mode 100644 index c1565e9d..00000000 --- a/src/features/auth/domain/logIn/RemoveInvitedFlagLogInHandler.ts +++ /dev/null @@ -1,20 +0,0 @@ -import ILogInHandler from "./ILogInHandler" - -export interface IMetadataUpdater { - /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ - updateMetadata(userId: string, metadata: {[key: string]: any}): Promise -} - -export default class RemoveInvitedFlagLogInHandler implements ILogInHandler { - private readonly metadataUpdater: IMetadataUpdater - - constructor(metadataUpdater: IMetadataUpdater) { - this.metadataUpdater = metadataUpdater - } - - async handleLogIn(userId: string): Promise { - await this.metadataUpdater.updateMetadata(userId, { - has_pending_invitation: false - }) - } -} diff --git a/src/features/auth/domain/logIn/index.ts b/src/features/auth/domain/logIn/index.ts index ec98a5e6..50aadb78 100644 --- a/src/features/auth/domain/logIn/index.ts +++ b/src/features/auth/domain/logIn/index.ts @@ -1,3 +1 @@ -export { default as CompositeLogInHandler } from "./CompositeLogInHandler" export { default as CredentialsTransferringLogInHandler } from "./CredentialsTransferringLogInHandler" -export { default as RemoveInvitedFlagLogInHandler } from "./RemoveInvitedFlagLogInHandler" From 53e8043132417a60ee610c262ec51aab40cfcc95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=B8vring?= Date: Wed, 8 Nov 2023 09:32:53 +0100 Subject: [PATCH 33/42] Revert "Removes logic resetting has_pending_invitation" This reverts commit 11d6c1f0e45f8f60098d203ea0310021c4938a1a. --- __test__/auth/CompositeLogInHandler.test.ts | 24 +++++++++++++ .../RemoveInvitedFlagLogInHandler.test.ts | 18 ++++++++++ src/composition.ts | 36 +++++++++++-------- .../auth/data/Auth0MetadataUpdater.ts | 28 +++++++++++++++ src/features/auth/data/index.ts | 1 + .../domain/logIn/CompositeLogInHandler.ts | 14 ++++++++ .../logIn/RemoveInvitedFlagLogInHandler.ts | 20 +++++++++++ src/features/auth/domain/logIn/index.ts | 2 ++ 8 files changed, 129 insertions(+), 14 deletions(-) create mode 100644 __test__/auth/CompositeLogInHandler.test.ts create mode 100644 __test__/auth/RemoveInvitedFlagLogInHandler.test.ts create mode 100644 src/features/auth/data/Auth0MetadataUpdater.ts create mode 100644 src/features/auth/domain/logIn/CompositeLogInHandler.ts create mode 100644 src/features/auth/domain/logIn/RemoveInvitedFlagLogInHandler.ts diff --git a/__test__/auth/CompositeLogInHandler.test.ts b/__test__/auth/CompositeLogInHandler.test.ts new file mode 100644 index 00000000..c342ffbb --- /dev/null +++ b/__test__/auth/CompositeLogInHandler.test.ts @@ -0,0 +1,24 @@ +import { CompositeLogInHandler } from "../../src/features/auth/domain" + +test("It invokes all log in handlers for user", async () => { + let userId1: string | undefined + let userId2: string | undefined + let userId3: string | undefined + const sut = new CompositeLogInHandler([{ + async handleLogIn(userId) { + userId1 = userId + } + }, { + async handleLogIn(userId) { + userId2 = userId + } + }, { + async handleLogIn(userId) { + userId3 = userId + } + }]) + await sut.handleLogIn("1234") + expect(userId1).toEqual("1234") + expect(userId2).toEqual("1234") + expect(userId3).toEqual("1234") +}) diff --git a/__test__/auth/RemoveInvitedFlagLogInHandler.test.ts b/__test__/auth/RemoveInvitedFlagLogInHandler.test.ts new file mode 100644 index 00000000..e4a276cd --- /dev/null +++ b/__test__/auth/RemoveInvitedFlagLogInHandler.test.ts @@ -0,0 +1,18 @@ +import { RemoveInvitedFlagLogInHandler } from "../../src/features/auth/domain" + +test("It removes invited flag from specified user", async () => { + let updatedUserId: string | undefined + /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ + let updatedMetadata: {[key: string]: any} | undefined + const sut = new RemoveInvitedFlagLogInHandler({ + async updateMetadata(userId, metadata) { + updatedUserId = userId + updatedMetadata = metadata + } + }) + await sut.handleLogIn("1234") + expect(updatedUserId).toEqual("1234") + expect(updatedMetadata).toEqual({ + has_pending_invitation: false + }) +}) diff --git a/src/composition.ts b/src/composition.ts index b5432314..46dce15b 100644 --- a/src/composition.ts +++ b/src/composition.ts @@ -22,6 +22,7 @@ import { import { GitHubOAuthTokenRefresher, GitHubInstallationAccessTokenDataSource, + Auth0MetadataUpdater, Auth0RefreshTokenReader, Auth0RepositoryAccessReader, Auth0UserIdentityProviderReader @@ -30,6 +31,7 @@ import { AccessTokenService, CachingRepositoryAccessReaderConfig, CachingUserIdentityProviderReader, + CompositeLogInHandler, CompositeLogOutHandler, CredentialsTransferringLogInHandler, ErrorIgnoringLogOutHandler, @@ -41,6 +43,7 @@ import { LockingAccessTokenService, OAuthTokenRepository, OnlyStaleRefreshingAccessTokenService, + RemoveInvitedFlagLogInHandler, RepositoryRestrictingAccessTokenDataSource, UserDataCleanUpLogOutHandler } from "@/features/auth/domain" @@ -187,20 +190,25 @@ export const projectDataSource = new CachingProjectDataSource( projectRepository ) -export const logInHandler = new CredentialsTransferringLogInHandler({ - isUserGuestReader: new IsUserGuestReader( - userIdentityProviderReader - ), - guestCredentialsTransferrer: new NullObjectCredentialsTransferrer(), - hostCredentialsTransferrer: new HostCredentialsTransferrer({ - refreshTokenReader: new Auth0RefreshTokenReader({ - ...auth0ManagementCredentials, - connection: "github" - }), - oAuthTokenRefresher: gitHubOAuthTokenRefresher, - oAuthTokenRepository: oAuthTokenRepository - }) -}) +export const logInHandler = new CompositeLogInHandler([ + new CredentialsTransferringLogInHandler({ + isUserGuestReader: new IsUserGuestReader( + userIdentityProviderReader + ), + guestCredentialsTransferrer: new NullObjectCredentialsTransferrer(), + hostCredentialsTransferrer: new HostCredentialsTransferrer({ + refreshTokenReader: new Auth0RefreshTokenReader({ + ...auth0ManagementCredentials, + connection: "github" + }), + oAuthTokenRefresher: gitHubOAuthTokenRefresher, + oAuthTokenRepository: oAuthTokenRepository + }) + }), + new RemoveInvitedFlagLogInHandler( + new Auth0MetadataUpdater({ ...auth0ManagementCredentials }) + ) +]) export const logOutHandler = new ErrorIgnoringLogOutHandler( new CompositeLogOutHandler([ diff --git a/src/features/auth/data/Auth0MetadataUpdater.ts b/src/features/auth/data/Auth0MetadataUpdater.ts new file mode 100644 index 00000000..88ffb524 --- /dev/null +++ b/src/features/auth/data/Auth0MetadataUpdater.ts @@ -0,0 +1,28 @@ +import { ManagementClient } from "auth0" + +type Auth0MetadataUpdaterConfig = { + readonly domain: string + readonly clientId: string + readonly clientSecret: string +} + +export default class Auth0MetadataUpdater { + private readonly managementClient: ManagementClient + + constructor(config: Auth0MetadataUpdaterConfig) { + this.managementClient = new ManagementClient({ + domain: config.domain, + clientId: config.clientId, + clientSecret: config.clientSecret + }) + } + + /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ + async updateMetadata(userId: string, metadata: {[key: string]: any}): Promise { + await this.managementClient.users.update({ + id: userId + }, { + app_metadata: metadata + }) + } +} diff --git a/src/features/auth/data/index.ts b/src/features/auth/data/index.ts index 5db47a9f..8090466b 100644 --- a/src/features/auth/data/index.ts +++ b/src/features/auth/data/index.ts @@ -1,3 +1,4 @@ +export { default as Auth0MetadataUpdater } from "./Auth0MetadataUpdater" export { default as Auth0RefreshTokenReader } from "./Auth0RefreshTokenReader" export { default as Auth0RepositoryAccessReader } from "./Auth0RepositoryAccessReader" export { default as Auth0UserIdentityProviderReader } from "./Auth0UserIdentityProviderReader" diff --git a/src/features/auth/domain/logIn/CompositeLogInHandler.ts b/src/features/auth/domain/logIn/CompositeLogInHandler.ts new file mode 100644 index 00000000..169be4c2 --- /dev/null +++ b/src/features/auth/domain/logIn/CompositeLogInHandler.ts @@ -0,0 +1,14 @@ +import ILogInHandler from "./ILogInHandler" + +export default class CompositeLogInHandler implements ILogInHandler { + private readonly handlers: ILogInHandler[] + + constructor(handlers: ILogInHandler[]) { + this.handlers = handlers + } + + async handleLogIn(userId: string): Promise { + const promises = this.handlers.map(e => e.handleLogIn(userId)) + await Promise.all(promises) + } +} diff --git a/src/features/auth/domain/logIn/RemoveInvitedFlagLogInHandler.ts b/src/features/auth/domain/logIn/RemoveInvitedFlagLogInHandler.ts new file mode 100644 index 00000000..c1565e9d --- /dev/null +++ b/src/features/auth/domain/logIn/RemoveInvitedFlagLogInHandler.ts @@ -0,0 +1,20 @@ +import ILogInHandler from "./ILogInHandler" + +export interface IMetadataUpdater { + /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ + updateMetadata(userId: string, metadata: {[key: string]: any}): Promise +} + +export default class RemoveInvitedFlagLogInHandler implements ILogInHandler { + private readonly metadataUpdater: IMetadataUpdater + + constructor(metadataUpdater: IMetadataUpdater) { + this.metadataUpdater = metadataUpdater + } + + async handleLogIn(userId: string): Promise { + await this.metadataUpdater.updateMetadata(userId, { + has_pending_invitation: false + }) + } +} diff --git a/src/features/auth/domain/logIn/index.ts b/src/features/auth/domain/logIn/index.ts index 50aadb78..ec98a5e6 100644 --- a/src/features/auth/domain/logIn/index.ts +++ b/src/features/auth/domain/logIn/index.ts @@ -1 +1,3 @@ +export { default as CompositeLogInHandler } from "./CompositeLogInHandler" export { default as CredentialsTransferringLogInHandler } from "./CredentialsTransferringLogInHandler" +export { default as RemoveInvitedFlagLogInHandler } from "./RemoveInvitedFlagLogInHandler" From 53b5d9390364ae3e83a3dafe49ccfc0fc079da9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=B8vring?= Date: Wed, 8 Nov 2023 09:35:54 +0100 Subject: [PATCH 34/42] Adds missing export --- src/features/auth/domain/logIn/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/features/auth/domain/logIn/index.ts b/src/features/auth/domain/logIn/index.ts index 50aadb78..0075dc5f 100644 --- a/src/features/auth/domain/logIn/index.ts +++ b/src/features/auth/domain/logIn/index.ts @@ -1 +1,2 @@ export { default as CredentialsTransferringLogInHandler } from "./CredentialsTransferringLogInHandler" +export type { default as ILogInHandler } from "./ILogInHandler" \ No newline at end of file From dc02280b0701b78b24373488e271f92610152430 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=B8vring?= Date: Wed, 8 Nov 2023 09:36:37 +0100 Subject: [PATCH 35/42] Adds missing trailing whitespace --- src/features/auth/domain/logIn/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/features/auth/domain/logIn/index.ts b/src/features/auth/domain/logIn/index.ts index 0075dc5f..47d8d206 100644 --- a/src/features/auth/domain/logIn/index.ts +++ b/src/features/auth/domain/logIn/index.ts @@ -1,2 +1,2 @@ export { default as CredentialsTransferringLogInHandler } from "./CredentialsTransferringLogInHandler" -export type { default as ILogInHandler } from "./ILogInHandler" \ No newline at end of file +export type { default as ILogInHandler } from "./ILogInHandler" From ec5646486252e07aa5d09f6de6bfc54a5e70d164 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=B8vring?= Date: Wed, 8 Nov 2023 09:37:57 +0100 Subject: [PATCH 36/42] Cleans up comment --- src/features/projects/domain/ForgivingProjectDataSource.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/features/projects/domain/ForgivingProjectDataSource.ts b/src/features/projects/domain/ForgivingProjectDataSource.ts index 64191e1e..82f5c40f 100644 --- a/src/features/projects/domain/ForgivingProjectDataSource.ts +++ b/src/features/projects/domain/ForgivingProjectDataSource.ts @@ -23,10 +23,9 @@ export default class ForgivingProjectDataSource implements IProjectDataSource { try { await this.accessTokenReader.getAccessToken() } catch { - // If we cannot get an access token for, we show an empty list - // of projects. It is common for guest users that we cannot get - // an access token because they have been incorrectly configured - // to have access to non-existing repositories. + // If we cannot get an access token, we show an empty list of projects. + // It is common for guest users that we cannot get an access token because they + // have been incorrectly configured to have access to non-existing repositories. return [] } return this.projectDataSource.getProjects() From 46af18632ba4c2a03554d67aa80d39dc376e3ca3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=B8vring?= Date: Wed, 8 Nov 2023 09:39:36 +0100 Subject: [PATCH 37/42] Removes forgiving data source --- src/composition.ts | 12 +++---- .../domain/ForgivingProjectDataSource.ts | 33 ------------------- src/features/projects/domain/index.ts | 1 - 3 files changed, 4 insertions(+), 42 deletions(-) delete mode 100644 src/features/projects/domain/ForgivingProjectDataSource.ts diff --git a/src/composition.ts b/src/composition.ts index b5432314..af8645e5 100644 --- a/src/composition.ts +++ b/src/composition.ts @@ -15,7 +15,6 @@ import { } from "@/features/projects/data" import { CachingProjectDataSource, - ForgivingProjectDataSource, ProjectRepository, SessionValidatingProjectDataSource } from "@/features/projects/domain" @@ -176,13 +175,10 @@ export const projectRepository = new ProjectRepository( export const projectDataSource = new CachingProjectDataSource( new SessionValidatingProjectDataSource( sessionValidator, - new ForgivingProjectDataSource({ - accessTokenReader: accessTokenService, - projectDataSource: new GitHubProjectDataSource( - userGitHubClient, - GITHUB_ORGANIZATION_NAME - ) - }) + new GitHubProjectDataSource( + userGitHubClient, + GITHUB_ORGANIZATION_NAME + ) ), projectRepository ) diff --git a/src/features/projects/domain/ForgivingProjectDataSource.ts b/src/features/projects/domain/ForgivingProjectDataSource.ts deleted file mode 100644 index 82f5c40f..00000000 --- a/src/features/projects/domain/ForgivingProjectDataSource.ts +++ /dev/null @@ -1,33 +0,0 @@ -import Project from "./Project" -import IProjectDataSource from "./IProjectDataSource" - -interface IAccessTokenReader { - getAccessToken(): Promise -} - -type ForgivingProjectDataSourceConfig = { - readonly accessTokenReader: IAccessTokenReader - readonly projectDataSource: IProjectDataSource -} - -export default class ForgivingProjectDataSource implements IProjectDataSource { - private readonly accessTokenReader: IAccessTokenReader - private readonly projectDataSource: IProjectDataSource - - constructor(config: ForgivingProjectDataSourceConfig) { - this.accessTokenReader = config.accessTokenReader - this.projectDataSource = config.projectDataSource - } - - async getProjects(): Promise { - try { - await this.accessTokenReader.getAccessToken() - } catch { - // If we cannot get an access token, we show an empty list of projects. - // It is common for guest users that we cannot get an access token because they - // have been incorrectly configured to have access to non-existing repositories. - return [] - } - return this.projectDataSource.getProjects() - } -} diff --git a/src/features/projects/domain/index.ts b/src/features/projects/domain/index.ts index f770e7c7..c70203d7 100644 --- a/src/features/projects/domain/index.ts +++ b/src/features/projects/domain/index.ts @@ -1,5 +1,4 @@ export { default as CachingProjectDataSource } from "./CachingProjectDataSource" -export { default as ForgivingProjectDataSource } from "./ForgivingProjectDataSource" export { default as getSelection } from "./getSelection" export type { default as IProjectConfig } from "./IProjectConfig" export type { default as IProjectDataSource } from "./IProjectDataSource" From 19b80da2132c5aeb10c780c2166288b217477249 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=B8vring?= Date: Wed, 8 Nov 2023 09:40:25 +0100 Subject: [PATCH 38/42] Revert "Removes forgiving data source" This reverts commit 46af18632ba4c2a03554d67aa80d39dc376e3ca3. --- src/composition.ts | 12 ++++--- .../domain/ForgivingProjectDataSource.ts | 33 +++++++++++++++++++ src/features/projects/domain/index.ts | 1 + 3 files changed, 42 insertions(+), 4 deletions(-) create mode 100644 src/features/projects/domain/ForgivingProjectDataSource.ts diff --git a/src/composition.ts b/src/composition.ts index af8645e5..b5432314 100644 --- a/src/composition.ts +++ b/src/composition.ts @@ -15,6 +15,7 @@ import { } from "@/features/projects/data" import { CachingProjectDataSource, + ForgivingProjectDataSource, ProjectRepository, SessionValidatingProjectDataSource } from "@/features/projects/domain" @@ -175,10 +176,13 @@ export const projectRepository = new ProjectRepository( export const projectDataSource = new CachingProjectDataSource( new SessionValidatingProjectDataSource( sessionValidator, - new GitHubProjectDataSource( - userGitHubClient, - GITHUB_ORGANIZATION_NAME - ) + new ForgivingProjectDataSource({ + accessTokenReader: accessTokenService, + projectDataSource: new GitHubProjectDataSource( + userGitHubClient, + GITHUB_ORGANIZATION_NAME + ) + }) ), projectRepository ) diff --git a/src/features/projects/domain/ForgivingProjectDataSource.ts b/src/features/projects/domain/ForgivingProjectDataSource.ts new file mode 100644 index 00000000..82f5c40f --- /dev/null +++ b/src/features/projects/domain/ForgivingProjectDataSource.ts @@ -0,0 +1,33 @@ +import Project from "./Project" +import IProjectDataSource from "./IProjectDataSource" + +interface IAccessTokenReader { + getAccessToken(): Promise +} + +type ForgivingProjectDataSourceConfig = { + readonly accessTokenReader: IAccessTokenReader + readonly projectDataSource: IProjectDataSource +} + +export default class ForgivingProjectDataSource implements IProjectDataSource { + private readonly accessTokenReader: IAccessTokenReader + private readonly projectDataSource: IProjectDataSource + + constructor(config: ForgivingProjectDataSourceConfig) { + this.accessTokenReader = config.accessTokenReader + this.projectDataSource = config.projectDataSource + } + + async getProjects(): Promise { + try { + await this.accessTokenReader.getAccessToken() + } catch { + // If we cannot get an access token, we show an empty list of projects. + // It is common for guest users that we cannot get an access token because they + // have been incorrectly configured to have access to non-existing repositories. + return [] + } + return this.projectDataSource.getProjects() + } +} diff --git a/src/features/projects/domain/index.ts b/src/features/projects/domain/index.ts index c70203d7..f770e7c7 100644 --- a/src/features/projects/domain/index.ts +++ b/src/features/projects/domain/index.ts @@ -1,4 +1,5 @@ export { default as CachingProjectDataSource } from "./CachingProjectDataSource" +export { default as ForgivingProjectDataSource } from "./ForgivingProjectDataSource" export { default as getSelection } from "./getSelection" export type { default as IProjectConfig } from "./IProjectConfig" export type { default as IProjectDataSource } from "./IProjectDataSource" From fc797b82ed9b33745254a028c4a25d09042a3c48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=B8vring?= Date: Wed, 8 Nov 2023 10:21:35 +0100 Subject: [PATCH 39/42] Proxies all blob requests --- .../blob/[owner]/[repository]/[...path]/route.ts | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/src/app/api/github/blob/[owner]/[repository]/[...path]/route.ts b/src/app/api/github/blob/[owner]/[repository]/[...path]/route.ts index 9c8b595c..2dd2d532 100644 --- a/src/app/api/github/blob/[owner]/[repository]/[...path]/route.ts +++ b/src/app/api/github/blob/[owner]/[repository]/[...path]/route.ts @@ -17,17 +17,12 @@ export async function GET(req: NextRequest, { params }: { params: GetBlobParams }) const url = new URL(item.downloadURL) const imageRegex = /\.(jpg|jpeg|png|webp|avif|gif)$/; - + const file = await fetch(url).then(r => r.blob()) + const headers = new Headers() if (new RegExp(imageRegex).exec(path)) { - const file = await fetch(url).then(r => r.blob()); - const headers = new Headers(); const cacheExpirationInSeconds = 60 * 60 * 24 * 30 // 30 days - headers.set("Content-Type", "image/*"); - headers.set("Cache-Control", `max-age=${cacheExpirationInSeconds}`); - - return new NextResponse(file, { status: 200, headers }) - } else { - return NextResponse.redirect(url) + headers.set("Cache-Control", `max-age=${cacheExpirationInSeconds}`) } + return new NextResponse(file, { status: 200, headers }) } From 47d8a9d558500eeb52868f44ba7b1a46609ad0e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=B8vring?= Date: Wed, 8 Nov 2023 10:47:15 +0100 Subject: [PATCH 40/42] Moves /api/github/blob to /api/blob --- .../{github => }/blob/[owner]/[repository]/[...path]/route.ts | 0 src/features/projects/data/GitHubProjectDataSource.ts | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename src/app/api/{github => }/blob/[owner]/[repository]/[...path]/route.ts (100%) diff --git a/src/app/api/github/blob/[owner]/[repository]/[...path]/route.ts b/src/app/api/blob/[owner]/[repository]/[...path]/route.ts similarity index 100% rename from src/app/api/github/blob/[owner]/[repository]/[...path]/route.ts rename to src/app/api/blob/[owner]/[repository]/[...path]/route.ts diff --git a/src/features/projects/data/GitHubProjectDataSource.ts b/src/features/projects/data/GitHubProjectDataSource.ts index f001549e..e055b4e3 100644 --- a/src/features/projects/data/GitHubProjectDataSource.ts +++ b/src/features/projects/data/GitHubProjectDataSource.ts @@ -238,6 +238,6 @@ export default class GitHubProjectDataSource implements IProjectDataSource { } private getGitHubBlobURL(owner: string, repository: string, path: string, ref: string): string { - return `/api/github/blob/${owner}/${repository}/${path}?ref=${ref}` + return `/api/blob/${owner}/${repository}/${path}?ref=${ref}` } } From ef88ba0978918c71352b5bfaaaedcd2ef523610a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20B=2E=20St=C3=B8vring?= Date: Wed, 8 Nov 2023 13:53:55 +0100 Subject: [PATCH 41/42] Update invite-guest.yml --- .github/workflows/invite-guest.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/invite-guest.yml b/.github/workflows/invite-guest.yml index e04d7347..b1e4904b 100644 --- a/.github/workflows/invite-guest.yml +++ b/.github/workflows/invite-guest.yml @@ -16,7 +16,7 @@ on: description: E-mail address to send invitation to required: true roles: - description: Comma-separated list of roles + description: Comma-separated list of repositories user needs access to required: true env: OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN_SHAPE_DOCS }} From f61b67da83f25c48d1ef915122fec7fddd1a4bcd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=B8vring?= Date: Wed, 8 Nov 2023 15:03:07 +0100 Subject: [PATCH 42/42] Expires values cached in Redis --- .../CachingRepositoryAccessReaderConfig.ts | 4 ++++ .../CachingUserIdentityProviderReader.test.ts | 5 ++++- __test__/auth/GuestAccessTokenService.test.ts | 4 ++-- __test__/auth/OAuthTokenRepository.test.ts | 5 ++++- .../KeyValueUserDataRepository.test.ts | 21 +++++++++++++++++++ src/common/keyValueStore/IKeyValueStore.ts | 5 +++++ .../keyValueStore/RedisKeyValueStore.ts | 8 +++++++ src/common/userData/IUserDataRepository.ts | 1 + .../userData/KeyValueUserDataRepository.ts | 4 ++++ .../accessToken/GuestAccessTokenService.ts | 4 ++-- .../domain/oAuthToken/OAuthTokenRepository.ts | 2 +- .../CachingRepositoryAccessReaderConfig.ts | 2 +- .../CachingUserIdentityProviderReader.ts | 2 +- .../projects/domain/ProjectRepository.ts | 2 +- 14 files changed, 59 insertions(+), 10 deletions(-) diff --git a/__test__/auth/CachingRepositoryAccessReaderConfig.ts b/__test__/auth/CachingRepositoryAccessReaderConfig.ts index 3ee2aa6a..8833cc64 100644 --- a/__test__/auth/CachingRepositoryAccessReaderConfig.ts +++ b/__test__/auth/CachingRepositoryAccessReaderConfig.ts @@ -9,6 +9,7 @@ test("It fetches repository names for user if they are not cached", async () => return null }, async set() {}, + async setExpiring() {}, async delete() {} }, repositoryAccessReader: { @@ -32,6 +33,7 @@ test("It does not fetch repository names if they are cached", async () => { return "[\"foo\"]" }, async set() {}, + async setExpiring() {}, async delete() {} }, repositoryAccessReader: { @@ -57,6 +59,7 @@ test("It caches fetched repository names for user", async () => { cachedUserId = userId cachedRepositoryNames = value }, + async setExpiring() {}, async delete() {} }, repositoryAccessReader: { @@ -77,6 +80,7 @@ test("It decodes cached repository names", async () => { return "[\"foo\",\"bar\"]" }, async set() {}, + async setExpiring() {}, async delete() {} }, repositoryAccessReader: { diff --git a/__test__/auth/CachingUserIdentityProviderReader.test.ts b/__test__/auth/CachingUserIdentityProviderReader.test.ts index 60515704..0075f9a3 100644 --- a/__test__/auth/CachingUserIdentityProviderReader.test.ts +++ b/__test__/auth/CachingUserIdentityProviderReader.test.ts @@ -8,6 +8,7 @@ test("It fetches user identity provider if it is not cached", async () => { return null }, async set() {}, + async setExpiring() {}, async delete() {} }, { async getUserIdentityProvider() { @@ -26,6 +27,7 @@ test("It does not fetch user identity provider if it is cached", async () => { return UserIdentityProvider.GITHUB }, async set() {}, + async setExpiring() {}, async delete() {} }, { async getUserIdentityProvider() { @@ -44,7 +46,8 @@ test("It caches fetched user identity provider for user", async () => { async get() { return null }, - async set(userId, userIdentityProvider) { + async set() {}, + async setExpiring(userId, userIdentityProvider) { cachedUserId = userId cachedUserIdentityProvider = userIdentityProvider }, diff --git a/__test__/auth/GuestAccessTokenService.test.ts b/__test__/auth/GuestAccessTokenService.test.ts index 708a630d..ceeceb4a 100644 --- a/__test__/auth/GuestAccessTokenService.test.ts +++ b/__test__/auth/GuestAccessTokenService.test.ts @@ -13,7 +13,7 @@ test("It gets the access token for the user", async () => { readUserId = userId return "foo" }, - async set() {} + async setExpiring() {} }, dataSource: { async getAccessToken() { @@ -38,7 +38,7 @@ test("It refreshes access token on demand when there is no cached access token", async get() { return null }, - async set() {} + async setExpiring() {} }, dataSource: { async getAccessToken() { diff --git a/__test__/auth/OAuthTokenRepository.test.ts b/__test__/auth/OAuthTokenRepository.test.ts index b5786016..2bcac8a0 100644 --- a/__test__/auth/OAuthTokenRepository.test.ts +++ b/__test__/auth/OAuthTokenRepository.test.ts @@ -11,6 +11,7 @@ test("It reads the auth token for the specified user", async () => { }) }, async set() {}, + async setExpiring() {}, async delete() {} }) await sut.get("1234") @@ -24,7 +25,8 @@ test("It stores the auth token for the specified user", async () => { async get() { return "" }, - async set(userId, data) { + async set() {}, + async setExpiring(userId, data) { storedUserId = userId storedJSON = data }, @@ -48,6 +50,7 @@ test("It deletes the auth token for the specified user", async () => { return "" }, async set() {}, + async setExpiring() {}, async delete(userId) { deletedUserId = userId } diff --git a/__test__/common/userData/KeyValueUserDataRepository.test.ts b/__test__/common/userData/KeyValueUserDataRepository.test.ts index 869403b4..3dc7cb04 100644 --- a/__test__/common/userData/KeyValueUserDataRepository.test.ts +++ b/__test__/common/userData/KeyValueUserDataRepository.test.ts @@ -8,6 +8,7 @@ test("It reads the expected key", async () => { return "" }, async set() {}, + async setExpiring() {}, async delete() {} }, "foo") await sut.get("123") @@ -23,12 +24,31 @@ test("It stores values under the expected key", async () => { async set(key) { storedKey = key }, + async setExpiring() {}, async delete() {} }, "foo") await sut.set("123", "bar") expect(storedKey).toBe("foo[123]") }) +test("It stores values under the expected key with expected time to live", async () => { + let storedKey: string | undefined + let storedTimeToLive: number | undefined + const sut = new KeyValueUserDataRepository({ + async get() { + return "" + }, + async set() {}, + async setExpiring(key, _value, timeToLive) { + storedKey = key + storedTimeToLive = timeToLive + }, + async delete() {} + }, "foo") + await sut.setExpiring("123", "bar", 24 * 3600) + expect(storedKey).toBe("foo[123]") + expect(storedTimeToLive).toBe(24 * 3600) +}) test("It deletes the expected key", async () => { let deletedKey: string | undefined @@ -37,6 +57,7 @@ test("It deletes the expected key", async () => { return "" }, async set() {}, + async setExpiring() {}, async delete(key) { deletedKey = key } diff --git a/src/common/keyValueStore/IKeyValueStore.ts b/src/common/keyValueStore/IKeyValueStore.ts index 8b604f56..470b8558 100644 --- a/src/common/keyValueStore/IKeyValueStore.ts +++ b/src/common/keyValueStore/IKeyValueStore.ts @@ -1,5 +1,10 @@ export default interface IKeyValueStore { get(key: string): Promise set(key: string, data: string | number | Buffer): Promise + setExpiring( + key: string, + data: string | number | Buffer, + timeToLive: number + ): Promise delete(key: string): Promise } diff --git a/src/common/keyValueStore/RedisKeyValueStore.ts b/src/common/keyValueStore/RedisKeyValueStore.ts index 4a552154..8c4c5401 100644 --- a/src/common/keyValueStore/RedisKeyValueStore.ts +++ b/src/common/keyValueStore/RedisKeyValueStore.ts @@ -16,6 +16,14 @@ export default class RedisKeyValueStore implements IKeyValueStore { await this.redis.set(key, data) } + async setExpiring( + key: string, + data: string | number | Buffer, + timeToLive: number + ): Promise { + await this.redis.setex(key, timeToLive, data) + } + async delete(key: string): Promise { await this.redis.del(key) } diff --git a/src/common/userData/IUserDataRepository.ts b/src/common/userData/IUserDataRepository.ts index cefadae4..8fca5d8e 100644 --- a/src/common/userData/IUserDataRepository.ts +++ b/src/common/userData/IUserDataRepository.ts @@ -1,5 +1,6 @@ export default interface IUserDataRepository { get(userId: string): Promise set(userId: string, value: T): Promise + setExpiring(userId: string, value: T, timeToLive: number): Promise delete(userId: string): Promise } diff --git a/src/common/userData/KeyValueUserDataRepository.ts b/src/common/userData/KeyValueUserDataRepository.ts index 58bd4fa6..b6318099 100644 --- a/src/common/userData/KeyValueUserDataRepository.ts +++ b/src/common/userData/KeyValueUserDataRepository.ts @@ -18,6 +18,10 @@ export default class KeyValueUserDataRepository implements IUserDataRepository { + await this.store.setExpiring(this.getKey(userId), value, timeToLive) + } + async delete(userId: string): Promise { await this.store.delete(this.getKey(userId)) } diff --git a/src/features/auth/domain/accessToken/GuestAccessTokenService.ts b/src/features/auth/domain/accessToken/GuestAccessTokenService.ts index a30c26f3..d2e1a059 100644 --- a/src/features/auth/domain/accessToken/GuestAccessTokenService.ts +++ b/src/features/auth/domain/accessToken/GuestAccessTokenService.ts @@ -6,7 +6,7 @@ export interface IUserIDReader { export interface Repository { get(userId: string): Promise - set(userId: string, token: string): Promise + setExpiring(userId: string, token: string, timeToLive: number): Promise } export interface DataSource { @@ -47,7 +47,7 @@ export default class GuestAccessTokenService implements IAccessTokenService { private async getNewAccessToken(): Promise { const userId = await this.userIdReader.getUserId() const newAccessToken = await this.dataSource.getAccessToken(userId) - await this.repository.set(userId, newAccessToken) + await this.repository.setExpiring(userId, newAccessToken, 7 * 24 * 3600) return newAccessToken } } \ No newline at end of file diff --git a/src/features/auth/domain/oAuthToken/OAuthTokenRepository.ts b/src/features/auth/domain/oAuthToken/OAuthTokenRepository.ts index 2a11b700..678f83bd 100644 --- a/src/features/auth/domain/oAuthToken/OAuthTokenRepository.ts +++ b/src/features/auth/domain/oAuthToken/OAuthTokenRepository.ts @@ -19,7 +19,7 @@ export default class OAuthTokenRepository implements IOAuthTokenRepository { async set(userId: string, token: OAuthToken): Promise { const string = ZodJSONCoder.encode(OAuthTokenSchema, token) - await this.repository.set(userId, string) + await this.repository.setExpiring(userId, string, 6 * 30 * 24 * 3600) } async delete(userId: string): Promise { diff --git a/src/features/auth/domain/repositoryAccess/CachingRepositoryAccessReaderConfig.ts b/src/features/auth/domain/repositoryAccess/CachingRepositoryAccessReaderConfig.ts index 15e4a717..02405b43 100644 --- a/src/features/auth/domain/repositoryAccess/CachingRepositoryAccessReaderConfig.ts +++ b/src/features/auth/domain/repositoryAccess/CachingRepositoryAccessReaderConfig.ts @@ -48,7 +48,7 @@ export default class CachingRepositoryAccessReader { const repositoryNames = await this.repositoryAccessReader.getRepositoryNames(userId) try { const str = ZodJSONCoder.encode(RepositoryNamesContainerSchema, repositoryNames) - await this.repository.set(userId, str) + await this.repository.setExpiring(userId, str, 7 * 24 * 3600) } catch (error: unknown) { console.error(error) } diff --git a/src/features/auth/domain/userIdentityProvider/CachingUserIdentityProviderReader.ts b/src/features/auth/domain/userIdentityProvider/CachingUserIdentityProviderReader.ts index 9d1fff8c..86456036 100644 --- a/src/features/auth/domain/userIdentityProvider/CachingUserIdentityProviderReader.ts +++ b/src/features/auth/domain/userIdentityProvider/CachingUserIdentityProviderReader.ts @@ -19,7 +19,7 @@ export default class CachingUserIdentityProviderReader implements IUserIdentityP return cachedValue as UserIdentityProvider } else { const userIdentity = await this.reader.getUserIdentityProvider(userId) - await this.repository.set(userId, userIdentity.toString()) + await this.repository.setExpiring(userId, userIdentity.toString(), 7 * 24 * 3600) return userIdentity } } diff --git a/src/features/projects/domain/ProjectRepository.ts b/src/features/projects/domain/ProjectRepository.ts index 45a494d2..c55e9341 100644 --- a/src/features/projects/domain/ProjectRepository.ts +++ b/src/features/projects/domain/ProjectRepository.ts @@ -29,7 +29,7 @@ export default class ProjectRepository implements IProjectRepository { async set(projects: Project[]): Promise { const userId = await this.userIDReader.getUserId() const string = ZodJSONCoder.encode(ProjectSchema.array(), projects) - await this.repository.set(userId, string) + await this.repository.setExpiring(userId, string, 30 * 24 * 3600) } async delete(): Promise {