feat(oauth2): add email format transformation support (#818)

* feat(oauth2): add email format transformation support

- Add enableEmailFormat, userEmailFormat, userEmailReplace fields
- Support regex pattern matching and replacement template ($1, $2, etc.)
- Add Linux Do OAuth2 template with email format pre-configured
- Add input length limit (256 chars) to prevent ReDoS attacks
- Update admin UI with conditional display and tooltips
- Update documentation (zh/en) with configuration examples

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* chore: update lock files and version

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: restore accessTokenFormat as optional field

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Dream Hunter
2026-01-23 21:30:44 +08:00
committed by GitHub
parent e4c96c9868
commit decede7ed3
13 changed files with 906 additions and 791 deletions

View File

@@ -26,6 +26,10 @@ const { t } = useI18n({
addOauth2: 'Add Oauth2',
name: 'Name',
oauth2Type: 'Oauth2 Type',
enableEmailFormat: 'Enable Email Format',
userEmailFormat: 'Email Regex Pattern',
userEmailReplace: 'Replace Template',
userEmailFormatTip: 'Use regex to transform email. Example: ^(.+)@old\\.com$ with $1@new.com',
tip: 'Third-party login will automatically use the user\'s email to register an account (the same email will be regarded as the same account), this account is the same as the registered account, and you can also set the password through the forget password',
},
zh: {
@@ -39,6 +43,10 @@ const { t } = useI18n({
addOauth2: '添加 Oauth2',
name: '名称',
oauth2Type: 'Oauth2 类型',
enableEmailFormat: '启用邮箱格式转换',
userEmailFormat: '邮箱正则表达式',
userEmailReplace: '替换模板',
userEmailFormatTip: '使用正则转换邮箱。示例: ^(.+)@old\\.com$ 配合 $1@new.com',
tip: '第三方登录会自动使用用户邮箱注册账号(邮箱相同将视为同一账号), 此账号和注册的账号相同, 也可以通过忘记密码设置密码',
}
}
@@ -75,80 +83,55 @@ const save = async () => {
}
const addNewOauth2 = () => {
const authorizationURL = () => {
switch (newOauth2Type.value) {
case 'github':
return 'https://github.com/login/oauth/authorize'
case 'authentik':
return 'https://youdomain/application/o/authorize/'
default:
return ''
}
}
const accessTokenURL = () => {
switch (newOauth2Type.value) {
case 'github':
return 'https://github.com/login/oauth/access_token'
case 'authentik':
return 'https://youdomain/application/o/token/'
default:
return ''
}
}
const accessTokenFormat = () => {
switch (newOauth2Type.value) {
case 'github':
return 'json'
case 'authentik':
return 'urlencoded'
default:
return ''
}
}
const userInfoURL = () => {
switch (newOauth2Type.value) {
case 'github':
return 'https://api.github.com/user'
case 'authentik':
return 'https://youdomain/application/o/userinfo/'
default:
return ''
}
}
const userEmailKey = () => {
switch (newOauth2Type.value) {
case 'github':
return 'email'
case 'authentik':
return 'email'
default:
return ''
}
}
const scope = () => {
switch (newOauth2Type.value) {
case 'github':
return 'user:email'
case 'authentik':
return 'email openid'
default:
return ''
}
const templates: Record<string, Partial<UserOauth2Settings>> = {
github: {
authorizationURL: 'https://github.com/login/oauth/authorize',
accessTokenURL: 'https://github.com/login/oauth/access_token',
accessTokenFormat: 'json',
userInfoURL: 'https://api.github.com/user',
userEmailKey: 'email',
scope: 'user:email',
},
linuxdo: {
authorizationURL: 'https://connect.linux.do/oauth2/authorize',
accessTokenURL: 'https://connect.linux.do/oauth2/token',
accessTokenFormat: 'urlencoded',
userInfoURL: 'https://connect.linux.do/api/user',
userEmailKey: 'id',
scope: 'user',
enableEmailFormat: true,
userEmailFormat: '^(.+)$',
userEmailReplace: 'linux_do_$1@oauth.linux.do',
},
authentik: {
authorizationURL: 'https://youdomain/application/o/authorize/',
accessTokenURL: 'https://youdomain/application/o/token/',
accessTokenFormat: 'urlencoded',
userInfoURL: 'https://youdomain/application/o/userinfo/',
userEmailKey: 'email',
scope: 'email openid',
},
custom: {},
}
const template = templates[newOauth2Type.value] || {}
userOauth2Settings.value.push({
name: newOauth2Name.value,
clientID: '',
clientSecret: '',
authorizationURL: authorizationURL(),
accessTokenURL: accessTokenURL(),
accessTokenFormat: accessTokenFormat(),
userInfoURL: userInfoURL(),
userEmailKey: userEmailKey(),
authorizationURL: '',
accessTokenURL: '',
accessTokenFormat: '',
userInfoURL: '',
userEmailKey: '',
redirectURL: `${window.location.origin}/user/oauth2/callback`,
logoutURL: '',
scope: scope(),
scope: '',
enableEmailFormat: false,
userEmailFormat: '',
userEmailReplace: '',
enableMailAllowList: false,
mailAllowList: constant.COMMOM_MAIL
mailAllowList: constant.COMMOM_MAIL,
...template,
} as UserOauth2Settings)
newOauth2Name.value = ''
showAddOauth2.value = false
@@ -174,6 +157,7 @@ onMounted(async () => {
<n-form-item-row :label="t('oauth2Type')" required>
<n-radio-group v-model:value="newOauth2Type">
<n-radio-button value="github" label="Github" />
<n-radio-button value="linuxdo" label="Linux Do" />
<n-radio-button value="authentik" label="Authentik" />
<n-radio-button value="custom" label="Custom" />
</n-radio-group>
@@ -218,7 +202,7 @@ onMounted(async () => {
<n-input v-model:value="item.clientID" />
</n-form-item-row>
<n-form-item-row label="Client Secret" required>
<n-input v-model:value="item.clientSecret" type="password" show-password-on="click" />
<n-input v-model:value="item.clientSecret" />
</n-form-item-row>
<n-form-item-row label="Authorization URL" required>
<n-input v-model:value="item.authorizationURL" />
@@ -235,6 +219,27 @@ onMounted(async () => {
<n-form-item-row label="User Email Key (Support JSONPATH like $[0].email)" required>
<n-input v-model:value="item.userEmailKey" />
</n-form-item-row>
<n-form-item-row :label="t('enableEmailFormat')">
<n-checkbox v-model:checked="item.enableEmailFormat">
{{ t('enable') }}
</n-checkbox>
</n-form-item-row>
<n-form-item-row v-if="item.enableEmailFormat" :label="t('userEmailFormat')">
<n-tooltip trigger="hover">
<template #trigger>
<n-input v-model:value="item.userEmailFormat" :placeholder="'^(.+)@old\\.com$'" />
</template>
{{ t('userEmailFormatTip') }}
</n-tooltip>
</n-form-item-row>
<n-form-item-row v-if="item.enableEmailFormat" :label="t('userEmailReplace')">
<n-tooltip trigger="hover">
<template #trigger>
<n-input v-model:value="item.userEmailReplace" placeholder="$1@new.com" />
</template>
{{ t('userEmailFormatTip') }}
</n-tooltip>
</n-form-item-row>
<n-form-item-row label="Redirect URL" required>
<n-input v-model:value="item.redirectURL" />
</n-form-item-row>