import output from '@/output' import { LOCAL_CONFIG_RELATIVE_PATH } from '@ones-open/cli-utils' import { AuthDAO, getYamlConfigContent, isEmail, isPassword, isPhone, isTcpURL, isURL, updateYamlConfigContent, } from '@ones-open/cli-utils' import type { GlobalCLIPluginConfigScope, ProjectLocalConfig } from '@ones-open/cli-utils' import type { LoginCLIOptionsSchema, LoginRequestParamsSchema } from '@ones-open/cli-utils' import { isNil, merge, omitBy } from '@senojs/lodash' import inquirer from 'inquirer' import type { QuestionCollection } from 'inquirer' import { join } from 'path' import { getReadableScopeListStr, getScopeList } from '../scope' import { cwd } from 'process' type LoginPromptsAnswer = LoginPromptsParamsAnswer | LoginPromptsScopeAnswer interface LoginPromptsURLs { baseURL?: string hostURL?: string } interface LoginPromptsParamsAnswer { baseURL: string hostURL: string username: string password: string } interface LoginPromptsScopeAnswer { baseURL: string hostURL: string scope: string | boolean } interface LoginPromptsInitialAnswer { baseURL?: string hostURL?: string username?: string password?: string scope?: string | boolean } async function displayLoginPrompts( URLs: LoginPromptsURLs, scopeMap: Map, options: { isNeedHostURL?: boolean } & LoginCLIOptionsSchema, ) { const { baseURL, hostURL } = URLs const { username, password, scope, isNeedHostURL = true } = options const loginInitialAnswers: LoginPromptsInitialAnswer = { baseURL, hostURL, username, password, scope, } const scopeList = await getScopeList(scopeMap) const readableScopeListString = await getReadableScopeListStr(scopeMap) const loginPrompts: QuestionCollection = [ { type: 'input', name: 'baseURL', message: 'Please enter the environment base URL:', transformer: (input: string) => { let baseURL = input.trim() // remove trailing slash baseURL.endsWith('/') && (baseURL = baseURL.slice(0, -1)) return baseURL }, validate: (input: string) => { if (!input) return 'URL is required' if (!isURL(input)) return 'URL is invalid' return true }, when: ({ baseURL }) => !baseURL, }, { type: 'input', name: 'hostURL', message: 'Please enter the environment host URL:', transformer: (input: string) => { let baseURL = input.trim() // remove trailing slash baseURL.endsWith('/') && (baseURL = baseURL.slice(0, -1)) return baseURL }, validate: (input: string) => { if (!input) return 'URL is required' if (!isTcpURL(input)) return 'host URL is invalid' return true }, when: ({ hostURL }) => isNeedHostURL && !hostURL, }, { type: 'list', name: 'scope', message: 'Please select a scope from the list below:', suffix: readableScopeListString, choices: scopeList, askAnswered: true, when: ({ scope }) => { if (!scope) return false if (!scopeMap.size) { output.warn('No scope is available, please configure a scope first') return false } const isNotSpecifyScope = typeof scope === 'boolean' if (isNotSpecifyScope) return true const isScopNotExists = !scopeMap.has(scope) if (isScopNotExists) { output.warn('The scope you entered does not exist') } return isScopNotExists }, }, { type: 'input', name: 'username', message: 'Username:', when: ({ scope, username }) => (scope && !scopeMap.size) || (!scope && !username), validate: (input: string) => { if (!input) return 'Username is required' const isValidated = isEmail(input) || isPhone(input) if (!isValidated) return 'Username is invalid' return true }, }, { type: 'password', name: 'password', message: 'Password:', when: ({ scope, password }) => (scope && !scopeMap.size) || (!scope && !password), validate: (input: string) => { if (!input) return 'Password is required' if (!isPassword(input)) return 'Password must be 8-32 characters and not contain spaces' return true }, }, ] const promptResult = await inquirer.prompt(loginPrompts, loginInitialAnswers) const answers = omitBy(promptResult, isNil) return answers as LoginPromptsAnswer } function convertUsernameField(username: string) { return isEmail(username) ? { email: username } : { phone: username } } async function convertPromptsAnswerToLoginParams( answers: LoginPromptsAnswer, scopeMap: Map, ): Promise { const { baseURL } = answers let params: LoginRequestParamsSchema if ('scope' in answers) { const { scope } = answers const isNotSpecifyScope = typeof scope === 'boolean' const finalScope = isNotSpecifyScope ? baseURL : scope const scopeConfig = scopeMap.get(finalScope) if (!scopeConfig) throw new Error(`Scope "${finalScope}" is not exits`) const { username: scopeUsername, password: scopePassword } = scopeConfig params = { baseURL, password: scopePassword, ...convertUsernameField(scopeUsername), } } else { const { username, password } = answers params = { baseURL, password, ...convertUsernameField(username), } } return params } async function fetchUserProfile(params: LoginRequestParamsSchema) { try { const { data } = await AuthDAO.login(params) const isLoginSuccess = !!data?.user?.token if (!isLoginSuccess) { throw new Error(`Unexpected response from server: \n${JSON.stringify(data, null, 2)}`) } return data } catch (error) { if (error instanceof Error) { error.message = `${error.message}\nPlease make sure your username, password and environment base URL are correct` throw error } } } async function getLocalConfigContent(dirPath?: string) { const localConfigPath = dirPath ? join(dirPath, LOCAL_CONFIG_RELATIVE_PATH) : join(cwd(), LOCAL_CONFIG_RELATIVE_PATH) const localConfigContent = await getYamlConfigContent(localConfigPath, { enableFileExistsCheck: true, }) return localConfigContent } async function updateLocalConfig( content: Record, options: { cwd: string; strategy: 'merge' | 'replace' } = { cwd: cwd(), strategy: 'merge' }, ) { const { cwd, strategy } = options const localConfigPath = join(cwd, LOCAL_CONFIG_RELATIVE_PATH) let tempContent = content if (strategy === 'merge') { try { const localConfigContent = await getLocalConfigContent(cwd) tempContent = merge(localConfigContent, tempContent) } catch (error) { if (error instanceof Error) { throw new Error( `${error.message}\nPlease make sure current working directory is root of project`, ) } } } await updateYamlConfigContent(localConfigPath, tempContent) } export { fetchUserProfile, convertUsernameField, convertPromptsAnswerToLoginParams, getLocalConfigContent, displayLoginPrompts, updateLocalConfig, } export type { LoginPromptsAnswer, LoginPromptsParamsAnswer, LoginPromptsScopeAnswer, LoginPromptsInitialAnswer, }