feat: oauth登录功能
This commit is contained in:
@@ -29,6 +29,14 @@ const coreRoutes: RouteRecordRaw[] = [
|
||||
path: '/',
|
||||
redirect: DEFAULT_HOME_PATH,
|
||||
},
|
||||
{
|
||||
component: () => import('#/views/_core/social-callback/index.vue'),
|
||||
meta: {
|
||||
title: $t('page.core.oauthLogin'),
|
||||
},
|
||||
name: 'OAuthRedirect',
|
||||
path: '/social-callback',
|
||||
},
|
||||
{
|
||||
component: AuthPageLayout,
|
||||
meta: {
|
||||
|
@@ -3,13 +3,14 @@ import { onMounted, ref } from 'vue';
|
||||
|
||||
import { AuthenticationLogin } from '@vben/common-ui';
|
||||
|
||||
import { message } from 'ant-design-vue';
|
||||
import { omit } from 'lodash-es';
|
||||
|
||||
import { tenantList, type TenantResp } from '#/api';
|
||||
import { captchaImage, type CaptchaResponse } from '#/api/core/captcha';
|
||||
import { useAuthStore } from '#/store';
|
||||
|
||||
import OauthLogin from './oauth-login.vue';
|
||||
|
||||
defineOptions({ name: 'Login' });
|
||||
|
||||
const authStore = useAuthStore();
|
||||
@@ -71,18 +72,6 @@ async function handleAccountLogin(values: LoginForm) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleOauthLogin(provider: string) {
|
||||
switch (provider) {
|
||||
case 'gitee': {
|
||||
message.success('todo gitee login');
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
message.warn('暂不支持该登录方式');
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -97,7 +86,10 @@ function handleOauthLogin(provider: string) {
|
||||
password-placeholder="密码"
|
||||
username-placeholder="用户名"
|
||||
@captcha-click="loadCaptcha"
|
||||
@oauth-login="handleOauthLogin"
|
||||
@submit="handleAccountLogin"
|
||||
/>
|
||||
>
|
||||
<template #third-party-login>
|
||||
<OauthLogin />
|
||||
</template>
|
||||
</AuthenticationLogin>
|
||||
</template>
|
||||
|
45
apps/web-antd/src/views/_core/authentication/oauth-login.vue
Normal file
45
apps/web-antd/src/views/_core/authentication/oauth-login.vue
Normal file
@@ -0,0 +1,45 @@
|
||||
<script setup lang="ts">
|
||||
import { createIconifyIcon } from '@vben/icons';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import { Col, Row, Tooltip } from 'ant-design-vue';
|
||||
|
||||
import { accountBindList } from '../oauth-common';
|
||||
|
||||
defineOptions({
|
||||
name: 'OAuthLogin',
|
||||
});
|
||||
|
||||
/**
|
||||
* 有action方法才会显示
|
||||
*/
|
||||
const clientList = accountBindList.filter((item) => item.action);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full sm:mx-auto md:max-w-md">
|
||||
<div class="mt-4 flex items-center justify-between">
|
||||
<span class="border-input w-[35%] border-b dark:border-gray-600"></span>
|
||||
<span class="text-muted-foreground text-center text-xs uppercase">
|
||||
{{ $t('authentication.thirdPartyLogin') }}
|
||||
</span>
|
||||
<span class="border-input w-[35%] border-b dark:border-gray-600"></span>
|
||||
</div>
|
||||
<Row class="enter-x flex items-center justify-evenly">
|
||||
<!-- todo 这里在点击登录时要disabled -->
|
||||
<Col v-for="item in clientList" :key="item.key" :span="4" class="my-2">
|
||||
<Tooltip :title="`${item.title}登录`">
|
||||
<span class="flex cursor-pointer items-center justify-center">
|
||||
<component
|
||||
:is="createIconifyIcon(item.avatar)"
|
||||
v-if="item.avatar"
|
||||
:style="{ color: item.color }"
|
||||
class="size-[24px]"
|
||||
@click="item.action"
|
||||
/>
|
||||
</span>
|
||||
</Tooltip>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
</template>
|
94
apps/web-antd/src/views/_core/oauth-common.ts
Normal file
94
apps/web-antd/src/views/_core/oauth-common.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import { authBinding } from '#/api/core/auth';
|
||||
/**
|
||||
* @description: 菜单
|
||||
* @param key key
|
||||
* @param title 标题
|
||||
* @param description 描述
|
||||
* @param extra 按钮文字
|
||||
* @param avatar 图标
|
||||
* @param color 图标颜色可直接写英文颜色/hex
|
||||
*/
|
||||
export interface ListItem {
|
||||
key: string;
|
||||
title: string;
|
||||
description: string;
|
||||
extra?: string;
|
||||
avatar?: string;
|
||||
color?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description: 绑定账号
|
||||
* @param source 来源 如gitee github 与后端的social-callback?source=xxx对应
|
||||
* @param bound 是否已经绑定
|
||||
* @param action 账号绑定回调
|
||||
*/
|
||||
export interface BindItem extends ListItem {
|
||||
source: string;
|
||||
bound?: boolean;
|
||||
action?: (source: string) => Promise<any>;
|
||||
}
|
||||
|
||||
/**
|
||||
* todo tenantId
|
||||
* 绑定授权从userStore.userInfo获取
|
||||
* 登录从localStorage获取
|
||||
* @param source
|
||||
*/
|
||||
async function handleAuthBinding(source: string) {
|
||||
const tenantId = localStorage.getItem('__oauth_tenant_id') ?? '000000';
|
||||
// 这里返回打开授权页面的链接
|
||||
const href = await authBinding(source, tenantId);
|
||||
window.location.href = href;
|
||||
}
|
||||
|
||||
/**
|
||||
* 账号绑定 list
|
||||
* 添加账号绑定只需要在这里增加即可
|
||||
* 添加过的项目会在个人主页-绑定账号中显示
|
||||
* action不为空的会在登录页显示
|
||||
*/
|
||||
export const accountBindList: BindItem[] = [
|
||||
{
|
||||
avatar: 'ri:taobao-fill',
|
||||
color: '#ff4000',
|
||||
description: '绑定淘宝账号',
|
||||
key: '1',
|
||||
source: 'taobao',
|
||||
title: '淘宝',
|
||||
},
|
||||
{
|
||||
avatar: 'fa-brands:alipay',
|
||||
color: '#2eabff',
|
||||
description: '绑定支付宝账号',
|
||||
key: '2',
|
||||
source: 'alipay',
|
||||
title: '支付宝',
|
||||
},
|
||||
{
|
||||
avatar: 'ri:dingding-fill',
|
||||
color: '#2eabff',
|
||||
description: '绑定钉钉账号',
|
||||
key: '3',
|
||||
source: 'ding',
|
||||
title: '钉钉',
|
||||
},
|
||||
{
|
||||
action: () => handleAuthBinding('gitee'),
|
||||
avatar: 'simple-icons:gitee',
|
||||
color: '#c71d23',
|
||||
description: '绑定GITEE账号',
|
||||
key: '4',
|
||||
source: 'gitee',
|
||||
title: 'GITEE',
|
||||
},
|
||||
{
|
||||
action: () => handleAuthBinding('github'),
|
||||
avatar: 'uiw:github',
|
||||
color: '',
|
||||
description: '绑定GITHUB账号',
|
||||
key: '5',
|
||||
source: 'github',
|
||||
title: 'GITHUB',
|
||||
},
|
||||
];
|
@@ -3,13 +3,26 @@ import type { ColumnsType } from 'ant-design-vue/es/table';
|
||||
|
||||
import type { SocialInfo } from '#/api/system/social/model';
|
||||
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { computed, onMounted, ref, unref } from 'vue';
|
||||
|
||||
import { Avatar, Modal, Table } from 'ant-design-vue';
|
||||
import { createIconifyIcon } from '@vben/icons';
|
||||
|
||||
import {
|
||||
Alert,
|
||||
Avatar,
|
||||
Card,
|
||||
List,
|
||||
ListItem,
|
||||
message,
|
||||
Modal,
|
||||
Table,
|
||||
} from 'ant-design-vue';
|
||||
|
||||
import { authUnbinding } from '#/api';
|
||||
import { socialList } from '#/api/system/social';
|
||||
|
||||
import { accountBindList, type BindItem } from '../../oauth-common';
|
||||
|
||||
const columns: ColumnsType = [
|
||||
{
|
||||
align: 'center',
|
||||
@@ -36,14 +49,6 @@ const columns: ColumnsType = [
|
||||
},
|
||||
];
|
||||
|
||||
const tableData = ref<SocialInfo[]>([]);
|
||||
|
||||
async function reload() {
|
||||
tableData.value = await socialList();
|
||||
}
|
||||
|
||||
onMounted(reload);
|
||||
|
||||
/**
|
||||
* 解绑账号
|
||||
*/
|
||||
@@ -58,10 +63,47 @@ function handleUnbind(record: Record<string, any>) {
|
||||
type: 'warning',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 没有传递action事件则不支持绑定 弹出默认提示
|
||||
*/
|
||||
function defaultTip(title: string) {
|
||||
message.info({ content: `暂不支持绑定${title}` });
|
||||
}
|
||||
|
||||
function buttonText(item: BindItem) {
|
||||
return item.bound ? '已绑定' : '绑定';
|
||||
}
|
||||
|
||||
/**
|
||||
* 已经绑定的平台
|
||||
*/
|
||||
const boundPlatformsList = ref<string[]>([]);
|
||||
const bindList = computed<BindItem[]>(() => {
|
||||
const list = [...accountBindList];
|
||||
list.forEach((item) => {
|
||||
item.bound = !!unref(boundPlatformsList).includes(item.source);
|
||||
});
|
||||
return list;
|
||||
});
|
||||
|
||||
const tableData = ref<SocialInfo[]>([]);
|
||||
|
||||
async function reload() {
|
||||
const resp = await socialList();
|
||||
/**
|
||||
* 平台转小写
|
||||
* 已经绑定的平台
|
||||
*/
|
||||
boundPlatformsList.value = resp.map((item) => item.source.toLowerCase());
|
||||
tableData.value = resp;
|
||||
}
|
||||
|
||||
onMounted(reload);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div class="flex flex-col gap-[16px]">
|
||||
<Table
|
||||
:columns="columns"
|
||||
:data-source="tableData"
|
||||
@@ -74,6 +116,74 @@ function handleUnbind(record: Record<string, any>) {
|
||||
</template>
|
||||
</template>
|
||||
</Table>
|
||||
<div>todo: 绑定功能</div>
|
||||
<div class="pb-3">
|
||||
<List
|
||||
:data-source="bindList"
|
||||
:grid="{ gutter: 8, xs: 1, sm: 1, md: 2, lg: 3, xl: 3, xxl: 3 }"
|
||||
>
|
||||
<template #renderItem="{ item }">
|
||||
<ListItem>
|
||||
<Card>
|
||||
<div class="flex w-full items-center gap-4">
|
||||
<div>
|
||||
<component
|
||||
:is="createIconifyIcon(item.avatar)"
|
||||
v-if="item.avatar"
|
||||
:style="{ color: item.color }"
|
||||
class="size-[40px]"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-1 items-center justify-between">
|
||||
<div class="flex flex-col">
|
||||
<h4
|
||||
class="mb-[4px] text-[14px] text-black/85 dark:text-white/85"
|
||||
>
|
||||
{{ item.title }}
|
||||
</h4>
|
||||
<span class="text-black/45 dark:text-white/45">
|
||||
{{ item.description }}
|
||||
</span>
|
||||
</div>
|
||||
<a-button
|
||||
:disabled="item.bound"
|
||||
size="small"
|
||||
type="link"
|
||||
@click="
|
||||
item.action ? item.action() : defaultTip(item.title)
|
||||
"
|
||||
>
|
||||
{{ buttonText(item) }}
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</ListItem>
|
||||
</template>
|
||||
</List>
|
||||
<Alert message="说明" type="info">
|
||||
<template #description>
|
||||
<p>
|
||||
需要添加第三方账号在
|
||||
<span class="font-bold">
|
||||
apps\web-antd\src\views\_core\oauth-common.ts
|
||||
</span>
|
||||
中accountBindList按模板添加
|
||||
</p>
|
||||
<p>
|
||||
添加对应模板后会在此处显示绑定, 但只有
|
||||
<span class="font-bold">实现了action才能在登录页显示</span>
|
||||
</p>
|
||||
</template>
|
||||
</Alert>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
/**
|
||||
list item 间距
|
||||
*/
|
||||
:deep(.ant-list-item) {
|
||||
padding: 6px;
|
||||
}
|
||||
</style>
|
||||
|
81
apps/web-antd/src/views/_core/social-callback/index.vue
Normal file
81
apps/web-antd/src/views/_core/social-callback/index.vue
Normal file
@@ -0,0 +1,81 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
|
||||
import { DEFAULT_HOME_PATH } from '@vben/constants';
|
||||
import { useAccessStore } from '@vben/stores';
|
||||
|
||||
import { message } from 'ant-design-vue';
|
||||
|
||||
import { type AuthApi, authCallback } from '#/api';
|
||||
import { useAuthStore } from '#/store';
|
||||
|
||||
import { accountBindList } from '../oauth-common';
|
||||
|
||||
const route = useRoute();
|
||||
|
||||
const code = route.query.code as string;
|
||||
const state = route.query.state as string;
|
||||
const stateJson = JSON.parse(atob(state));
|
||||
// 来源
|
||||
const source = route.query.source as string;
|
||||
// 租户ID
|
||||
const defaultTenantId = '000000';
|
||||
const tenantId = (stateJson.tenantId as string) ?? defaultTenantId;
|
||||
const domain = stateJson.domain as string;
|
||||
|
||||
const accessStore = useAccessStore();
|
||||
const authStore = useAuthStore();
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
onMounted(async () => {
|
||||
// 如果域名不相等 则重定向处理
|
||||
const host = window.location.host;
|
||||
if (domain !== host) {
|
||||
const urlFull = new URL(window.location.href);
|
||||
urlFull.host = domain;
|
||||
window.location.href = urlFull.toString();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 已经实现的平台
|
||||
const currentClient = accountBindList.find(
|
||||
(item) => item.source === source && item.action,
|
||||
);
|
||||
if (!currentClient) {
|
||||
message.error({ content: `未找到${source}平台` });
|
||||
return;
|
||||
}
|
||||
const data: AuthApi.OAuthLoginParams = {
|
||||
grantType: 'social',
|
||||
socialCode: code,
|
||||
socialState: state,
|
||||
source,
|
||||
tenantId,
|
||||
};
|
||||
// 没有token为登录 有token是授权
|
||||
if (accessStore.accessToken) {
|
||||
await authCallback(data);
|
||||
message.success(`${source}授权成功`);
|
||||
} else {
|
||||
// todo
|
||||
await authStore.authLogin(data as any);
|
||||
message.success(`${source}登录成功`);
|
||||
}
|
||||
} catch {
|
||||
// 500 你还没有绑定第三方账号,绑定后才可以登录!
|
||||
} finally {
|
||||
setTimeout(() => {
|
||||
router.push(DEFAULT_HOME_PATH);
|
||||
}, 1500);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div></div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
46
cspell.json
46
cspell.json
@@ -4,49 +4,51 @@
|
||||
"language": "en,en-US",
|
||||
"allowCompoundWords": true,
|
||||
"words": [
|
||||
"clsx",
|
||||
"esno",
|
||||
"demi",
|
||||
"unref",
|
||||
"taze",
|
||||
"acmr",
|
||||
"antd",
|
||||
"lucide",
|
||||
"antdv",
|
||||
"astro",
|
||||
"brotli",
|
||||
"clsx",
|
||||
"defu",
|
||||
"demi",
|
||||
"echarts",
|
||||
"ependencies",
|
||||
"esno",
|
||||
"etag",
|
||||
"execa",
|
||||
"Gitee",
|
||||
"iconify",
|
||||
"intlify",
|
||||
"lockb",
|
||||
"lucide",
|
||||
"mkdist",
|
||||
"mockjs",
|
||||
"nocheck",
|
||||
"noopener",
|
||||
"noreferrer",
|
||||
"nprogress",
|
||||
"nuxt",
|
||||
"pinia",
|
||||
"prefixs",
|
||||
"publint",
|
||||
"Qqchat",
|
||||
"qrcode",
|
||||
"shadcn",
|
||||
"sonner",
|
||||
"sortablejs",
|
||||
"styl",
|
||||
"taze",
|
||||
"ui-kit",
|
||||
"unplugin",
|
||||
"unref",
|
||||
"vben",
|
||||
"vbenjs",
|
||||
"vueuse",
|
||||
"yxxx",
|
||||
"nuxt",
|
||||
"lockb",
|
||||
"astro",
|
||||
"ui-kit",
|
||||
"styl",
|
||||
"vnode",
|
||||
"nocheck",
|
||||
"prefixs",
|
||||
"vitepress",
|
||||
"antdv",
|
||||
"ependencies",
|
||||
"vite",
|
||||
"echarts",
|
||||
"sortablejs",
|
||||
"etag"
|
||||
"vitepress",
|
||||
"vnode",
|
||||
"vueuse",
|
||||
"yxxx"
|
||||
],
|
||||
"ignorePaths": [
|
||||
"**/node_modules/**",
|
||||
|
@@ -92,6 +92,19 @@ const formState = reactive({
|
||||
username: localUsername,
|
||||
});
|
||||
|
||||
/**
|
||||
* oauth登录 需要tenantId参数
|
||||
*/
|
||||
watch(
|
||||
() => formState.tenantId,
|
||||
(tenantId) => {
|
||||
localStorage.setItem('__oauth_tenant_id', tenantId);
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* 默认选中第一项租户
|
||||
*/
|
||||
|
@@ -5,7 +5,8 @@
|
||||
"register": "Register",
|
||||
"codeLogin": "Code Login",
|
||||
"qrcodeLogin": "Qr Code Login",
|
||||
"forgetPassword": "Forget Password"
|
||||
"forgetPassword": "Forget Password",
|
||||
"oauthLogin": "Oauth Login"
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Dashboard",
|
||||
|
@@ -5,7 +5,8 @@
|
||||
"register": "注册",
|
||||
"codeLogin": "验证码登录",
|
||||
"qrcodeLogin": "二维码登录",
|
||||
"forgetPassword": "忘记密码"
|
||||
"forgetPassword": "忘记密码",
|
||||
"oauthLogin": "第三方登录"
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "概览",
|
||||
|
Reference in New Issue
Block a user