diff --git a/__test__/auth/AccessTokenService.test.ts b/__test__/auth/AccessTokenService.test.ts
index 4ad0edbf..9c0024c5 100644
--- a/__test__/auth/AccessTokenService.test.ts
+++ b/__test__/auth/AccessTokenService.test.ts
@@ -1,86 +1,121 @@
import { AccessTokenService } 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
+test("It reads the access token for a guest user", async () => {
+ let didReadAccessToken = false
const sut = new AccessTokenService({
- userIdReader: {
- async getUserId() {
- return "1234"
+ isGuestReader: {
+ async getIsGuest() {
+ return true
}
},
- repository: {
- async get(userId) {
- readUserID = userId
- return { accessToken: "foo", refreshToken: "bar" }
+ guestAccessTokenService: {
+ async getAccessToken() {
+ didReadAccessToken = true
+ return "oldAccessToken"
},
- async set() {},
- async delete() {},
+ async refreshAccessToken() {
+ return "newAccessToken"
+ }
},
- refresher: {
- async refreshOAuthToken() {
- return { accessToken: "foo", refreshToken: "bar" }
+ hostAccessTokenService: {
+ async getAccessToken() {
+ return "oldAccessToken"
+ },
+ async refreshAccessToken() {
+ return "newAccessToken"
}
}
})
- const accessToken = await sut.getAccessToken()
- expect(readUserID).toBe("1234")
- expect(accessToken).toBe("foo")
+ await sut.getAccessToken()
+ expect(didReadAccessToken).toBeTruthy()
})
-test("It refreshes OAuth using stored refresh token", async () => {
- let usedRefreshToken: string | undefined
+test("It refreshes the access token for a guest user", async () => {
+ let didRefreshAccessToken = false
const sut = new AccessTokenService({
- userIdReader: {
- async getUserId() {
- return "1234"
+ isGuestReader: {
+ async getIsGuest() {
+ return true
}
},
- repository: {
- async get() {
- return { accessToken: "oldAccessToken", refreshToken: "oldRefreshToken" }
+ guestAccessTokenService: {
+ async getAccessToken() {
+ return "oldAccessToken"
},
- async set() {},
- async delete() {},
+ async refreshAccessToken() {
+ didRefreshAccessToken = true
+ return "newAccessToken"
+ }
},
- refresher: {
- async refreshOAuthToken(refreshToken) {
- usedRefreshToken = refreshToken
- return { accessToken: "newAccessToken", refreshToken: "newRefreshToken" }
+ hostAccessTokenService: {
+ async getAccessToken() {
+ return "oldAccessToken"
+ },
+ async refreshAccessToken() {
+ return "newAccessToken"
}
}
})
await sut.refreshAccessToken("oldAccessToken")
- expect(usedRefreshToken).toBe("oldRefreshToken")
+ expect(didRefreshAccessToken).toBeTruthy()
})
-test("It stores the new OAuth token for the user", async () => {
- let storedUserId: string | undefined
- let storedOAuthToken: OAuthToken | undefined
+test("It reads the access token for a host user", async () => {
+ let didReadAccessToken = false
const sut = new AccessTokenService({
- userIdReader: {
- async getUserId() {
- return "1234"
+ isGuestReader: {
+ async getIsGuest() {
+ return false
}
},
- repository: {
- async get() {
- return { accessToken: "oldAccessToken", refreshToken: "oldRefreshToken" }
+ guestAccessTokenService: {
+ async getAccessToken() {
+ return "oldAccessToken"
},
- async set(userId, oAuthToken) {
- storedUserId = userId
- storedOAuthToken = oAuthToken
+ 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 AccessTokenService({
+ isGuestReader: {
+ async getIsGuest() {
+ return false
+ }
+ },
+ guestAccessTokenService: {
+ async getAccessToken() {
+ return "oldAccessToken"
},
- async delete() {},
+ async refreshAccessToken() {
+ return "newAccessToken"
+ }
},
- refresher: {
- async refreshOAuthToken() {
- return { accessToken: "newAccessToken", refreshToken: "newRefreshToken" }
+ hostAccessTokenService: {
+ async getAccessToken() {
+ return "oldAccessToken"
+ },
+ async refreshAccessToken() {
+ didRefreshAccessToken = true
+ return "newAccessToken"
}
}
})
- await sut.refreshAccessToken("foo")
- expect(storedUserId).toBe("1234")
- expect(storedOAuthToken?.accessToken).toBe("newAccessToken")
- expect(storedOAuthToken?.refreshToken).toBe("newRefreshToken")
+ await sut.refreshAccessToken("oldAccessToken")
+ expect(didRefreshAccessToken).toBeTruthy()
})
diff --git a/__test__/auth/CachingRepositoryAccessReaderConfig.ts b/__test__/auth/CachingRepositoryAccessReaderConfig.ts
new file mode 100644
index 00000000..3ee2aa6a
--- /dev/null
+++ b/__test__/auth/CachingRepositoryAccessReaderConfig.ts
@@ -0,0 +1,90 @@
+import { CachingRepositoryAccessReaderConfig } from "../../src/features/auth/domain"
+
+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/CachingUserIdentityProviderReader.test.ts b/__test__/auth/CachingUserIdentityProviderReader.test.ts
new file mode 100644
index 00000000..60515704
--- /dev/null
+++ b/__test__/auth/CachingUserIdentityProviderReader.test.ts
@@ -0,0 +1,60 @@
+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
+ 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())
+})
diff --git a/__test__/auth/CredentialsTransferringLogInHandler.test.ts b/__test__/auth/CredentialsTransferringLogInHandler.test.ts
index 43f2d956..f1061b40 100644
--- a/__test__/auth/CredentialsTransferringLogInHandler.test.ts
+++ b/__test__/auth/CredentialsTransferringLogInHandler.test.ts
@@ -1,12 +1,43 @@
import { CredentialsTransferringLogInHandler } from "../../src/features/auth/domain"
-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..708a630d
--- /dev/null
+++ b/__test__/auth/GuestAccessTokenService.test.ts
@@ -0,0 +1,52 @@
+import { GuestAccessTokenService } from "../../src/features/auth/domain"
+
+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 refreshes access token on demand when there is no cached access token", async () => {
+ let didRefreshAccessToken = false
+ const sut = new GuestAccessTokenService({
+ userIdReader: {
+ async getUserId() {
+ return "1234"
+ }
+ },
+ repository: {
+ async get() {
+ return null
+ },
+ async set() {}
+ },
+ dataSource: {
+ async getAccessToken() {
+ didRefreshAccessToken = true
+ return "foo"
+ }
+ }
+ })
+ await sut.getAccessToken()
+ expect(didRefreshAccessToken).toBeTruthy()
+})
diff --git a/__test__/auth/HostAccessTokenService.test.ts b/__test__/auth/HostAccessTokenService.test.ts
new file mode 100644
index 00000000..79fcaf72
--- /dev/null
+++ b/__test__/auth/HostAccessTokenService.test.ts
@@ -0,0 +1,86 @@
+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
+ const sut = new HostAccessTokenService({
+ userIdReader: {
+ async getUserId() {
+ return "1234"
+ }
+ },
+ repository: {
+ async get(userId) {
+ readUserID = userId
+ return { accessToken: "foo", refreshToken: "bar" }
+ },
+ async set() {},
+ async delete() {},
+ },
+ refresher: {
+ async refreshOAuthToken() {
+ return { accessToken: "foo", refreshToken: "bar" }
+ }
+ }
+ })
+ const accessToken = await sut.getAccessToken()
+ expect(readUserID).toBe("1234")
+ expect(accessToken).toBe("foo")
+})
+
+test("It refreshes OAuth using stored refresh token", async () => {
+ let usedRefreshToken: string | undefined
+ const sut = new HostAccessTokenService({
+ userIdReader: {
+ async getUserId() {
+ return "1234"
+ }
+ },
+ repository: {
+ async get() {
+ return { accessToken: "oldAccessToken", refreshToken: "oldRefreshToken" }
+ },
+ async set() {},
+ async delete() {},
+ },
+ refresher: {
+ async refreshOAuthToken(refreshToken) {
+ usedRefreshToken = refreshToken
+ return { accessToken: "newAccessToken", refreshToken: "newRefreshToken" }
+ }
+ }
+ })
+ await sut.refreshAccessToken("oldAccessToken")
+ expect(usedRefreshToken).toBe("oldRefreshToken")
+})
+
+test("It stores the new OAuth token for the user", async () => {
+ let storedUserId: string | undefined
+ let storedOAuthToken: OAuthToken | undefined
+ const sut = new HostAccessTokenService({
+ userIdReader: {
+ async getUserId() {
+ return "1234"
+ }
+ },
+ repository: {
+ async get() {
+ return { accessToken: "oldAccessToken", refreshToken: "oldRefreshToken" }
+ },
+ async set(userId, oAuthToken) {
+ storedUserId = userId
+ storedOAuthToken = oAuthToken
+ },
+ async delete() {},
+ },
+ refresher: {
+ async refreshOAuthToken() {
+ return { accessToken: "newAccessToken", refreshToken: "newRefreshToken" }
+ }
+ }
+ })
+ await sut.refreshAccessToken("foo")
+ expect(storedUserId).toBe("1234")
+ expect(storedOAuthToken?.accessToken).toBe("newAccessToken")
+ expect(storedOAuthToken?.refreshToken).toBe("newRefreshToken")
+})
diff --git a/__test__/auth/CredentialsTransferrer.test.ts b/__test__/auth/HostCredentialsTransferrer.test.ts
similarity index 90%
rename from __test__/auth/CredentialsTransferrer.test.ts
rename to __test__/auth/HostCredentialsTransferrer.test.ts
index 4a405803..a5988aa8 100644
--- a/__test__/auth/CredentialsTransferrer.test.ts
+++ b/__test__/auth/HostCredentialsTransferrer.test.ts
@@ -1,9 +1,9 @@
-import { CredentialsTransferrer } from "../../src/features/auth/domain"
+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
- 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/IsUserGuestReader.test.ts b/__test__/auth/IsUserGuestReader.test.ts
new file mode 100644
index 00000000..2d604818
--- /dev/null
+++ b/__test__/auth/IsUserGuestReader.test.ts
@@ -0,0 +1,22 @@
+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({
+ 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()
+})
diff --git a/__test__/auth/RepositoryRestrictingAccessTokenDataSource.ts b/__test__/auth/RepositoryRestrictingAccessTokenDataSource.ts
new file mode 100644
index 00000000..c4a26be7
--- /dev/null
+++ b/__test__/auth/RepositoryRestrictingAccessTokenDataSource.ts
@@ -0,0 +1,20 @@
+import { RepositoryRestrictingAccessTokenDataSource } from "../../src/features/auth/domain"
+
+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/__test__/common/session/SessionValidator.test.ts b/__test__/common/session/SessionValidator.test.ts
new file mode 100644
index 00000000..057bd6cd
--- /dev/null
+++ b/__test__/common/session/SessionValidator.test.ts
@@ -0,0 +1,49 @@
+import { SessionValidator } from "../../../src/common"
+
+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/app/[...slug]/page.tsx b/src/app/[...slug]/page.tsx
index 2193779a..cfcb585e 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 { getProjectId, getSpecificationId, getVersionId } from "../../common"
+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/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/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/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/Auth0Session.ts b/src/common/session/Auth0Session.ts
index 4db56f72..10c9e02c 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 { UnauthorizedError } from "../../common"
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/common/session/SessionValidator.ts b/src/common/session/SessionValidator.ts
new file mode 100644
index 00000000..4b3da8c9
--- /dev/null
+++ b/src/common/session/SessionValidator.ts
@@ -0,0 +1,32 @@
+import ISessionValidator from "./ISessionValidator"
+
+interface IIsGuestReader {
+ getIsGuest(): Promise
+}
+
+type SessionValidatorConfig = {
+ readonly isGuestReader: IIsGuestReader
+ readonly guestSessionValidator: ISessionValidator
+ readonly hostSessionValidator: ISessionValidator
+}
+
+export default class SessionValidator implements ISessionValidator {
+ private readonly isGuestReader: IIsGuestReader
+ private readonly guestSessionValidator: ISessionValidator
+ private readonly hostSessionValidator: ISessionValidator
+
+ constructor(config: SessionValidatorConfig) {
+ this.isGuestReader = config.isGuestReader
+ this.guestSessionValidator = config.guestSessionValidator
+ this.hostSessionValidator = config.hostSessionValidator
+ }
+
+ async validateSession(): Promise {
+ const isGuest = await this.isGuestReader.getIsGuest()
+ if (isGuest) {
+ return await this.guestSessionValidator.validateSession()
+ } else {
+ return await this.hostSessionValidator.validateSession()
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/common/session/index.ts b/src/common/session/index.ts
index 5e920e71..4d19783f 100644
--- a/src/common/session/index.ts
+++ b/src/common/session/index.ts
@@ -1,3 +1,5 @@
+export { default as AlwaysValidSessionValidator } from "./AlwaysValidSessionValidator"
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/composition.ts b/src/composition.ts
index b0fc8bc5..af8645e5 100644
--- a/src/composition.ts
+++ b/src/composition.ts
@@ -1,37 +1,48 @@
import Auth0Session from "@/common/session/Auth0Session"
import RedisKeyedMutexFactory from "@/common/mutex/RedisKeyedMutexFactory"
import RedisKeyValueStore from "@/common/keyValueStore/RedisKeyValueStore"
-
import {
AccessTokenRefreshingGitHubClient,
+ AlwaysValidSessionValidator,
GitHubClient,
- GitHubOrganizationSessionValidator,
+ GitHubOrganizationSessionValidator,
KeyValueUserDataRepository,
- SessionMutexFactory
+ SessionMutexFactory,
+ SessionValidator
} from "@/common"
import {
+ GitHubProjectDataSource
+} from "@/features/projects/data"
+import {
+ CachingProjectDataSource,
+ ProjectRepository,
+ SessionValidatingProjectDataSource
+} from "@/features/projects/domain"
+import {
+ GitHubOAuthTokenRefresher,
+ GitHubInstallationAccessTokenDataSource,
Auth0RefreshTokenReader,
- GitHubOAuthTokenRefresher
+ Auth0RepositoryAccessReader,
+ Auth0UserIdentityProviderReader
} from "@/features/auth/data"
import {
AccessTokenService,
+ CachingRepositoryAccessReaderConfig,
+ CachingUserIdentityProviderReader,
CompositeLogOutHandler,
- CredentialsTransferrer,
CredentialsTransferringLogInHandler,
ErrorIgnoringLogOutHandler,
+ GuestAccessTokenService,
+ NullObjectCredentialsTransferrer,
+ HostAccessTokenService,
+ HostCredentialsTransferrer,
+ IsUserGuestReader,
LockingAccessTokenService,
- OnlyStaleRefreshingAccessTokenService,
OAuthTokenRepository,
+ OnlyStaleRefreshingAccessTokenService,
+ RepositoryRestrictingAccessTokenDataSource,
UserDataCleanUpLogOutHandler
} from "@/features/auth/domain"
-import {
- GitHubProjectDataSource
-} from "@/features/projects/data"
-import {
- CachingProjectDataSource,
- ProjectRepository,
- SessionValidatingProjectDataSource
-} from "@/features/projects/domain"
const {
AUTH0_MANAGEMENT_DOMAIN,
@@ -45,18 +56,71 @@ 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
+}
-export const session = new Auth0Session()
+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 oAuthTokenRepository = new OAuthTokenRepository(
+export const userIdentityProviderReader = new CachingUserIdentityProviderReader(
+ userIdentityProviderRepository,
+ new Auth0UserIdentityProviderReader(auth0ManagementCredentials)
+)
+
+export const session = new Auth0Session({
+ isUserGuestReader: new IsUserGuestReader(userIdentityProviderReader)
+})
+
+const oAuthTokenRepository = new OAuthTokenRepository(
new KeyValueUserDataRepository(
new RedisKeyValueStore(REDIS_URL),
"authToken"
)
)
-const accessTokenService = new LockingAccessTokenService(
+const gitHubOAuthTokenRefresher = new GitHubOAuthTokenRefresher({
+ clientId: gitHubAppCredentials.clientId,
+ clientSecret: gitHubAppCredentials.clientSecret
+})
+
+const accessTokenRepository = new KeyValueUserDataRepository(
+ new RedisKeyValueStore(REDIS_URL),
+ "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),
session,
@@ -64,21 +128,23 @@ const accessTokenService = new LockingAccessTokenService(
),
new OnlyStaleRefreshingAccessTokenService(
new AccessTokenService({
- userIdReader: session,
- repository: oAuthTokenRepository,
- refresher: new GitHubOAuthTokenRefresher({
- clientId: GITHUB_CLIENT_ID,
- clientSecret: GITHUB_CLIENT_SECRET
+ isGuestReader: session,
+ guestAccessTokenService: new GuestAccessTokenService({
+ userIdReader: session,
+ repository: accessTokenRepository,
+ dataSource: guestAccessTokenDataSource
+ }),
+ hostAccessTokenService: new HostAccessTokenService({
+ userIdReader: session,
+ repository: oAuthTokenRepository,
+ refresher: gitHubOAuthTokenRefresher
})
})
)
)
export const gitHubClient = new GitHubClient({
- appId: GITHUB_APP_ID,
- clientId: GITHUB_CLIENT_ID,
- clientSecret: GITHUB_CLIENT_SECRET,
- privateKey: gitHubPrivateKey,
+ ...gitHubAppCredentials,
accessTokenReader: accessTokenService
})
@@ -87,10 +153,14 @@ export const userGitHubClient = new AccessTokenRefreshingGitHubClient(
gitHubClient
)
-export const sessionValidator = new GitHubOrganizationSessionValidator(
- userGitHubClient,
- GITHUB_ORGANIZATION_NAME
-)
+export const sessionValidator = new SessionValidator({
+ isGuestReader: session,
+ guestSessionValidator: new AlwaysValidSessionValidator(),
+ hostSessionValidator: new GitHubOrganizationSessionValidator(
+ userGitHubClient,
+ GITHUB_ORGANIZATION_NAME
+ )
+})
const projectUserDataRepository = new KeyValueUserDataRepository(
new RedisKeyValueStore(REDIS_URL),
@@ -113,25 +183,27 @@ export const projectDataSource = new CachingProjectDataSource(
projectRepository
)
-export const logInHandler = new CredentialsTransferringLogInHandler(
- new CredentialsTransferrer({
+export const logInHandler = new CredentialsTransferringLogInHandler({
+ isUserGuestReader: new IsUserGuestReader(
+ userIdentityProviderReader
+ ),
+ guestCredentialsTransferrer: new NullObjectCredentialsTransferrer(),
+ 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, userIdentityProviderRepository),
+ new UserDataCleanUpLogOutHandler(session, guestRepositoryAccessRepository),
+ new UserDataCleanUpLogOutHandler(session, oAuthTokenRepository),
+ new UserDataCleanUpLogOutHandler(session, accessTokenRepository)
])
)
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/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/Auth0UserIdentityProviderReader.ts b/src/features/auth/data/Auth0UserIdentityProviderReader.ts
new file mode 100644
index 00000000..dfd56b98
--- /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"
+
+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..7a958619
--- /dev/null
+++ b/src/features/auth/data/GitHubInstallationAccessTokenDataSource.ts
@@ -0,0 +1,50 @@
+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(repositoryNames: string[]): 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
+ 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
+ }
+ }
+}
diff --git a/src/features/auth/data/index.ts b/src/features/auth/data/index.ts
index 91e3b14e..5db47a9f 100644
--- a/src/features/auth/data/index.ts
+++ b/src/features/auth/data/index.ts
@@ -1,2 +1,5 @@
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/AccessTokenService.ts b/src/features/auth/domain/accessToken/AccessTokenService.ts
index 729f40c3..095aef31 100644
--- a/src/features/auth/domain/accessToken/AccessTokenService.ts
+++ b/src/features/auth/domain/accessToken/AccessTokenService.ts
@@ -1,39 +1,42 @@
import IAccessTokenService from "./IAccessTokenService"
-import IOAuthTokenRepository from "../oAuthToken/IOAuthTokenRepository"
-import IOAuthTokenRefresher from "../oAuthToken/IOAuthTokenRefresher"
-export interface IUserIDReader {
- getUserId(): Promise
+export interface IIsGuestReader {
+ getIsGuest(): Promise
}
-type AccessTokenServiceConfig = {
- readonly userIdReader: IUserIDReader
- readonly repository: IOAuthTokenRepository
- readonly refresher: IOAuthTokenRefresher
+interface AccessTokenServiceConfig {
+ readonly isGuestReader: IIsGuestReader
+ readonly guestAccessTokenService: IAccessTokenService
+ readonly hostAccessTokenService: IAccessTokenService
}
export default class AccessTokenService implements IAccessTokenService {
- private readonly userIdReader: IUserIDReader
- private readonly repository: IOAuthTokenRepository
- private readonly refresher: IOAuthTokenRefresher
+ private readonly isGuestReader: IIsGuestReader
+ private readonly guestAccessTokenService: IAccessTokenService
+ private readonly hostAccessTokenService: IAccessTokenService
constructor(config: AccessTokenServiceConfig) {
- this.userIdReader = config.userIdReader
- this.repository = config.repository
- this.refresher = config.refresher
+ this.isGuestReader = config.isGuestReader
+ this.guestAccessTokenService = config.guestAccessTokenService
+ this.hostAccessTokenService = config.hostAccessTokenService
}
async getAccessToken(): Promise {
- const userId = await this.userIdReader.getUserId()
- const oAuthToken = await this.repository.get(userId)
- return oAuthToken.accessToken
+ const service = await this.getService()
+ return await service.getAccessToken()
}
- async refreshAccessToken(_accessToken: string): Promise {
- const userId = await this.userIdReader.getUserId()
- const oAuthToken = await this.repository.get(userId)
- const newOAuthToken = await this.refresher.refreshOAuthToken(oAuthToken.refreshToken)
- await this.repository.set(userId, newOAuthToken)
- return newOAuthToken.accessToken
+ 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/accessToken/GuestAccessTokenService.ts b/src/features/auth/domain/accessToken/GuestAccessTokenService.ts
new file mode 100644
index 00000000..a30c26f3
--- /dev/null
+++ b/src/features/auth/domain/accessToken/GuestAccessTokenService.ts
@@ -0,0 +1,53 @@
+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(userId: string): 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) {
+ // 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)
+ return newAccessToken
+ }
+}
\ No newline at end of file
diff --git a/src/features/auth/domain/accessToken/HostAccessTokenService.ts b/src/features/auth/domain/accessToken/HostAccessTokenService.ts
new file mode 100644
index 00000000..7b400082
--- /dev/null
+++ b/src/features/auth/domain/accessToken/HostAccessTokenService.ts
@@ -0,0 +1,39 @@
+import IAccessTokenService from "./IAccessTokenService"
+import IOAuthTokenRepository from "../oAuthToken/IOAuthTokenRepository"
+import IOAuthTokenRefresher from "../oAuthToken/IOAuthTokenRefresher"
+
+export interface IUserIDReader {
+ getUserId(): Promise
+}
+
+type HostAccessTokenServiceConfig = {
+ readonly userIdReader: IUserIDReader
+ readonly repository: IOAuthTokenRepository
+ readonly refresher: IOAuthTokenRefresher
+}
+
+export default class HostAccessTokenService implements IAccessTokenService {
+ private readonly userIdReader: IUserIDReader
+ private readonly repository: IOAuthTokenRepository
+ private readonly refresher: IOAuthTokenRefresher
+
+ constructor(config: HostAccessTokenServiceConfig) {
+ this.userIdReader = config.userIdReader
+ this.repository = config.repository
+ this.refresher = config.refresher
+ }
+
+ async getAccessToken(): Promise {
+ const userId = await this.userIdReader.getUserId()
+ const oAuthToken = await this.repository.get(userId)
+ return oAuthToken.accessToken
+ }
+
+ async refreshAccessToken(_accessToken: string): Promise {
+ const userId = await this.userIdReader.getUserId()
+ const oAuthToken = await this.repository.get(userId)
+ const newOAuthToken = await this.refresher.refreshOAuthToken(oAuthToken.refreshToken)
+ await this.repository.set(userId, newOAuthToken)
+ return newOAuthToken.accessToken
+ }
+}
\ No newline at end of file
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/accessToken/index.ts b/src/features/auth/domain/accessToken/index.ts
index f23b4d05..fd3cb104 100644
--- a/src/features/auth/domain/accessToken/index.ts
+++ b/src/features/auth/domain/accessToken/index.ts
@@ -1,4 +1,5 @@
-export { default as AccessTokenService } from "./AccessTokenService"
-export type { default as IAccessTokenService } from "./IAccessTokenService"
+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 AccessTokenService } from "./AccessTokenService"
diff --git a/src/features/auth/domain/credentialsTransfer/CredentialsTransferrer.ts b/src/features/auth/domain/credentialsTransfer/HostCredentialsTransferrer.ts
similarity index 85%
rename from src/features/auth/domain/credentialsTransfer/CredentialsTransferrer.ts
rename to src/features/auth/domain/credentialsTransfer/HostCredentialsTransferrer.ts
index c061a2cb..d66a0534 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
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 {}
+}
diff --git a/src/features/auth/domain/credentialsTransfer/index.ts b/src/features/auth/domain/credentialsTransfer/index.ts
index d59f555e..3961d607 100644
--- a/src/features/auth/domain/credentialsTransfer/index.ts
+++ b/src/features/auth/domain/credentialsTransfer/index.ts
@@ -1,2 +1,2 @@
-export { default as CredentialsTransferrer } from "./CredentialsTransferrer"
-export type { default as ICredentialsTransferrer } from "./ICredentialsTransferrer"
+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
index bf537f9c..63cbe382 100644
--- a/src/features/auth/domain/index.ts
+++ b/src/features/auth/domain/index.ts
@@ -3,3 +3,5 @@ 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/CredentialsTransferringLogInHandler.ts b/src/features/auth/domain/logIn/CredentialsTransferringLogInHandler.ts
index 618b2713..ae967eb5 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) {
+ await this.guestCredentialsTransferrer.transferCredentials(userId)
+ } else {
+ await this.hostCredentialsTransferrer.transferCredentials(userId)
+ }
}
}
diff --git a/src/features/auth/domain/repositoryAccess/CachingRepositoryAccessReaderConfig.ts b/src/features/auth/domain/repositoryAccess/CachingRepositoryAccessReaderConfig.ts
new file mode 100644
index 00000000..15e4a717
--- /dev/null
+++ b/src/features/auth/domain/repositoryAccess/CachingRepositoryAccessReaderConfig.ts
@@ -0,0 +1,57 @@
+import { z } from "zod"
+import { ZodJSONCoder, IUserDataRepository } from "../../../../common"
+
+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)
+ } catch (error: unknown) {
+ 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)
+ } catch (error: unknown) {
+ 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)
+ }
+}
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/CachingUserIdentityProviderReader.ts b/src/features/auth/domain/userIdentityProvider/CachingUserIdentityProviderReader.ts
new file mode 100644
index 00000000..9d1fff8c
--- /dev/null
+++ b/src/features/auth/domain/userIdentityProvider/CachingUserIdentityProviderReader.ts
@@ -0,0 +1,26 @@
+import { IUserDataRepository } from "@/common"
+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
diff --git a/src/features/auth/domain/userIdentityProvider/index.ts b/src/features/auth/domain/userIdentityProvider/index.ts
new file mode 100644
index 00000000..74b56b57
--- /dev/null
+++ b/src/features/auth/domain/userIdentityProvider/index.ts
@@ -0,0 +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/auth/view/SessionAccessTokenBarrier.tsx b/src/features/auth/view/SessionAccessTokenBarrier.tsx
new file mode 100644
index 00000000..58996582
--- /dev/null
+++ b/src/features/auth/view/SessionAccessTokenBarrier.tsx
@@ -0,0 +1,19 @@
+import { ReactNode } from "react"
+import { redirect } from "next/navigation"
+import { session, accessTokenService } from "@/composition"
+
+export default async function SessionAccessTokenBarrier({
+ children
+}: {
+ children: ReactNode
+}) {
+ try {
+ const isGuest = await session.getIsGuest()
+ if (!isGuest) {
+ await accessTokenService.getAccessToken()
+ }
+ return <>{children}>
+ } catch {
+ redirect("/api/auth/logout")
+ }
+}
diff --git a/src/features/auth/view/SessionOAuthTokenBarrier.tsx b/src/features/auth/view/SessionOAuthTokenBarrier.tsx
deleted file mode 100644
index 158ec381..00000000
--- a/src/features/auth/view/SessionOAuthTokenBarrier.tsx
+++ /dev/null
@@ -1,17 +0,0 @@
-import { ReactNode } from "react"
-import { redirect } from "next/navigation"
-import { session, oAuthTokenRepository } from "@/composition"
-
-export default async function SessionOAuthTokenBarrier({
- children
-}: {
- children: ReactNode
-}) {
- try {
- const userId = await session.getUserId()
- await oAuthTokenRepository.get(userId)
- return <>{children}>
- } catch {
- redirect("/api/auth/logout")
- }
-}