feat(oauth2): add SVG icon support for OAuth2 providers (#825)

- Add optional `icon` field to UserOauth2Settings type
- Include preset SVG icons for GitHub, Linux Do, and Authentik templates
- Render icons on OAuth2 login buttons
- Add icon configuration UI with preview in admin panel

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Dream Hunter
2026-02-01 21:00:15 +08:00
committed by GitHub
parent f0da9289fc
commit d367bc92b2
6 changed files with 59 additions and 3 deletions

View File

@@ -1,5 +1,6 @@
export type UserOauth2Settings = {
name: string;
icon?: string; // SVG icon string for the provider
clientID: string;
clientSecret: string;
authorizationURL: string;

View File

@@ -83,7 +83,7 @@ export const useGlobalState = createGlobalState(
fetched: false,
enable: false,
enableMailVerify: false,
/** @type {{ clientID: string, name: string }[]} */
/** @type {{ clientID: string, name: string, icon?: string }[]} */
oauth2ClientIDs: [],
});
const userSettings = ref({

View File

@@ -25,6 +25,8 @@ const { t } = useI18n({
mailAllowList: 'Mail Address Allow List',
addOauth2: 'Add Oauth2',
name: 'Name',
icon: 'Icon (SVG, please ensure trusted source)',
iconPreview: 'Preview',
oauth2Type: 'Oauth2 Type',
enableEmailFormat: 'Enable Email Format',
userEmailFormat: 'Email Regex Pattern',
@@ -42,6 +44,8 @@ const { t } = useI18n({
mailAllowList: '邮件地址白名单',
addOauth2: '添加 Oauth2',
name: '名称',
icon: '图标 (SVG, 请确保来源可信)',
iconPreview: '预览',
oauth2Type: 'Oauth2 类型',
enableEmailFormat: '启用邮箱格式转换',
userEmailFormat: '邮箱正则表达式',
@@ -52,6 +56,12 @@ const { t } = useI18n({
}
});
const OAUTH2_ICONS: Record<string, string> = {
github: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0 0 24 12c0-6.63-5.37-12-12-12z"/></svg>',
linuxdo: '<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" width="1em" height="1em"><g><path d="m7.44,0s.09,0,.13,0c.09,0,.19,0,.28,0,.14,0,.29,0,.43,0,.09,0,.18,0,.27,0q.12,0,.25,0t.26.08c.15.03.29.06.44.08,1.97.38,3.78,1.47,4.95,3.11.04.06.09.12.13.18.67.96,1.15,2.11,1.3,3.28q0,.19.09.26c0,.15,0,.29,0,.44,0,.04,0,.09,0,.13,0,.09,0,.19,0,.28,0,.14,0,.29,0,.43,0,.09,0,.18,0,.27,0,.08,0,.17,0,.25q0,.19-.08.26c-.03.15-.06.29-.08.44-.38,1.97-1.47,3.78-3.11,4.95-.06.04-.12.09-.18.13-.96.67-2.11,1.15-3.28,1.3q-.19,0-.26.09c-.15,0-.29,0-.44,0-.04,0-.09,0-.13,0-.09,0-.19,0-.28,0-.14,0-.29,0-.43,0-.09,0-.18,0-.27,0-.08,0-.17,0-.25,0q-.19,0-.26-.08c-.15-.03-.29-.06-.44-.08-1.97-.38-3.78-1.47-4.95-3.11q-.07-.09-.13-.18c-.67-.96-1.15-2.11-1.3-3.28q0-.19-.09-.26c0-.15,0-.29,0-.44,0-.04,0-.09,0-.13,0-.09,0-.19,0-.28,0-.14,0-.29,0-.43,0-.09,0-.18,0-.27,0-.08,0-.17,0-.25q0-.19.08-.26c.03-.15.06-.29.08-.44.38-1.97,1.47-3.78,3.11-4.95.06-.04.12-.09.18-.13C4.42.73,5.57.26,6.74.1,7,.07,7.15,0,7.44,0Z" fill="#EFEFEF"/><path d="m1.27,11.33h13.45c-.94,1.89-2.51,3.21-4.51,3.88-1.99.59-3.96.37-5.8-.57-1.25-.7-2.67-1.9-3.14-3.3Z" fill="#FEB005"/><path d="m12.54,1.99c.87.7,1.82,1.59,2.18,2.68H1.27c.87-1.74,2.33-3.13,4.2-3.78,2.44-.79,5-.47,7.07,1.1Z" fill="#1D1D1F"/></g></svg>',
authentik: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M12 1L3 5v6c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V5l-9-4zm0 2.18l7 3.12v4.7c0 4.83-3.23 9.36-7 10.57-3.77-1.21-7-5.74-7-10.57V6.3l7-3.12zM11 7v6h2V7h-2zm0 8v2h2v-2h-2z"/></svg>',
};
const mailAllowOptions = constant.COMMOM_MAIL.map((item) => {
return { label: item, value: item }
})
@@ -91,6 +101,7 @@ const addNewOauth2 = () => {
userInfoURL: 'https://api.github.com/user',
userEmailKey: 'email',
scope: 'user:email',
icon: OAUTH2_ICONS.github,
},
linuxdo: {
authorizationURL: 'https://connect.linux.do/oauth2/authorize',
@@ -102,6 +113,7 @@ const addNewOauth2 = () => {
enableEmailFormat: true,
userEmailFormat: '^(.+)$',
userEmailReplace: 'linux_do_$1@oauth.linux.do',
icon: OAUTH2_ICONS.linuxdo,
},
authentik: {
authorizationURL: 'https://youdomain/application/o/authorize/',
@@ -110,12 +122,14 @@ const addNewOauth2 = () => {
userInfoURL: 'https://youdomain/application/o/userinfo/',
userEmailKey: 'email',
scope: 'email openid',
icon: OAUTH2_ICONS.authentik,
},
custom: {},
}
const template = templates[newOauth2Type.value] || {}
userOauth2Settings.value.push({
name: newOauth2Name.value,
icon: '',
clientID: '',
clientSecret: '',
authorizationURL: '',
@@ -198,6 +212,13 @@ onMounted(async () => {
<n-form-item-row :label="t('name')" required>
<n-input v-model:value="item.name" />
</n-form-item-row>
<n-form-item-row :label="t('icon')">
<n-input v-model:value="item.icon" type="textarea"
:autosize="{ minRows: 2, maxRows: 5 }" style="width: 100%;" />
</n-form-item-row>
<n-form-item-row v-if="item.icon" :label="t('iconPreview')">
<span class="oauth2-icon-preview" v-html="item.icon"></span>
</n-form-item-row>
<n-form-item-row label="Client ID" required>
<n-input v-model:value="item.clientID" />
</n-form-item-row>
@@ -276,4 +297,20 @@ onMounted(async () => {
place-items: center;
justify-content: center;
}
.oauth2-icon-preview {
display: inline-flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
border: 1px solid var(--n-border-color);
border-radius: 4px;
padding: 4px;
}
.oauth2-icon-preview :deep(svg) {
width: 100%;
height: 100%;
}
</style>

View File

@@ -233,6 +233,9 @@ onMounted(async () => {
</n-button>
<n-button @click="oauth2Login(item.clientID)" v-for="item in userOpenSettings.oauth2ClientIDs"
:key="item.clientID" block secondary strong>
<template #icon v-if="item.icon">
<span class="oauth2-icon" v-html="item.icon"></span>
</template>
{{ t('loginWith', { provider: item.name }) }}
</n-button>
</n-form>
@@ -305,4 +308,17 @@ onMounted(async () => {
.n-button {
margin-top: 10px;
}
.oauth2-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 18px;
height: 18px;
}
.oauth2-icon :deep(svg) {
width: 100%;
height: 100%;
}
</style>

View File

@@ -146,6 +146,7 @@ export class WebhookSettings {
export type UserOauth2Settings = {
name: string;
icon?: string; // SVG icon string for the provider
clientID: string;
clientSecret: string;
authorizationURL: string;

View File

@@ -11,13 +11,14 @@ export default {
openSettings: async (c: Context<HonoCustomType>) => {
const value = await getJsonSetting(c, CONSTANTS.USER_SETTINGS_KEY);
const settings = new UserSettings(value);
const oauth2ClientIDs = [] as { clientID: string, name: string }[];
const oauth2ClientIDs = [] as { clientID: string, name: string, icon?: string }[];
try {
const oauth2Settings = await getJsonSetting<UserOauth2Settings[]>(c, CONSTANTS.OAUTH2_SETTINGS_KEY);
oauth2ClientIDs.push(
...oauth2Settings?.map(s => ({
clientID: s.clientID,
name: s.name
name: s.name,
icon: s.icon,
})) || []
);
} catch (e) {