feat: oauth登录功能

This commit is contained in:
dap
2024-09-04 09:19:02 +08:00
parent 70fe375e25
commit fb525669b5
10 changed files with 398 additions and 51 deletions

View File

@@ -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: {

View File

@@ -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>

View 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>

View 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',
},
];

View File

@@ -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>

View 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>

View File

@@ -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/**",

View File

@@ -92,6 +92,19 @@ const formState = reactive({
username: localUsername,
});
/**
* oauth登录 需要tenantId参数
*/
watch(
() => formState.tenantId,
(tenantId) => {
localStorage.setItem('__oauth_tenant_id', tenantId);
},
{
immediate: true,
},
);
/**
* 默认选中第一项租户
*/

View File

@@ -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",

View File

@@ -5,7 +5,8 @@
"register": "注册",
"codeLogin": "验证码登录",
"qrcodeLogin": "二维码登录",
"forgetPassword": "忘记密码"
"forgetPassword": "忘记密码",
"oauthLogin": "第三方登录"
},
"dashboard": {
"title": "概览",