bde148f286
* wip * some extra fixes * stuff kinda works? * need to figure out mocks * need to figure out mocks * fix db listener * gqlgen fix * minor gqlgen watch adjustment * lint fixes * delete old codegen file * converting migrations to ESM * getModuleDIrectory * vitest sort of works * added back ts-vitest * resolve gql double load * fixing test timeout configs * TSC lint fix * fix automate tests * moar debugging * debugging * more debugging * codegen update * server works * yargs migrated * chore(server): getting rid of global mocks for Server ESM (#5046) * got rid of email mock * got rid of comment mocks * got rid of multi region mocks * got rid of stripe mock * admin override mock updated * removed final mock * fixing import.meta.resolve calls * another import.meta.resolve fix * added requested test * nyc ESM fix * removed unneeded deps + linting * yarn lock forgot to commit * tryna fix flakyness * email capture util fix * sendEmail fix * fix TSX check * sender transporter fix + CR comments * merge main fix * test fixx * circleci fix * gqlgen bigint fix * error formatter fix * more error formatting improvements * esmloader added to Dockerfile * more dockerfile fixes * bg jobs fix
446 lines
13 KiB
TypeScript
446 lines
13 KiB
TypeScript
import { logger, moduleLogger } from '@/observability/logging'
|
|
import { getDefaultApp } from '@/modules/auth/defaultApps'
|
|
import {
|
|
CreateApp,
|
|
CreateAuthorizationCode,
|
|
CreateRefreshToken,
|
|
DeleteApp,
|
|
DeleteAuthorizationCode,
|
|
GetAllAppsAuthorizedByUser,
|
|
GetAllAppsCreatedByUser,
|
|
GetAllPublicApps,
|
|
GetAllScopes,
|
|
GetApp,
|
|
GetAuthorizationCode,
|
|
GetRefreshToken,
|
|
GetTokenAppInfo,
|
|
RegisterDefaultApp,
|
|
RevokeExistingAppCredentials,
|
|
RevokeExistingAppCredentialsForUser,
|
|
RevokeRefreshToken,
|
|
UpdateApp,
|
|
UpdateDefaultApp
|
|
} from '@/modules/auth/domain/operations'
|
|
import {
|
|
ScopeRecord,
|
|
ServerAppsScopesRecord,
|
|
TokenScopeRecord,
|
|
UserServerAppTokenRecord
|
|
} from '@/modules/auth/helpers/types'
|
|
import {
|
|
ApiTokenRecord,
|
|
AuthorizationCodeRecord,
|
|
RefreshTokenRecord
|
|
} from '@/modules/auth/repositories'
|
|
import {
|
|
ApiTokens,
|
|
AuthorizationCodes,
|
|
RefreshTokens,
|
|
Scopes,
|
|
ServerApps,
|
|
ServerAppsScopes,
|
|
TokenScopes,
|
|
Users,
|
|
UserServerAppTokens
|
|
} from '@/modules/core/dbSchema'
|
|
import { ServerAppRecord, UserRecord } from '@/modules/core/helpers/types'
|
|
import cryptoRandomString from 'crypto-random-string'
|
|
import { Knex } from 'knex'
|
|
import { difference, omit } from 'lodash-es'
|
|
import { AppCreateError } from '@/modules/auth/errors'
|
|
import { UserInputError } from '@/modules/core/errors/userinput'
|
|
|
|
const tables = {
|
|
serverApps: (db: Knex) => db<ServerAppRecord>(ServerApps.name),
|
|
scopes: (db: Knex) => db<ScopeRecord>(Scopes.name),
|
|
serverAppsScopes: (db: Knex) => db<ServerAppsScopesRecord>(ServerAppsScopes.name),
|
|
users: (db: Knex) => db<UserRecord>(Users.name),
|
|
userServerAppTokens: (db: Knex) =>
|
|
db<UserServerAppTokenRecord>(UserServerAppTokens.name),
|
|
tokenScopes: (db: Knex) => db<TokenScopeRecord>(TokenScopes.name),
|
|
authorizationCodes: (db: Knex) =>
|
|
db<AuthorizationCodeRecord>(AuthorizationCodes.name),
|
|
refreshTokens: (db: Knex) => db<RefreshTokenRecord>(RefreshTokens.name),
|
|
apiTokens: (db: Knex) => db<ApiTokenRecord>(ApiTokens.name)
|
|
}
|
|
|
|
const getAppRedirectUrl = (app: Pick<ServerAppRecord, 'redirectUrl' | 'id'>) => {
|
|
const defaultApp = getDefaultApp({ id: app.id })
|
|
return defaultApp ? defaultApp.redirectUrl : app.redirectUrl
|
|
}
|
|
|
|
export const getAppFactory =
|
|
(deps: { db: Knex }): GetApp =>
|
|
async (params) => {
|
|
const { id } = params
|
|
|
|
const allScopes = await getAllScopesFactory(deps)()
|
|
|
|
const app = await tables.serverApps(deps.db).select('*').where({ id }).first()
|
|
if (!app) return null
|
|
|
|
const appScopeNames = (
|
|
await tables.serverAppsScopes(deps.db).select('scopeName').where({ appId: id })
|
|
).map((s) => s.scopeName)
|
|
|
|
const appScopes = allScopes.filter(
|
|
(scope) => appScopeNames.indexOf(scope.name) !== -1
|
|
)
|
|
const appAuthor = await tables
|
|
.users(deps.db)
|
|
.select('id', 'name', 'avatar')
|
|
.where({ id: app.authorId })
|
|
.first()
|
|
|
|
return {
|
|
...app,
|
|
scopes: appScopes,
|
|
author: appAuthor || null,
|
|
redirectUrl: getAppRedirectUrl(app)
|
|
}
|
|
}
|
|
|
|
export const getAllPublicAppsFactory =
|
|
(deps: { db: Knex }): GetAllPublicApps =>
|
|
async () => {
|
|
const apps: Array<
|
|
Pick<
|
|
ServerAppRecord,
|
|
| 'id'
|
|
| 'name'
|
|
| 'description'
|
|
| 'trustByDefault'
|
|
| 'redirectUrl'
|
|
| 'logo'
|
|
| 'termsAndConditionsLink'
|
|
> &
|
|
Partial<{ authorName: string; authorId: string }>
|
|
> = await tables
|
|
.serverApps(deps.db)
|
|
.select(
|
|
'server_apps.id',
|
|
'server_apps.name',
|
|
'server_apps.description',
|
|
'server_apps.trustByDefault',
|
|
'server_apps.redirectUrl',
|
|
'server_apps.logo',
|
|
'server_apps.termsAndConditionsLink',
|
|
'users.name as authorName',
|
|
'users.id as authorId'
|
|
)
|
|
.where({ public: true })
|
|
.leftJoin('users', 'users.id', '=', 'server_apps.authorId')
|
|
.orderBy('server_apps.trustByDefault', 'DESC')
|
|
|
|
return apps.map((app) => ({
|
|
...app,
|
|
redirectUrl: getAppRedirectUrl(app),
|
|
author:
|
|
app.authorId && app.authorName
|
|
? { name: app.authorName, id: app.authorId, avatar: null }
|
|
: null
|
|
}))
|
|
}
|
|
|
|
export const getAllAppsCreatedByUserFactory =
|
|
(deps: { db: Knex }): GetAllAppsCreatedByUser =>
|
|
async ({ userId }) => {
|
|
const apps: Array<
|
|
ServerAppRecord & Partial<{ authorName: string; authorId: string }>
|
|
> = await tables
|
|
.serverApps(deps.db)
|
|
.select('server_apps.*', 'users.name as authorName', 'users.id as authorId')
|
|
.where({ authorId: userId })
|
|
.leftJoin('users', 'users.id', '=', 'server_apps.authorId')
|
|
|
|
return apps.map((app) => ({
|
|
...app,
|
|
redirectUrl: getAppRedirectUrl(app),
|
|
author:
|
|
app.authorId && app.authorName
|
|
? { name: app.authorName, id: app.authorId, avatar: null }
|
|
: null
|
|
}))
|
|
}
|
|
|
|
export const getAllAppsAuthorizedByUserFactory =
|
|
(deps: { db: Knex }): GetAllAppsAuthorizedByUser =>
|
|
async ({ userId }) => {
|
|
const query = deps.db.raw(
|
|
`
|
|
SELECT DISTINCT ON (a."appId") a."appId" as id, sa."name", sa."description", sa."trustByDefault", sa."redirectUrl" as "redirectUrl", sa.logo, sa."termsAndConditionsLink", json_build_object('name', u.name, 'id', sa."authorId") as author
|
|
FROM user_server_app_tokens a
|
|
LEFT JOIN server_apps sa ON sa.id = a."appId"
|
|
LEFT JOIN users u ON sa."authorId" = u.id
|
|
WHERE a."userId" = ?
|
|
`,
|
|
[userId]
|
|
)
|
|
|
|
const { rows } = (await query) as {
|
|
rows: Array<ServerAppRecord & { author: Pick<UserRecord, 'name' | 'id'> }>
|
|
}
|
|
return rows.map((r) => ({
|
|
...r,
|
|
redirectUrl: getAppRedirectUrl(r),
|
|
author: r.author?.id ? { ...r.author, avatar: null } : null
|
|
}))
|
|
}
|
|
|
|
export const getAllScopesFactory =
|
|
(deps: { db: Knex }): GetAllScopes =>
|
|
async () => {
|
|
return tables.scopes(deps.db).select('*')
|
|
}
|
|
|
|
export const registerDefaultAppFactory =
|
|
(deps: { db: Knex }): RegisterDefaultApp =>
|
|
async (app) => {
|
|
const scopes = app.scopes.map((s) => ({ appId: app.id, scopeName: s }))
|
|
|
|
await tables.serverApps(deps.db).insert(omit(app, 'scopes'))
|
|
await tables.serverAppsScopes(deps.db).insert(scopes)
|
|
}
|
|
|
|
export const updateDefaultAppFactory =
|
|
(deps: { db: Knex }): UpdateDefaultApp =>
|
|
async (app, existingApp) => {
|
|
const { db: knex } = deps
|
|
|
|
const existingAppScopes = existingApp.scopes.map((s) => s.name)
|
|
|
|
const newScopes = difference(app.scopes, existingAppScopes)
|
|
const removedScopes = difference(existingAppScopes, app.scopes)
|
|
|
|
let affectedTokenIds: string[] = []
|
|
|
|
if (newScopes.length || removedScopes.length) {
|
|
moduleLogger.info(`🔑 Updating default app ${app.name}`)
|
|
affectedTokenIds = await tables
|
|
.userServerAppTokens(knex)
|
|
.where({ appId: app.id })
|
|
.pluck('tokenId')
|
|
}
|
|
|
|
// the internal code block makes sure if an error occurred, the trx gets rolled back
|
|
await knex.transaction(async (trx) => {
|
|
// add new scopes to the app
|
|
if (newScopes.length)
|
|
await tables
|
|
.serverAppsScopes(knex)
|
|
.insert(newScopes.map((s) => ({ appId: app.id, scopeName: s })))
|
|
.transacting(trx)
|
|
|
|
// remove scopes from the app
|
|
if (removedScopes.length)
|
|
await tables
|
|
.serverAppsScopes(knex)
|
|
.where({ appId: app.id })
|
|
.whereIn('scopeName', removedScopes)
|
|
.delete()
|
|
.transacting(trx)
|
|
|
|
//update user tokens with scope changes
|
|
if (affectedTokenIds.length)
|
|
await Promise.all(
|
|
affectedTokenIds.map(async (tokenId) => {
|
|
if (newScopes.length)
|
|
await tables
|
|
.tokenScopes(knex)
|
|
.insert(newScopes.map((s) => ({ tokenId, scopeName: s })))
|
|
.transacting(trx)
|
|
|
|
if (removedScopes.length)
|
|
await tables
|
|
.tokenScopes(knex)
|
|
.where({ tokenId })
|
|
.whereIn('scopeName', removedScopes)
|
|
.delete()
|
|
.transacting(trx)
|
|
})
|
|
)
|
|
|
|
// not writing the redirect url to the DB anymore
|
|
// it will be patched on an application level from the default app definitions
|
|
await tables
|
|
.serverApps(knex)
|
|
.where({ id: app.id })
|
|
.update(omit(app, ['scopes', 'redirectUrl']))
|
|
.transacting(trx)
|
|
})
|
|
}
|
|
|
|
export const createAppFactory =
|
|
(deps: { db: Knex }): CreateApp =>
|
|
async (app) => {
|
|
const id = cryptoRandomString({ length: 10 })
|
|
const secret = cryptoRandomString({ length: 10 })
|
|
const scopes = (app.scopes || []).filter((s) => !!s?.length)
|
|
|
|
if (!scopes.length) {
|
|
throw new AppCreateError('Cannot create an app with no scopes.')
|
|
}
|
|
|
|
const insertableApp = {
|
|
...omit(app, ['scopes', 'firstparty', 'trustByDefault']),
|
|
id,
|
|
secret
|
|
}
|
|
|
|
await tables.serverApps(deps.db).insert(insertableApp)
|
|
await tables
|
|
.serverAppsScopes(deps.db)
|
|
.insert(scopes.map((s) => ({ appId: id, scopeName: s })))
|
|
|
|
return { id, secret }
|
|
}
|
|
|
|
const revokeExistingAppCredentialsForMaybeUserFactory =
|
|
(deps: { db: Knex }) =>
|
|
async ({ appId, userId }: { appId: string; userId?: string }) => {
|
|
const maybeUserContext = userId ? { userId, appId } : { appId }
|
|
|
|
await tables
|
|
.authorizationCodes(deps.db)
|
|
.where({ ...maybeUserContext })
|
|
.del()
|
|
await tables
|
|
.refreshTokens(deps.db)
|
|
.where({ ...maybeUserContext })
|
|
.del()
|
|
|
|
const q = tables
|
|
.apiTokens(deps.db)
|
|
.whereIn('id', (qb) => {
|
|
qb.select('tokenId')
|
|
.from('user_server_app_tokens')
|
|
.where({ ...maybeUserContext })
|
|
})
|
|
.del()
|
|
|
|
const resApiTokenDelete = await q
|
|
return resApiTokenDelete
|
|
}
|
|
|
|
export const revokeExistingAppCredentialsForUserFactory =
|
|
({ db }: { db: Knex }): RevokeExistingAppCredentialsForUser =>
|
|
async ({ appId, userId }) =>
|
|
await revokeExistingAppCredentialsForMaybeUserFactory({ db })({ appId, userId })
|
|
|
|
export const revokeExistingAppCredentialsFactory =
|
|
({ db }: { db: Knex }): RevokeExistingAppCredentials =>
|
|
async ({ appId }) =>
|
|
await revokeExistingAppCredentialsForMaybeUserFactory({ db })({ appId })
|
|
|
|
export const updateAppFactory =
|
|
(deps: { db: Knex }): UpdateApp =>
|
|
async ({ app }) => {
|
|
// any app update should nuke everything and force users to re-authorize it.
|
|
await revokeExistingAppCredentialsFactory({ db: deps.db })({ appId: app.id })
|
|
|
|
if (app.scopes) {
|
|
logger.debug(app.scopes, app.id)
|
|
// Flush existing app scopes
|
|
await tables.serverAppsScopes(deps.db).where({ appId: app.id }).del()
|
|
// Update new scopes
|
|
await tables
|
|
.serverAppsScopes(deps.db)
|
|
.insert(app.scopes.map((s) => ({ appId: app.id, scopeName: s })))
|
|
}
|
|
|
|
const updatableApp = omit(app, ['secret', 'scopes'])
|
|
|
|
const [{ id }] = await tables
|
|
.serverApps(deps.db)
|
|
.returning('id')
|
|
.where({ id: app.id })
|
|
.update<ServerAppRecord[]>(updatableApp)
|
|
|
|
return id
|
|
}
|
|
|
|
export const deleteAppFactory =
|
|
(deps: { db: Knex }): DeleteApp =>
|
|
async ({ id }) => {
|
|
await revokeExistingAppCredentialsFactory({ db: deps.db })({ appId: id })
|
|
|
|
return await tables.serverApps(deps.db).where({ id }).del()
|
|
}
|
|
|
|
export const revokeRefreshTokenFactory =
|
|
(deps: { db: Knex }): RevokeRefreshToken =>
|
|
async ({ tokenId }) => {
|
|
tokenId = tokenId.slice(0, 10)
|
|
await tables.refreshTokens(deps.db).where({ id: tokenId }).del()
|
|
return true
|
|
}
|
|
|
|
export const createAuthorizationCodeFactory =
|
|
(deps: { db: Knex }): CreateAuthorizationCode =>
|
|
async ({ appId, userId, challenge }) => {
|
|
if (!challenge) throw new UserInputError('Please provide a valid challenge.')
|
|
|
|
const ac = {
|
|
id: cryptoRandomString({ length: 42 }),
|
|
appId,
|
|
userId,
|
|
challenge
|
|
}
|
|
|
|
await tables.authorizationCodes(deps.db).insert(ac)
|
|
return ac.id
|
|
}
|
|
|
|
export const getAuthorizationCodeFactory =
|
|
(deps: { db: Knex }): GetAuthorizationCode =>
|
|
async ({ id }) => {
|
|
return await tables.authorizationCodes(deps.db).select().where({ id }).first()
|
|
}
|
|
|
|
export const deleteAuthorizationCodeFactory =
|
|
(deps: { db: Knex }): DeleteAuthorizationCode =>
|
|
async ({ id }) => {
|
|
return await tables.authorizationCodes(deps.db).where({ id }).del()
|
|
}
|
|
|
|
export const createRefreshTokenFactory =
|
|
(deps: { db: Knex }): CreateRefreshToken =>
|
|
async ({ token }) => {
|
|
const [ret] = await tables.refreshTokens(deps.db).insert(token, '*')
|
|
return ret
|
|
}
|
|
|
|
export const getRefreshTokenFactory =
|
|
(deps: { db: Knex }): GetRefreshToken =>
|
|
async ({ id }) => {
|
|
return await tables.refreshTokens(deps.db).select('*').where({ id }).first()
|
|
}
|
|
|
|
export const getTokenAppInfoFactory =
|
|
(deps: { db: Knex }): GetTokenAppInfo =>
|
|
async (params: { token: string; appId?: string }) => {
|
|
const { token, appId } = params
|
|
const tokenId = token.slice(0, 10)
|
|
|
|
const q = tables
|
|
.apiTokens(deps.db)
|
|
.select<ServerAppRecord[]>(ServerApps.cols)
|
|
.where({
|
|
[ApiTokens.col.id]: tokenId,
|
|
...(appId
|
|
? {
|
|
[UserServerAppTokens.col.appId]: appId
|
|
}
|
|
: {})
|
|
})
|
|
.innerJoin(
|
|
UserServerAppTokens.name,
|
|
ApiTokens.col.id,
|
|
UserServerAppTokens.col.tokenId
|
|
)
|
|
.innerJoin(ServerApps.name, ServerApps.col.id, UserServerAppTokens.col.appId)
|
|
.first()
|
|
|
|
return await q
|
|
}
|