sso login

This commit is contained in:
ximplez 2024-03-30 23:11:22 +08:00
parent f23e1f7ea4
commit a556457aba
35 changed files with 688 additions and 231 deletions

6
.env
View File

@ -1,8 +1,8 @@
VITE_BASE_URL=/
VITE_APP_TITLE=SoybeanAdmin
VITE_APP_TITLE=COCO
VITE_APP_DESC=SoybeanAdmin is a fresh and elegant admin template
VITE_APP_DESC=COCO is a Config Center
# the prefix of the icon name
VITE_ICON_PREFIX=icon
@ -27,7 +27,7 @@ VITE_HTTP_PROXY=Y
VITE_ROUTER_HISTORY_MODE=history
# success code of backend service, when the code is received, the request is successful
VITE_SERVICE_SUCCESS_CODE=0000
VITE_SERVICE_SUCCESS_CODE=0
# logout codes of backend service, when the code is received, the user will be logged out and redirected to login page
VITE_SERVICE_LOGOUT_CODES=8888,8889

View File

@ -1,5 +1,5 @@
# backend service base url, prod environment
VITE_SERVICE_BASE_URL=https://mock.apifox.com/m1/3109515-0-default
VITE_SERVICE_BASE_URL=https://api.959617.xyz/coco
# other backend service base url, prod environment
VITE_OTHER_SERVICE_BASE_URL= `{

View File

@ -1,5 +1,5 @@
# backend service base url, test environment
VITE_SERVICE_BASE_URL=https://mock.apifox.com/m1/3109515-0-default
VITE_SERVICE_BASE_URL=http://127.0.0.1:8000
# other backend service base url, test environment
VITE_OTHER_SERVICE_BASE_URL= `{

View File

@ -23,3 +23,23 @@ jobs:
- run: npx githublogen
env:
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
- uses: softprops/action-gh-release@v2
build-and-push:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@latest
- name: Login to Docker Hub
uses: docker/login-action@latest
with:
registry: ghcr.io
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build and push Docker image
uses: docker/build-push-action@latest
with:
context: .
push: true
tags: v1.0.0

View File

@ -15,7 +15,7 @@ export function setupElegantRouter() {
const key = routeName as RouteKey;
if (key === 'login') {
const modules: UnionKey.LoginModule[] = ['pwd-login', 'code-login', 'register', 'reset-pwd', 'bind-wechat'];
const modules: UnionKey.LoginModule[] = ['pwd-login', 'code-login', 'register', 'reset-pwd', 'bind-wechat', 'sso-login', 'sso-callback'];
const moduleReg = modules.join('|');

17
docker/Dockerfile Normal file
View File

@ -0,0 +1,17 @@
# 安装Node.js
FROM node:20-alpine3.17 as build
WORKDIR /app
COPY .. /app
RUN npm install -g pnpm \
&& pnpm install \
&& pnpm run build
FROM nginx:stable-alpine
COPY --from=build /app/dist /usr/share/nginx/html/
# COPY nginx.conf /etc/nginx/conf.d/default.conf
COPY nginx.conf /etc/nginx/conf.d/
EXPOSE 80
CMD ["nginx","-g","daemon off;"]

50
docker/nginx.conf Normal file
View File

@ -0,0 +1,50 @@
server {
listen 80;
server_name localhost;
client_max_body_size 100m;
client_body_buffer_size 128k;
proxy_connect_timeout 5;
proxy_send_timeout 1800;
proxy_read_timeout 1800;
proxy_buffer_size 4k;
proxy_buffers 4 32k;
proxy_busy_buffers_size 64k;
proxy_temp_file_write_size 64k;
auth_basic "status";
#开启gzip
gzip on;
#低于1kb的资源不压缩
gzip_min_length 1k;
#压缩级别1-9越大压缩率越高同时消耗cpu资源也越多建议设置在5左右。
gzip_comp_level 5;
#需要压缩哪些响应类型的资源,多个空格隔开。不建议压缩图片.
gzip_types text/plain application/javascript application/x-javascript text/javascript text/xml text/css;
#配置禁用gzip条件支持正则。此处表示ie6及以下不启用gzip因为ie低版本不支持
gzip_disable "MSIE [1-6]\.";
#是否添加“Vary: Accept-Encoding”响应头
gzip_vary on;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
try_files $uri $uri/ /index.html; #VUE项目配置路由必须
}
location ^~ /assessh5 {
alias /usr/share/nginx/html; # inflow uni-app H5编译文件的目录,index.html所在目录
try_files $uri $uri/ /index.html last;
index index.html index.htm;
}
# location /inflow {
# try_files $uri $uri/ /inflow/index.html;
# root /usr/share/nginx/html/inflow/;
# index index.html;
# }
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}

View File

@ -35,12 +35,14 @@ export interface RequestOption<ResponseData = any> {
response: AxiosResponse<ResponseData>,
instance: AxiosInstance
) => Promise<AxiosResponse | null> | Promise<void>;
/**
* transform backend response when the responseType is json
*
* @param response Axios response
*/
transformBackendResponse(response: AxiosResponse<ResponseData>): any | Promise<any>;
/**
* The hook to handle error
*
@ -51,6 +53,13 @@ export interface RequestOption<ResponseData = any> {
onError: (error: AxiosError<ResponseData>) => void | Promise<void>;
}
export interface ErrorCodeHandle<ResponseData = any> {
handle: (
response: AxiosResponse<ResponseData>,
instance: AxiosInstance
) => Promise<AxiosResponse | null> | Promise<void>;
}
interface ResponseMap {
blob: Blob;
text: string;
@ -58,6 +67,7 @@ interface ResponseMap {
stream: ReadableStream<Uint8Array>;
document: Document;
}
export type ResponseType = keyof ResponseMap | 'json';
export type MappedType<R extends ResponseType, JsonType = any> = R extends keyof ResponseMap

View File

@ -10,6 +10,8 @@ export const themeSchemaOptions = transformRecordToOption(themeSchemaRecord);
export const loginModuleRecord: Record<UnionKey.LoginModule, App.I18n.I18nKey> = {
'pwd-login': 'page.login.pwdLogin.title',
'sso-login': 'page.login.ssoLogin.title',
'sso-callback': 'page.login.ssoLogin.title',
'code-login': 'page.login.codeLogin.title',
register: 'page.login.register.title',
'reset-pwd': 'page.login.resetPwd.title',

View File

@ -5,3 +5,7 @@ export enum SetupStoreId {
Route = 'route-store',
Tab = 'tab-store'
}
export enum SsoAuthor {
Authing = 'authing'
}

View File

@ -52,7 +52,7 @@ export function useRouterPush(inSetup = true) {
* @param redirectUrl The redirect url, if not specified, it will be the current route fullPath
*/
async function toLogin(loginModule?: UnionKey.LoginModule, redirectUrl?: string) {
const module = loginModule || 'pwd-login';
const module = loginModule || 'sso-login';
const options: RouterPushOptions = {
params: {

View File

@ -1,18 +1,18 @@
<script setup lang="ts">
import { computed } from 'vue';
import type { VNode } from 'vue';
import { useAuthStore } from '@/store/modules/auth';
import { useRouterPush } from '@/hooks/common/router';
import { useSvgIcon } from '@/hooks/common/icon';
import { $t } from '@/locales';
import type {VNode} from 'vue';
import {computed} from 'vue';
import {useAuthStore} from '@/store/modules/auth';
import {useRouterPush} from '@/hooks/common/router';
import {useSvgIcon} from '@/hooks/common/icon';
import {$t} from '@/locales';
defineOptions({
name: 'UserAvatar'
});
const authStore = useAuthStore();
const { routerPushByKey, toLogin } = useRouterPush();
const { SvgIconVNode } = useSvgIcon();
const {routerPushByKey, toLogin} = useRouterPush();
const {SvgIconVNode} = useSvgIcon();
function loginOrRegister() {
toLogin();
@ -22,21 +22,21 @@ type DropdownKey = 'user-center' | 'logout';
type DropdownOption =
| {
key: DropdownKey;
label: string;
icon?: () => VNode;
}
key: DropdownKey;
label: string;
icon?: () => VNode;
}
| {
type: 'divider';
key: string;
};
type: 'divider';
key: string;
};
const options = computed(() => {
const opts: DropdownOption[] = [
{
label: $t('common.userCenter'),
key: 'user-center',
icon: SvgIconVNode({ icon: 'ph:user-circle', fontSize: 18 })
icon: SvgIconVNode({icon: 'ph:user-circle', fontSize: 18})
},
{
type: 'divider',
@ -45,7 +45,7 @@ const options = computed(() => {
{
label: $t('common.logout'),
key: 'logout',
icon: SvgIconVNode({ icon: 'ph:sign-out', fontSize: 18 })
icon: SvgIconVNode({icon: 'ph:sign-out', fontSize: 18})
}
];
@ -59,7 +59,7 @@ function logout() {
positiveText: $t('common.confirm'),
negativeText: $t('common.cancel'),
onPositiveClick: () => {
authStore.resetStore();
authStore.logoutSso();
}
});
}
@ -80,7 +80,7 @@ function handleDropdown(key: DropdownKey) {
<NDropdown v-else placement="bottom" trigger="click" :options="options" @select="handleDropdown">
<div>
<ButtonIcon>
<SvgIcon icon="ph:user-circle" class="text-icon-large" />
<SvgIcon icon="ph:user-circle" class="text-icon-large"/>
<span class="text-16px font-medium">{{ authStore.userInfo.userName }}</span>
</ButtonIcon>
</div>

View File

@ -193,6 +193,9 @@ const local: App.I18n.Schema = {
admin: 'Admin',
user: 'User'
},
ssoLogin: {
title: 'SSO Login',
},
codeLogin: {
title: 'Verification Code Login',
getCode: 'Get verification code',

View File

@ -193,6 +193,9 @@ const local: App.I18n.Schema = {
admin: '管理员',
user: '普通用户'
},
ssoLogin: {
title: 'SSO登录',
},
codeLogin: {
title: '验证码登录',
getCode: '获取验证码',

View File

@ -181,7 +181,7 @@ export const generatedRoutes: GeneratedRoute[] = [
},
{
name: 'login',
path: '/login/:module(pwd-login|code-login|register|reset-pwd|bind-wechat)?',
path: '/login/:module(pwd-login|code-login|register|reset-pwd|bind-wechat|sso-login|sso-callback)?/:author?',
component: 'layout.blank$view.login',
props: true,
meta: {

View File

@ -162,7 +162,7 @@ const routeMap: RouteMap = {
"function_tab": "/function/tab",
"function_toggle-auth": "/function/toggle-auth",
"home": "/home",
"login": "/login/:module(pwd-login|code-login|register|reset-pwd|bind-wechat)?",
"login": "/login/:module(pwd-login|code-login|register|reset-pwd|bind-wechat|sso-login|sso-callback)?/:author?",
"manage": "/manage",
"manage_menu": "/manage/menu",
"manage_role": "/manage/role",

View File

@ -30,7 +30,7 @@ export function createRouteGuard(router: Router) {
const loginRoute: RouteKey = 'login';
const noAuthorizationRoute: RouteKey = '403';
const isLogin = Boolean(localStg.get('token'));
const isLogin = Boolean(await checkLogin());
const needLogin = !to.meta.constant;
const routeRoles = to.meta.roles || [];
@ -182,12 +182,21 @@ async function initRoute(to: RouteLocationNormalized): Promise<RouteLocationRaw
return null;
}
async function checkLogin() {
return await useAuthStore().checkLogin();
}
function handleRouteSwitch(to: RouteLocationNormalized, from: RouteLocationNormalized, next: NavigationGuardNext) {
// route with href
if (to.meta.href) {
window.open(to.meta.href, '_blank');
next({ path: from.fullPath, replace: true, query: from.query, hash: to.hash });
next({
path: from.fullPath,
replace: true,
query: from.query,
hash: to.hash
});
return;
}

View File

@ -1,3 +1,4 @@
export * from './auth';
export * from './route';
export * from './system-manage';
export * from './sso'

31
src/service/api/sso.ts Normal file
View File

@ -0,0 +1,31 @@
import {requestCoco} from '../request';
export function fetchLoginSsoUrl(author: string) {
return requestCoco<string>({
url: `/auth/login/${author}`,
method: 'get'
});
}
export function doLoginSso(author: string, code: string, state: string) {
return requestCoco<string>({
url: `/auth/login/${author}/callback`,
method: 'get',
params: {
code,
state
}
});
}
export function doLogoutSso(author: string) {
return requestCoco<string>({url: `/logout/${author}`});
}
export function fetchLoginSsoCheck(author: string) {
return requestCoco<boolean>({url: `/auth/login/${author}/check`});
}
export function fetchSsoUserInfo(author: string) {
return requestCoco<Api.Auth.UserInfo>({url: `/userInfo/${author}`});
}

View File

@ -0,0 +1,7 @@
import type {ErrorCodeHandle} from '~/packages/axios';
import {actionIdempotent} from '@/service/request/action';
export const codeActions: Map<Api.ErrorCode.Code, ErrorCodeHandle> = new Map([
['1', actionIdempotent],
['2', actionIdempotent]
]);

View File

@ -0,0 +1,24 @@
import type {ErrorCodeHandle} from '~/packages/axios';
export const actionIdempotent = createAction({
async handle(response, instance) {
return response;
}
});
export const action2 = createAction({
async handle(response, instance) {
return null;
}
});
function createAction(options?: Partial<ErrorCodeHandle>) {
const opts: ErrorCodeHandle<any> = {
handle: async () => {
}
};
Object.assign(opts, options);
return opts;
}

View File

@ -1,13 +1,14 @@
import type { AxiosResponse } from 'axios';
import { BACKEND_ERROR_CODE, createFlatRequest, createRequest } from '@sa/axios';
import { useAuthStore } from '@/store/modules/auth';
import { localStg } from '@/utils/storage';
import { getServiceBaseURL } from '@/utils/service';
import { $t } from '@/locales';
import { handleRefreshToken } from './shared';
import type {AxiosResponse} from 'axios';
import {BACKEND_ERROR_CODE, createFlatRequest, createRequest} from '@sa/axios';
import {useAuthStore} from '@/store/modules/auth';
import {localStg} from '@/utils/storage';
import {getServiceBaseURL} from '@/utils/service';
import {$t} from '@/locales';
import {codeActions} from '@/service/request/action-type';
import {handleRefreshToken} from './shared';
const isHttpProxy = import.meta.env.DEV && import.meta.env.VITE_HTTP_PROXY === 'Y';
const { baseURL, otherBaseURL } = getServiceBaseURL(import.meta.env, isHttpProxy);
const {baseURL, otherBaseURL} = getServiceBaseURL(import.meta.env, isHttpProxy);
interface InstanceState {
/** whether the request is refreshing token */
@ -23,12 +24,12 @@ export const request = createFlatRequest<App.Service.Response, InstanceState>(
},
{
async onRequest(config) {
const { headers } = config;
const {headers} = config;
// set token
const token = localStg.get('token');
const Authorization = token ? `Bearer ${token}` : null;
Object.assign(headers, { Authorization });
Object.assign(headers, {Authorization});
return config;
},
@ -133,12 +134,12 @@ export const demoRequest = createRequest<App.Service.DemoResponse>(
},
{
async onRequest(config) {
const { headers } = config;
const {headers} = config;
// set token
const token = localStg.get('token');
const Authorization = token ? `Bearer ${token}` : null;
Object.assign(headers, { Authorization });
Object.assign(headers, {Authorization});
return config;
},
@ -168,3 +169,60 @@ export const demoRequest = createRequest<App.Service.DemoResponse>(
}
}
);
export const requestCoco = createFlatRequest<App.Service.Response, InstanceState>(
{
baseURL
},
{
async onRequest(config) {
// const { headers } = config;
// set token
// const token = localStg.get('token');
// const Authorization = token ? `Bearer ${token}` : null;
// Object.assign(headers, { Authorization });
return config;
},
isBackendSuccess(response) {
// when the backend response code is "0000"(default), it means the request is success
// to change this logic by yourself, you can modify the `VITE_SERVICE_SUCCESS_CODE` in `.env` file
return String(response.data.code) === String(import.meta.env.VITE_SERVICE_SUCCESS_CODE);
},
async onBackendFail(response, instance) {
const key: string = String(response.data.code);
const act = codeActions.get(key as Api.ErrorCode.Code);
return act?.handle(response, instance) as Promise<AxiosResponse | any>;
},
transformBackendResponse(response) {
return response.data.data;
},
onError(error) {
// when the request is fail, you can show error message
let message = error.message;
let backendErrorCode = '';
// get backend error message and code
if (error.code === BACKEND_ERROR_CODE) {
message = error.response?.data?.msg || message;
backendErrorCode = error.response?.data?.code || '';
}
// the error message is displayed in the modal
const modalLogoutCodes = import.meta.env.VITE_SERVICE_MODAL_LOGOUT_CODES?.split(',') || [];
if (modalLogoutCodes.includes(backendErrorCode)) {
return;
}
// when the token is expired, refresh token and retry request, so no need to show error message
const expiredTokenCodes = import.meta.env.VITE_SERVICE_EXPIRED_TOKEN_CODES?.split(',') || [];
if (expiredTokenCodes.includes(backendErrorCode)) {
return;
}
window.$message?.error?.(message);
}
}
);

View File

@ -1,9 +1,16 @@
import { computed, reactive, ref } from 'vue';
import { defineStore } from 'pinia';
import { useLoading } from '@sa/hooks';
import { SetupStoreId } from '@/enum';
import {SetupStoreId, SsoAuthor} from '@/enum';
import { useRouterPush } from '@/hooks/common/router';
import { fetchGetUserInfo, fetchLogin } from '@/service/api';
import {
doLoginSso,
doLogoutSso,
fetchGetUserInfo,
fetchLogin,
fetchLoginSsoCheck,
fetchLoginSsoUrl, fetchSsoUserInfo
} from '@/service/api';
import { localStg } from '@/utils/storage';
import { $t } from '@/locales';
import { useRouteStore } from '../route';
@ -101,6 +108,85 @@ export const useAuthStore = defineStore(SetupStoreId.Auth, () => {
return false;
}
async function fetchSsoUrl(author: string) {
startLoading();
const {data: url, error} = await fetchLoginSsoUrl(author);
if (error) {
resetStore();
endLoading();
return null;
}
// 若发生幂等
if (!url) {
const res = await fetchUser(author);
if (res) {
await redirectFromLogin();
endLoading();
if (routeStore.isInitAuthRoute) {
window.$notification?.success({
title: $t('page.login.common.loginSuccess'),
content: $t('page.login.common.welcomeBack', {userName: userInfo.userName}),
duration: 4500
});
}
}
}
endLoading();
return url;
}
async function loginSso(author: string, code: string, state: string) {
startLoading();
const {error} = await doLoginSso(author, code, state);
if (!error) {
await fetchUser(author);
endLoading();
return true;
}
await resetStore();
endLoading();
return false;
}
async function fetchUser(author: string) {
const {data: info, error: err} = await fetchSsoUserInfo(author);
if (!err && info !== null) {
info.roles = ['R_SUPER'];
localStg.set('token', info.userId);
// 2. store user info
localStg.set('userInfo', info);
localStg.set('author', author);
// 3. update auth route
token.value = info.userId;
Object.assign(userInfo, info);
await routeStore.initAuthRoute();
return true;
}
return false;
}
async function logoutSso() {
await resetStore();
await doLogoutSso(localStg.get('author') || '');
}
async function checkLogin() {
if (!token.value) {
return false;
}
const {data: check, error} = await fetchLoginSsoCheck(localStg.get('author') || SsoAuthor.Authing);
if (!error) {
return check;
}
return false;
}
return {
token,
userInfo,
@ -108,6 +194,10 @@ export const useAuthStore = defineStore(SetupStoreId.Auth, () => {
isLogin,
loginLoading,
resetStore,
login
login,
fetchSsoUrl,
loginSso,
logoutSso,
checkLogin
};
});

View File

@ -4,6 +4,9 @@
* All backend api type
*/
declare namespace Api {
namespace ErrorCode {
type Code = '1' | '2';
}
namespace Common {
/** common params of paginating */
interface PaginatingCommonParams {

View File

@ -373,6 +373,9 @@ declare namespace App {
admin: string;
user: string;
};
ssoLogin: {
title: string;
};
codeLogin: {
title: string;
getCode: string;

View File

@ -36,7 +36,7 @@ declare module "@elegant-router/types" {
"function_tab": "/function/tab";
"function_toggle-auth": "/function/toggle-auth";
"home": "/home";
"login": "/login/:module(pwd-login|code-login|register|reset-pwd|bind-wechat)?";
"login": "/login/:module(pwd-login|code-login|register|reset-pwd|bind-wechat|sso-login|sso-callback)?/:author?";
"manage": "/manage";
"manage_menu": "/manage/menu";
"manage_role": "/manage/role";

View File

@ -12,6 +12,7 @@ declare namespace StorageType {
interface Local {
/** The i18n language */
lang: App.I18n.LangType;
author: string;
/** The token */
token: string;
/** The refresh token */

View File

@ -9,7 +9,14 @@ declare namespace UnionKey {
* - reset-pwd: reset password
* - bind-wechat: bind wechat
*/
type LoginModule = 'pwd-login' | 'code-login' | 'register' | 'reset-pwd' | 'bind-wechat';
type LoginModule =
'pwd-login'
| 'code-login'
| 'register'
| 'reset-pwd'
| 'bind-wechat'
| 'sso-login'
| 'sso-callback';
/** Theme scheme */
type ThemeScheme = 'light' | 'dark' | 'auto';

View File

@ -1,16 +1,18 @@
<script setup lang="ts">
import { computed } from 'vue';
import type { Component } from 'vue';
import { getColorPalette, mixColor } from '@sa/utils';
import { $t } from '@/locales';
import { useAppStore } from '@/store/modules/app';
import { useThemeStore } from '@/store/modules/theme';
import { loginModuleRecord } from '@/constants/app';
import type {Component} from 'vue';
import {computed} from 'vue';
import {getColorPalette, mixColor} from '@sa/utils';
import {$t} from '@/locales';
import {useAppStore} from '@/store/modules/app';
import {useThemeStore} from '@/store/modules/theme';
import {loginModuleRecord} from '@/constants/app';
import PwdLogin from './modules/pwd-login.vue';
import CodeLogin from './modules/code-login.vue';
import Register from './modules/register.vue';
import ResetPwd from './modules/reset-pwd.vue';
import BindWechat from './modules/bind-wechat.vue';
import SsoLogin from "@/views/_builtin/login/modules/sso-login.vue";
import SsoCallback from "@/views/_builtin/login/modules/sso-callback.vue";
interface Props {
/** The login module */
@ -28,14 +30,16 @@ interface LoginModule {
}
const moduleMap: Record<UnionKey.LoginModule, LoginModule> = {
'pwd-login': { label: loginModuleRecord['pwd-login'], component: PwdLogin },
'code-login': { label: loginModuleRecord['code-login'], component: CodeLogin },
register: { label: loginModuleRecord.register, component: Register },
'reset-pwd': { label: loginModuleRecord['reset-pwd'], component: ResetPwd },
'bind-wechat': { label: loginModuleRecord['bind-wechat'], component: BindWechat }
'pwd-login': {label: loginModuleRecord['pwd-login'], component: PwdLogin},
'sso-login': {label: loginModuleRecord['sso-login'], component: SsoLogin},
'sso-callback': {label: loginModuleRecord['sso-callback'], component: SsoCallback},
'code-login': {label: loginModuleRecord['code-login'], component: CodeLogin},
register: {label: loginModuleRecord.register, component: Register},
'reset-pwd': {label: loginModuleRecord['reset-pwd'], component: ResetPwd},
'bind-wechat': {label: loginModuleRecord['bind-wechat'], component: BindWechat}
};
const activeModule = computed(() => moduleMap[props.module || 'pwd-login']);
const activeModule = computed(() => moduleMap[props.module || 'sso-login']);
const bgThemeColor = computed(() =>
themeStore.darkMode ? getColorPalette(themeStore.themeColor, 7) : themeStore.themeColor
@ -52,11 +56,11 @@ const bgColor = computed(() => {
<template>
<div class="relative size-full flex-center overflow-hidden" :style="{ backgroundColor: bgColor }">
<WaveBg :theme-color="bgThemeColor" />
<WaveBg :theme-color="bgThemeColor"/>
<NCard :bordered="false" class="relative z-4 w-auto rd-12px">
<div class="w-400px lt-sm:w-300px">
<header class="flex-y-center justify-between">
<SystemLogo class="text-64px text-primary lt-sm:text-48px" />
<SystemLogo class="text-64px text-primary lt-sm:text-48px"/>
<h3 class="text-28px text-primary font-500 lt-sm:text-22px">{{ $t('system.title') }}</h3>
<div class="i-flex-col">
<ThemeSchemaSwitch
@ -77,7 +81,7 @@ const bgColor = computed(() => {
<h3 class="text-18px text-primary font-medium">{{ $t(activeModule.label) }}</h3>
<div class="pt-24px">
<Transition :name="themeStore.page.animateMode" mode="out-in" appear>
<component :is="activeModule.component" />
<component :is="activeModule.component"/>
</Transition>
</div>
</main>

View File

@ -1,17 +1,17 @@
<script setup lang="ts">
import { computed, reactive } from 'vue';
import { $t } from '@/locales';
import { useRouterPush } from '@/hooks/common/router';
import { useFormRules, useNaiveForm } from '@/hooks/common/form';
import { useCaptcha } from '@/hooks/business/captcha';
import {computed, reactive} from 'vue';
import {$t} from '@/locales';
import {useRouterPush} from '@/hooks/common/router';
import {useFormRules, useNaiveForm} from '@/hooks/common/form';
import {useCaptcha} from '@/hooks/business/captcha';
defineOptions({
name: 'CodeLogin'
});
const { toggleLoginModule } = useRouterPush();
const { formRef, validate } = useNaiveForm();
const { label, isCounting, loading, getCaptcha } = useCaptcha();
const {toggleLoginModule} = useRouterPush();
const {formRef, validate} = useNaiveForm();
const {label, isCounting, loading, getCaptcha} = useCaptcha();
interface FormModel {
phone: string;
@ -24,7 +24,7 @@ const model: FormModel = reactive({
});
const rules = computed<Record<keyof FormModel, App.Global.FormRule[]>>(() => {
const { formRules } = useFormRules();
const {formRules} = useFormRules();
return {
phone: formRules.phone,
@ -40,27 +40,27 @@ async function handleSubmit() {
</script>
<template>
<NForm ref="formRef" :model="model" :rules="rules" size="large" :show-label="false">
<NFormItem path="phone">
<NInput v-model:value="model.phone" :placeholder="$t('page.login.common.phonePlaceholder')" />
</NFormItem>
<NFormItem path="code">
<div class="w-full flex-y-center gap-16px">
<NInput v-model:value="model.code" :placeholder="$t('page.login.common.codePlaceholder')" />
<NButton size="large" :disabled="isCounting" :loading="loading" @click="getCaptcha(model.phone)">
{{ label }}
</NButton>
</div>
</NFormItem>
<NSpace vertical :size="18" class="w-full">
<NButton type="primary" size="large" round block @click="handleSubmit">
{{ $t('common.confirm') }}
</NButton>
<NButton size="large" round block @click="toggleLoginModule('pwd-login')">
{{ $t('page.login.common.back') }}
</NButton>
</NSpace>
</NForm>
<!-- <NForm ref="formRef" :model="model" :rules="rules" size="large" :show-label="false">-->
<!-- <NFormItem path="phone">-->
<!-- <NInput v-model:value="model.phone" :placeholder="$t('page.login.common.phonePlaceholder')" />-->
<!-- </NFormItem>-->
<!-- <NFormItem path="code">-->
<!-- <div class="w-full flex-y-center gap-16px">-->
<!-- <NInput v-model:value="model.code" :placeholder="$t('page.login.common.codePlaceholder')" />-->
<!-- <NButton size="large" :disabled="isCounting" :loading="loading" @click="getCaptcha(model.phone)">-->
<!-- {{ label }}-->
<!-- </NButton>-->
<!-- </div>-->
<!-- </NFormItem>-->
<!-- <NSpace vertical :size="18" class="w-full">-->
<!-- <NButton type="primary" size="large" round block @click="handleSubmit">-->
<!-- {{ $t('common.confirm') }}-->
<!-- </NButton>-->
<!-- <NButton size="large" round block @click="toggleLoginModule('pwd-login')">-->
<!-- {{ $t('page.login.common.back') }}-->
<!-- </NButton>-->
<!-- </NSpace>-->
<!-- </NForm>-->
</template>
<style scoped></style>

View File

@ -1,18 +1,17 @@
<script setup lang="ts">
import { computed, reactive } from 'vue';
import { $t } from '@/locales';
import { loginModuleRecord } from '@/constants/app';
import { useRouterPush } from '@/hooks/common/router';
import { useFormRules, useNaiveForm } from '@/hooks/common/form';
import { useAuthStore } from '@/store/modules/auth';
import {computed, reactive} from 'vue';
import {$t} from '@/locales';
import {useRouterPush} from '@/hooks/common/router';
import {useFormRules, useNaiveForm} from '@/hooks/common/form';
import {useAuthStore} from '@/store/modules/auth';
defineOptions({
name: 'PwdLogin'
});
const authStore = useAuthStore();
const { toggleLoginModule } = useRouterPush();
const { formRef, validate } = useNaiveForm();
const {toggleLoginModule} = useRouterPush();
const {formRef, validate} = useNaiveForm();
interface FormModel {
userName: string;
@ -26,7 +25,7 @@ const model: FormModel = reactive({
const rules = computed<Record<keyof FormModel, App.Global.FormRule[]>>(() => {
// inside computed to make locale reactive, if not apply i18n, you can define it without computed
const { formRules } = useFormRules();
const {formRules} = useFormRules();
return {
userName: formRules.userName,
@ -75,44 +74,44 @@ async function handleAccountLogin(account: Account) {
</script>
<template>
<NForm ref="formRef" :model="model" :rules="rules" size="large" :show-label="false">
<NFormItem path="userName">
<NInput v-model:value="model.userName" :placeholder="$t('page.login.common.userNamePlaceholder')" />
</NFormItem>
<NFormItem path="password">
<NInput
v-model:value="model.password"
type="password"
show-password-on="click"
:placeholder="$t('page.login.common.passwordPlaceholder')"
/>
</NFormItem>
<NSpace vertical :size="24">
<div class="flex-y-center justify-between">
<NCheckbox>{{ $t('page.login.pwdLogin.rememberMe') }}</NCheckbox>
<NButton quaternary @click="toggleLoginModule('reset-pwd')">
{{ $t('page.login.pwdLogin.forgetPassword') }}
</NButton>
</div>
<NButton type="primary" size="large" round block :loading="authStore.loginLoading" @click="handleSubmit">
{{ $t('common.confirm') }}
</NButton>
<div class="flex-y-center justify-between gap-12px">
<NButton class="flex-1" block @click="toggleLoginModule('code-login')">
{{ $t(loginModuleRecord['code-login']) }}
</NButton>
<NButton class="flex-1" block @click="toggleLoginModule('register')">
{{ $t(loginModuleRecord.register) }}
</NButton>
</div>
<NDivider class="text-14px text-#666 !m-0">{{ $t('page.login.pwdLogin.otherAccountLogin') }}</NDivider>
<div class="flex-center gap-12px">
<NButton v-for="item in accounts" :key="item.key" type="primary" @click="handleAccountLogin(item)">
{{ item.label }}
</NButton>
</div>
</NSpace>
</NForm>
<!-- <NForm ref="formRef" :model="model" :rules="rules" size="large" :show-label="false">-->
<!-- <NFormItem path="userName">-->
<!-- <NInput v-model:value="model.userName" :placeholder="$t('page.login.common.userNamePlaceholder')" />-->
<!-- </NFormItem>-->
<!-- <NFormItem path="password">-->
<!-- <NInput-->
<!-- v-model:value="model.password"-->
<!-- type="password"-->
<!-- show-password-on="click"-->
<!-- :placeholder="$t('page.login.common.passwordPlaceholder')"-->
<!-- />-->
<!-- </NFormItem>-->
<!-- <NSpace vertical :size="24">-->
<!-- <div class="flex-y-center justify-between">-->
<!-- <NCheckbox>{{ $t('page.login.pwdLogin.rememberMe') }}</NCheckbox>-->
<!-- <NButton quaternary @click="toggleLoginModule('reset-pwd')">-->
<!-- {{ $t('page.login.pwdLogin.forgetPassword') }}-->
<!-- </NButton>-->
<!-- </div>-->
<!-- <NButton type="primary" size="large" round block :loading="authStore.loginLoading" @click="handleSubmit">-->
<!-- {{ $t('common.confirm') }}-->
<!-- </NButton>-->
<!-- <div class="flex-y-center justify-between gap-12px">-->
<!-- <NButton class="flex-1" block @click="toggleLoginModule('code-login')">-->
<!-- {{ $t(loginModuleRecord['code-login']) }}-->
<!-- </NButton>-->
<!-- <NButton class="flex-1" block @click="toggleLoginModule('register')">-->
<!-- {{ $t(loginModuleRecord.register) }}-->
<!-- </NButton>-->
<!-- </div>-->
<!-- <NDivider class="text-14px text-#666 !m-0">{{ $t('page.login.pwdLogin.otherAccountLogin') }}</NDivider>-->
<!-- <div class="flex-center gap-12px">-->
<!-- <NButton v-for="item in accounts" :key="item.key" type="primary" @click="handleAccountLogin(item)">-->
<!-- {{ item.label }}-->
<!-- </NButton>-->
<!-- </div>-->
<!-- </NSpace>-->
<!-- </NForm>-->
</template>
<style scoped></style>

View File

@ -1,17 +1,17 @@
<script setup lang="ts">
import { computed, reactive } from 'vue';
import { $t } from '@/locales';
import { useRouterPush } from '@/hooks/common/router';
import { useFormRules, useNaiveForm } from '@/hooks/common/form';
import { useCaptcha } from '@/hooks/business/captcha';
import {computed, reactive} from 'vue';
import {$t} from '@/locales';
import {useRouterPush} from '@/hooks/common/router';
import {useFormRules, useNaiveForm} from '@/hooks/common/form';
import {useCaptcha} from '@/hooks/business/captcha';
defineOptions({
name: 'CodeLogin'
});
const { toggleLoginModule } = useRouterPush();
const { formRef, validate } = useNaiveForm();
const { label, isCounting, loading, getCaptcha } = useCaptcha();
const {toggleLoginModule} = useRouterPush();
const {formRef, validate} = useNaiveForm();
const {label, isCounting, loading, getCaptcha} = useCaptcha();
interface FormModel {
phone: string;
@ -28,7 +28,7 @@ const model: FormModel = reactive({
});
const rules = computed<Record<keyof FormModel, App.Global.FormRule[]>>(() => {
const { formRules, createConfirmPwdRule } = useFormRules();
const {formRules, createConfirmPwdRule} = useFormRules();
return {
phone: formRules.phone,
@ -46,43 +46,43 @@ async function handleSubmit() {
</script>
<template>
<NForm ref="formRef" :model="model" :rules="rules" size="large" :show-label="false">
<NFormItem path="phone">
<NInput v-model:value="model.phone" :placeholder="$t('page.login.common.phonePlaceholder')" />
</NFormItem>
<NFormItem path="code">
<div class="w-full flex-y-center gap-16px">
<NInput v-model:value="model.code" :placeholder="$t('page.login.common.codePlaceholder')" />
<NButton size="large" :disabled="isCounting" :loading="loading" @click="getCaptcha(model.phone)">
{{ label }}
</NButton>
</div>
</NFormItem>
<NFormItem path="password">
<NInput
v-model:value="model.password"
type="password"
show-password-on="click"
:placeholder="$t('page.login.common.passwordPlaceholder')"
/>
</NFormItem>
<NFormItem path="confirmPassword">
<NInput
v-model:value="model.confirmPassword"
type="password"
show-password-on="click"
:placeholder="$t('page.login.common.confirmPasswordPlaceholder')"
/>
</NFormItem>
<NSpace vertical :size="18" class="w-full">
<NButton type="primary" size="large" round block @click="handleSubmit">
{{ $t('common.confirm') }}
</NButton>
<NButton size="large" round block @click="toggleLoginModule('pwd-login')">
{{ $t('page.login.common.back') }}
</NButton>
</NSpace>
</NForm>
<!-- <NForm ref="formRef" :model="model" :rules="rules" size="large" :show-label="false">-->
<!-- <NFormItem path="phone">-->
<!-- <NInput v-model:value="model.phone" :placeholder="$t('page.login.common.phonePlaceholder')" />-->
<!-- </NFormItem>-->
<!-- <NFormItem path="code">-->
<!-- <div class="w-full flex-y-center gap-16px">-->
<!-- <NInput v-model:value="model.code" :placeholder="$t('page.login.common.codePlaceholder')" />-->
<!-- <NButton size="large" :disabled="isCounting" :loading="loading" @click="getCaptcha(model.phone)">-->
<!-- {{ label }}-->
<!-- </NButton>-->
<!-- </div>-->
<!-- </NFormItem>-->
<!-- <NFormItem path="password">-->
<!-- <NInput-->
<!-- v-model:value="model.password"-->
<!-- type="password"-->
<!-- show-password-on="click"-->
<!-- :placeholder="$t('page.login.common.passwordPlaceholder')"-->
<!-- />-->
<!-- </NFormItem>-->
<!-- <NFormItem path="confirmPassword">-->
<!-- <NInput-->
<!-- v-model:value="model.confirmPassword"-->
<!-- type="password"-->
<!-- show-password-on="click"-->
<!-- :placeholder="$t('page.login.common.confirmPasswordPlaceholder')"-->
<!-- />-->
<!-- </NFormItem>-->
<!-- <NSpace vertical :size="18" class="w-full">-->
<!-- <NButton type="primary" size="large" round block @click="handleSubmit">-->
<!-- {{ $t('common.confirm') }}-->
<!-- </NButton>-->
<!-- <NButton size="large" round block @click="toggleLoginModule('pwd-login')">-->
<!-- {{ $t('page.login.common.back') }}-->
<!-- </NButton>-->
<!-- </NSpace>-->
<!-- </NForm>-->
</template>
<style scoped></style>

View File

@ -1,15 +1,15 @@
<script setup lang="ts">
import { computed, reactive } from 'vue';
import { $t } from '@/locales';
import { useRouterPush } from '@/hooks/common/router';
import { useFormRules, useNaiveForm } from '@/hooks/common/form';
import {computed, reactive} from 'vue';
import {$t} from '@/locales';
import {useRouterPush} from '@/hooks/common/router';
import {useFormRules, useNaiveForm} from '@/hooks/common/form';
defineOptions({
name: 'ResetPwd'
});
const { toggleLoginModule } = useRouterPush();
const { formRef, validate } = useNaiveForm();
const {toggleLoginModule} = useRouterPush();
const {formRef, validate} = useNaiveForm();
interface FormModel {
phone: string;
@ -28,7 +28,7 @@ const model: FormModel = reactive({
type RuleRecord = Partial<Record<keyof FormModel, App.Global.FormRule[]>>;
const rules = computed<RuleRecord>(() => {
const { formRules, createConfirmPwdRule } = useFormRules();
const {formRules, createConfirmPwdRule} = useFormRules();
return {
phone: formRules.phone,
@ -45,38 +45,38 @@ async function handleSubmit() {
</script>
<template>
<NForm ref="formRef" :model="model" :rules="rules" size="large" :show-label="false">
<NFormItem path="phone">
<NInput v-model:value="model.phone" :placeholder="$t('page.login.common.phonePlaceholder')" />
</NFormItem>
<NFormItem path="code">
<NInput v-model:value="model.code" :placeholder="$t('page.login.common.codePlaceholder')" />
</NFormItem>
<NFormItem path="password">
<NInput
v-model:value="model.password"
type="password"
show-password-on="click"
:placeholder="$t('page.login.common.passwordPlaceholder')"
/>
</NFormItem>
<NFormItem path="confirmPassword">
<NInput
v-model:value="model.confirmPassword"
type="password"
show-password-on="click"
:placeholder="$t('page.login.common.confirmPasswordPlaceholder')"
/>
</NFormItem>
<NSpace vertical :size="18" class="w-full">
<NButton type="primary" size="large" round block @click="handleSubmit">
{{ $t('common.confirm') }}
</NButton>
<NButton size="large" round block @click="toggleLoginModule('pwd-login')">
{{ $t('page.login.common.back') }}
</NButton>
</NSpace>
</NForm>
<!-- <NForm ref="formRef" :model="model" :rules="rules" size="large" :show-label="false">-->
<!-- <NFormItem path="phone">-->
<!-- <NInput v-model:value="model.phone" :placeholder="$t('page.login.common.phonePlaceholder')" />-->
<!-- </NFormItem>-->
<!-- <NFormItem path="code">-->
<!-- <NInput v-model:value="model.code" :placeholder="$t('page.login.common.codePlaceholder')" />-->
<!-- </NFormItem>-->
<!-- <NFormItem path="password">-->
<!-- <NInput-->
<!-- v-model:value="model.password"-->
<!-- type="password"-->
<!-- show-password-on="click"-->
<!-- :placeholder="$t('page.login.common.passwordPlaceholder')"-->
<!-- />-->
<!-- </NFormItem>-->
<!-- <NFormItem path="confirmPassword">-->
<!-- <NInput-->
<!-- v-model:value="model.confirmPassword"-->
<!-- type="password"-->
<!-- show-password-on="click"-->
<!-- :placeholder="$t('page.login.common.confirmPasswordPlaceholder')"-->
<!-- />-->
<!-- </NFormItem>-->
<!-- <NSpace vertical :size="18" class="w-full">-->
<!-- <NButton type="primary" size="large" round block @click="handleSubmit">-->
<!-- {{ $t('common.confirm') }}-->
<!-- </NButton>-->
<!-- <NButton size="large" round block @click="toggleLoginModule('pwd-login')">-->
<!-- {{ $t('page.login.common.back') }}-->
<!-- </NButton>-->
<!-- </NSpace>-->
<!-- </NForm>-->
</template>
<style scoped></style>

View File

@ -0,0 +1,68 @@
<script setup lang="ts">
import {onBeforeMount} from 'vue';
import {useRoute} from 'vue-router';
import {useRouterPush} from '@/hooks/common/router';
import {useAuthStore} from '@/store/modules/auth';
import {$t} from '@/locales';
import {SsoAuthor} from '@/enum';
import {useRouteStore} from '@/store/modules/route';
defineOptions({
name: 'SsoCallback'
});
const route = useRoute();
const routeStore = useRouteStore();
const routerPush = useRouterPush();
const authStore = useAuthStore();
onBeforeMount(async () => {
const success = await authStore.loginSso(
String(route.params.author),
String(route.query.code),
String(route.query.state)
);
if (success) {
routerPush.redirectFromLogin();
if (routeStore.isInitAuthRoute) {
window.$notification?.success({
title: $t('page.login.common.loginSuccess'),
content: $t('page.login.common.welcomeBack', {userName: authStore.userInfo.userName}),
duration: 4500
});
}
} else {
routerPush.toLogin();
}
});
const {toggleLoginModule} = useRouterPush();
async function toggleSsoLogin(author: string) {
const url = await authStore.fetchSsoUrl(author);
if (url) {
window.open(url, '_self');
} else {
routerPush.toLogin();
}
}
</script>
<template>
<NSpace vertical :size="18" class="w-full">
<NGrid cols="s:1 m:2 l:4" responsive="screen" :x-gap="16" :y-gap="16">
<NGi v-for="item in SsoAuthor" :key="item">
<NButton size="large" type="info" round secondary block @click="toggleSsoLogin(item)">
{{ item }}
</NButton>
</NGi>
</NGrid>
<NButton size="large" round block @click="toggleLoginModule('pwd-login')">
{{ $t('page.login.common.back') }}
</NButton>
</NSpace>
</template>
<style scoped></style>
<style scoped></style>

View File

@ -0,0 +1,43 @@
<script setup lang="ts">
import {useRouterPush} from '@/hooks/common/router';
import {SsoAuthor} from '@/enum';
import {useAuthStore} from '@/store/modules/auth';
defineOptions({
name: 'SsoLogin'
});
const {toggleLoginModule} = useRouterPush();
const ssoAuthor = SsoAuthor;
const authStore = useAuthStore();
const {toLogin} = useRouterPush();
async function toggleSsoLogin(author: string) {
const url = await authStore.fetchSsoUrl(author);
console.log(url)
if (url) {
window.open(url, '_self');
} else {
toLogin();
}
}
</script>
<template>
<NSpace vertical :size="18" class="w-full">
<NGrid cols="s:1 m:2 l:4" responsive="screen" :x-gap="16" :y-gap="16">
<NGi v-for="item in ssoAuthor" :key="item">
<NButton size="large" type="info" round secondary block @click="toggleSsoLogin(item)">
{{ item }}
</NButton>
</NGi>
</NGrid>
<!-- <NButton size="large" round block @click="toggleLoginModule('pwd-login')">-->
<!-- {{ $t('page.login.common.back') }}-->
<!-- </NButton>-->
</NSpace>
</template>
<style scoped></style>