-import { AccessDeniedError } from 'oauth2-server'
+import express from 'express'
+import { AccessDeniedError } from '@node-oauth/oauth2-server'
import { PluginManager } from '@server/lib/plugins/plugin-manager'
-import { ActorModel } from '@server/models/activitypub/actor'
+import { AccountModel } from '@server/models/account/account'
+import { AuthenticatedResultUpdaterFieldName, RegisterServerAuthenticatedResult } from '@server/types'
import { MOAuthClient } from '@server/types/models'
import { MOAuthTokenUser } from '@server/types/models/oauth/oauth-token'
-import { MUser } from '@server/types/models/user/user'
-import { UserAdminFlag } from '@shared/models/users/user-flag.model'
-import { UserRole } from '@shared/models/users/user-role'
+import { MUser, MUserDefault } from '@server/types/models/user/user'
+import { pick } from '@shared/core-utils'
+import { AttributesOnly } from '@shared/typescript-utils'
import { logger } from '../../helpers/logger'
import { CONFIG } from '../../initializers/config'
-import { UserModel } from '../../models/account/user'
import { OAuthClientModel } from '../../models/oauth/oauth-client'
import { OAuthTokenModel } from '../../models/oauth/oauth-token'
-import { createUserAccountAndChannelAndPlaylist } from '../user'
+import { UserModel } from '../../models/user/user'
+import { findAvailableLocalActorName } from '../local-actor'
+import { buildUser, createUserAccountAndChannelAndPlaylist } from '../user'
+import { ExternalUser } from './external-auth'
import { TokensCache } from './tokens-cache'
type TokenInfo = {
bypass: boolean
pluginName: string
authName?: string
- user: {
- username: string
- email: string
- displayName: string
- role: UserRole
- }
+ user: ExternalUser
+ userUpdater: RegisterServerAuthenticatedResult['userUpdater']
}
async function getAccessToken (bearerToken: string) {
- logger.debug('Getting access token (bearerToken: ' + bearerToken + ').')
+ logger.debug('Getting access token.')
if (!bearerToken) return undefined
logger.info('Bypassing oauth login by plugin %s.', bypassLogin.pluginName)
let user = await UserModel.loadByEmail(bypassLogin.user.email)
+
if (!user) user = await createUserFromExternal(bypassLogin.pluginName, bypassLogin.user)
+ else user = await updateUserFromExternal(user, bypassLogin.user, bypassLogin.userUpdater)
// Cannot create a user
if (!user) throw new AccessDeniedError('Cannot create such user: an actor with that name already exists.')
// Then we just go through a regular login process
if (user.pluginAuth !== null) {
// This user does not belong to this plugin, skip it
- if (user.pluginAuth !== bypassLogin.pluginName) return null
+ if (user.pluginAuth !== bypassLogin.pluginName) {
+ logger.info(
+ 'Cannot bypass oauth login by plugin %s because %s has another plugin auth method (%s).',
+ bypassLogin.pluginName, bypassLogin.user.email, user.pluginAuth
+ )
+
+ return null
+ }
checkUserValidityOrThrow(user)
logger.debug('Getting User (username/email: ' + usernameOrEmail + ', password: ******).')
const user = await UserModel.loadByUsernameOrEmail(usernameOrEmail)
+
// If we don't find the user, or if the user belongs to a plugin
if (!user || user.pluginAuth !== null || !password) return null
async function revokeToken (
tokenInfo: { refreshToken: string },
- explicitLogout?: boolean
+ options: {
+ req?: express.Request
+ explicitLogout?: boolean
+ } = {}
): Promise<{ success: boolean, redirectUrl?: string }> {
+ const { req, explicitLogout } = options
+
const token = await OAuthTokenModel.getByRefreshTokenAndPopulateUser(tokenInfo.refreshToken)
if (token) {
let redirectUrl: string
if (explicitLogout === true && token.User.pluginAuth && token.authName) {
- redirectUrl = await PluginManager.Instance.onLogout(token.User.pluginAuth, token.authName, token.User, this.request)
+ redirectUrl = await PluginManager.Instance.onLogout(token.User.pluginAuth, token.authName, token.User, req)
}
TokensCache.Instance.clearCacheByToken(token.accessToken)
// ---------------------------------------------------------------------------
-async function createUserFromExternal (pluginAuth: string, options: {
- username: string
- email: string
- role: UserRole
- displayName: string
-}) {
- // Check an actor does not already exists with that name (removed user)
- const actor = await ActorModel.loadLocalByName(options.username)
- if (actor) return null
-
- const userToCreate = new UserModel({
- username: options.username,
+async function createUserFromExternal (pluginAuth: string, userOptions: ExternalUser) {
+ const username = await findAvailableLocalActorName(userOptions.username)
+
+ const userToCreate = buildUser({
+ ...pick(userOptions, [ 'email', 'role', 'adminFlags', 'videoQuota', 'videoQuotaDaily' ]),
+
+ username,
+ emailVerified: null,
password: null,
- email: options.email,
- nsfwPolicy: CONFIG.INSTANCE.DEFAULT_NSFW_POLICY,
- autoPlayVideo: true,
- role: options.role,
- videoQuota: CONFIG.USER.VIDEO_QUOTA,
- videoQuotaDaily: CONFIG.USER.VIDEO_QUOTA_DAILY,
- adminFlags: UserAdminFlag.NONE,
pluginAuth
- }) as MUser
+ })
const { user } = await createUserAccountAndChannelAndPlaylist({
userToCreate,
- userDisplayName: options.displayName
+ userDisplayName: userOptions.displayName
})
return user
}
+async function updateUserFromExternal (
+ user: MUserDefault,
+ userOptions: ExternalUser,
+ userUpdater: RegisterServerAuthenticatedResult['userUpdater']
+) {
+ if (!userUpdater) return user
+
+ {
+ type UserAttributeKeys = keyof AttributesOnly<UserModel>
+ const mappingKeys: { [ id in UserAttributeKeys ]?: AuthenticatedResultUpdaterFieldName } = {
+ role: 'role',
+ adminFlags: 'adminFlags',
+ videoQuota: 'videoQuota',
+ videoQuotaDaily: 'videoQuotaDaily'
+ }
+
+ for (const modelKey of Object.keys(mappingKeys)) {
+ const pluginOptionKey = mappingKeys[modelKey]
+
+ const newValue = userUpdater({ fieldName: pluginOptionKey, currentValue: user[modelKey], newValue: userOptions[pluginOptionKey] })
+ user.set(modelKey, newValue)
+ }
+ }
+
+ {
+ type AccountAttributeKeys = keyof Partial<AttributesOnly<AccountModel>>
+ const mappingKeys: { [ id in AccountAttributeKeys ]?: AuthenticatedResultUpdaterFieldName } = {
+ name: 'displayName'
+ }
+
+ for (const modelKey of Object.keys(mappingKeys)) {
+ const optionKey = mappingKeys[modelKey]
+
+ const newValue = userUpdater({ fieldName: optionKey, currentValue: user.Account[modelKey], newValue: userOptions[optionKey] })
+ user.Account.set(modelKey, newValue)
+ }
+ }
+
+ logger.debug('Updated user %s with plugin userUpdated function.', user.email, { user, userOptions })
+
+ user.Account = await user.Account.save()
+
+ return user.save()
+}
+
function checkUserValidityOrThrow (user: MUser) {
if (user.blocked) throw new AccessDeniedError('User is blocked.')
}