mirror of
https://github.com/certd/certd.git
synced 2026-05-01 02:17:27 +08:00
chore: 优化oidc登录
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
import { PermissionException, ValidateException } from './exception/index.js';
|
import { PermissionException, ValidateException } from './exception/index.js';
|
||||||
import { In, Repository, SelectQueryBuilder } from 'typeorm';
|
import { FindOneOptions, In, Repository, SelectQueryBuilder } from 'typeorm';
|
||||||
import { Inject } from '@midwayjs/core';
|
import { Inject } from '@midwayjs/core';
|
||||||
import { TypeORMDataSourceManager } from '@midwayjs/typeorm';
|
import { TypeORMDataSourceManager } from '@midwayjs/typeorm';
|
||||||
import { EntityManager } from 'typeorm/entity-manager/EntityManager.js';
|
import { EntityManager } from 'typeorm/entity-manager/EntityManager.js';
|
||||||
@@ -238,4 +238,8 @@ export abstract class BaseService<T> {
|
|||||||
|
|
||||||
await this.delete(ids);
|
await this.delete(ids);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async findOne(options: FindOneOptions<T>) {
|
||||||
|
return await this.getRepository().findOne(options);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ export type AddonDefine = Registrable & {
|
|||||||
[key: string]: AddonInputDefine;
|
[key: string]: AddonInputDefine;
|
||||||
};
|
};
|
||||||
showTest?: boolean;
|
showTest?: boolean;
|
||||||
|
icon?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type AddonInstanceConfig = {
|
export type AddonInstanceConfig = {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<div id="userLayout" :class="['user-layout-wrapper']">
|
<div id="userLayout" :class="['user-layout-wrapper']">
|
||||||
<div class="login-container flex-center">
|
<div class="login-container flex justify-start">
|
||||||
<div class="user-layout-content flex-center flex-col">
|
<div class="user-layout-content flex-col justify-start">
|
||||||
<div class="top flex flex-col items-center justify-center">
|
<div class="top flex flex-col items-center justify-start">
|
||||||
<div class="header flex flex-row items-center">
|
<div class="header flex flex-row items-center">
|
||||||
<img :src="siteInfo.loginLogo" class="logo" alt="logo" />
|
<img :src="siteInfo.loginLogo" class="logo" alt="logo" />
|
||||||
<span class="title"></span>
|
<span class="title"></span>
|
||||||
@@ -10,8 +10,9 @@
|
|||||||
<div class="desc">{{ siteInfo.slogan }}</div>
|
<div class="desc">{{ siteInfo.slogan }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<router-view />
|
<div class="flex-1 flex flex-col justify-start items-center">
|
||||||
|
<router-view />
|
||||||
|
</div>
|
||||||
<div class="footer">
|
<div class="footer">
|
||||||
<div class="copyright">
|
<div class="copyright">
|
||||||
<span v-if="!settingStore.isComm">
|
<span v-if="!settingStore.isComm">
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ export default {
|
|||||||
passwordPlaceholder: "Please enter your password",
|
passwordPlaceholder: "Please enter your password",
|
||||||
mobilePlaceholder: "Please enter your mobile number",
|
mobilePlaceholder: "Please enter your mobile number",
|
||||||
loginButton: "Log In",
|
loginButton: "Log In",
|
||||||
|
bindButton: "Bind Account",
|
||||||
forgotPassword: "Forgot password?",
|
forgotPassword: "Forgot password?",
|
||||||
forgotAdminPassword: "Forgot admin password?",
|
forgotAdminPassword: "Forgot admin password?",
|
||||||
registerLink: "Register",
|
registerLink: "Register",
|
||||||
|
|||||||
@@ -760,6 +760,13 @@ export default {
|
|||||||
fixedCertExpireDays: "Fixed Cert Expire Days",
|
fixedCertExpireDays: "Fixed Cert Expire Days",
|
||||||
fixedCertExpireDaysHelper: "Fixed cert expiration days, helpful for table list progress bar display",
|
fixedCertExpireDaysHelper: "Fixed cert expiration days, helpful for table list progress bar display",
|
||||||
fixedCertExpireDaysRecommend: "Recommend 90",
|
fixedCertExpireDaysRecommend: "Recommend 90",
|
||||||
|
|
||||||
|
enableOauth: "Enable OAuth2 Login",
|
||||||
|
oauthEnabledHelper: "Whether to enable OAuth2 login",
|
||||||
|
oauthProviders: "OAuth2 Login Providers",
|
||||||
|
oauthType: "OAuth2 Login Type",
|
||||||
|
oauthConfig: "OAuth2 Login Config",
|
||||||
|
oauthProviderSelectorPlaceholder: "Please select OAuth2 login provider",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
modal: {
|
modal: {
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ export default {
|
|||||||
passwordPlaceholder: "请输入密码",
|
passwordPlaceholder: "请输入密码",
|
||||||
mobilePlaceholder: "请输入手机号",
|
mobilePlaceholder: "请输入手机号",
|
||||||
loginButton: "登录",
|
loginButton: "登录",
|
||||||
|
bindButton: "绑定账号",
|
||||||
forgotPassword: "忘记密码?",
|
forgotPassword: "忘记密码?",
|
||||||
forgotAdminPassword: "忘记管理员密码?",
|
forgotAdminPassword: "忘记管理员密码?",
|
||||||
registerLink: "注册",
|
registerLink: "注册",
|
||||||
|
|||||||
@@ -761,6 +761,13 @@ export default {
|
|||||||
fixedCertExpireDays: "固定证书有效期天数",
|
fixedCertExpireDays: "固定证书有效期天数",
|
||||||
fixedCertExpireDaysHelper: "固定证书有效期天数,有助于列表进度条整齐显示",
|
fixedCertExpireDaysHelper: "固定证书有效期天数,有助于列表进度条整齐显示",
|
||||||
fixedCertExpireDaysRecommend: "推荐90",
|
fixedCertExpireDaysRecommend: "推荐90",
|
||||||
|
|
||||||
|
enableOauth: "启用OAuth2登录",
|
||||||
|
oauthEnabledHelper: "是否启用OAuth2登录",
|
||||||
|
oauthProviders: "OAuth2登录提供商",
|
||||||
|
oauthType: "OAuth2登录类型",
|
||||||
|
oauthConfig: "OAuth2登录配置",
|
||||||
|
oauthProviderSelectorPlaceholder: "请选择OAuth2登录提供商",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
modal: {
|
modal: {
|
||||||
|
|||||||
@@ -48,28 +48,26 @@
|
|||||||
</a-tabs>
|
</a-tabs>
|
||||||
<a-form-item>
|
<a-form-item>
|
||||||
<a-button type="primary" size="large" html-type="button" :loading="loading" class="login-button" @click="handleFinish">
|
<a-button type="primary" size="large" html-type="button" :loading="loading" class="login-button" @click="handleFinish">
|
||||||
{{ t("authentication.loginButton") }}
|
{{ queryBindCode ? t("authentication.bindButton") : t("authentication.loginButton") }}
|
||||||
</a-button>
|
</a-button>
|
||||||
|
|
||||||
<div v-if="!!settingStore.sysPublic.selfServicePasswordRetrievalEnabled && !queryBindCode" class="mt-2">
|
<div class="mt-2 flex justify-between items-center">
|
||||||
<router-link :to="{ name: 'forgotPassword' }">
|
<div class="flex items-center gap-2">
|
||||||
{{ t("authentication.forgotPassword") }}
|
<language-toggle class="text-blue-500"></language-toggle>
|
||||||
</router-link>
|
<router-link v-if="!!settingStore.sysPublic.selfServicePasswordRetrievalEnabled && !queryBindCode" :to="{ name: 'forgotPassword' }">
|
||||||
</div>
|
{{ t("authentication.forgotPassword") }}
|
||||||
</a-form-item>
|
</router-link>
|
||||||
|
</div>
|
||||||
|
|
||||||
<a-form-item class="user-login-other">
|
|
||||||
<div class="flex flex-between justify-between items-center">
|
|
||||||
<language-toggle class="color-blue"></language-toggle>
|
|
||||||
<router-link v-if="hasRegisterTypeEnabled() && !queryBindCode" class="register" :to="{ name: 'register' }">
|
<router-link v-if="hasRegisterTypeEnabled() && !queryBindCode" class="register" :to="{ name: 'register' }">
|
||||||
{{ t("authentication.registerLink") }}
|
{{ t("authentication.registerLink") }}
|
||||||
</router-link>
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-between justify-between items-center mt-5">
|
|
||||||
<oauth-footer></oauth-footer>
|
|
||||||
</div>
|
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
|
|
||||||
|
<div v-if="!queryBindCode" class="w-full">
|
||||||
|
<oauth-footer></oauth-footer>
|
||||||
|
</div>
|
||||||
</a-form>
|
</a-form>
|
||||||
<a-form v-else ref="twoFactorFormRef" class="user-layout-login" :model="twoFactor" v-bind="layout">
|
<a-form v-else ref="twoFactorFormRef" class="user-layout-login" :model="twoFactor" v-bind="layout">
|
||||||
<div class="mb-10 flex flex-center">请打开您的Authenticator APP,获取动态验证码。</div>
|
<div class="mb-10 flex flex-center">请打开您的Authenticator APP,获取动态验证码。</div>
|
||||||
|
|||||||
@@ -12,13 +12,13 @@ export async function OauthLogin(type: string) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function OauthCallback(type: string, query: Record<string, string>) {
|
export async function OauthToken(type: string, validationCode: string) {
|
||||||
return await request({
|
return await request({
|
||||||
url: apiPrefix + `/callback`,
|
url: apiPrefix + `/token`,
|
||||||
method: "post",
|
method: "post",
|
||||||
data: {
|
data: {
|
||||||
type,
|
type,
|
||||||
...query,
|
validationCode,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -43,3 +43,10 @@ export async function BindUser(code: string) {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function GetOauthProviders() {
|
||||||
|
return await request({
|
||||||
|
url: apiPrefix + "/providers",
|
||||||
|
method: "post",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,7 +2,8 @@
|
|||||||
<div class="oauth-callback-page">
|
<div class="oauth-callback-page">
|
||||||
<div class="oauth-callback-content">
|
<div class="oauth-callback-content">
|
||||||
<div v-if="!bindRequired" class="oauth-callback-title">
|
<div v-if="!bindRequired" class="oauth-callback-title">
|
||||||
<span>登录中...</span>
|
<span v-if="!error">登录中...</span>
|
||||||
|
<span v-else>{{ error }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="oauth-callback-title">
|
<div v-else class="oauth-callback-title">
|
||||||
<div>第三方登录成功,还未绑定账号,请选择</div>
|
<div>第三方登录成功,还未绑定账号,请选择</div>
|
||||||
@@ -29,17 +30,16 @@ import { useUserStore } from "/@/store/user";
|
|||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const oauthType = route.params.type as string;
|
const oauthType = route.params.type as string;
|
||||||
|
const validationCode = route.query.validationCode as string;
|
||||||
const query = route.query as Record<string, string>;
|
const error = ref(route.query.error as string);
|
||||||
|
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
|
|
||||||
const bindRequired = ref(false);
|
const bindRequired = ref(false);
|
||||||
const bindCode = ref("");
|
const bindCode = ref("");
|
||||||
|
|
||||||
async function handleOauthCallback() {
|
async function handleOauthToken() {
|
||||||
//处理第三方登录回调
|
//处理第三方登录回调
|
||||||
const res = await api.OauthCallback(oauthType, query);
|
const res = await api.OauthToken(oauthType, validationCode);
|
||||||
if (res.token) {
|
if (res.token) {
|
||||||
//登录成功
|
//登录成功
|
||||||
userStore.onLoginSuccess(res);
|
userStore.onLoginSuccess(res);
|
||||||
@@ -55,7 +55,10 @@ async function handleOauthCallback() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await handleOauthCallback();
|
if (error.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await handleOauthToken();
|
||||||
});
|
});
|
||||||
|
|
||||||
async function goBindUser() {
|
async function goBindUser() {
|
||||||
@@ -95,6 +98,7 @@ async function autoRegister() {
|
|||||||
width: 500px;
|
width: 500px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
margin-top: 50px;
|
margin-top: 50px;
|
||||||
|
margin-bottom: 100px;
|
||||||
|
|
||||||
.oauth-callback-title {
|
.oauth-callback-title {
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
|
|||||||
@@ -1,24 +1,25 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="oauth-footer">
|
<div class="oauth-footer relative">
|
||||||
|
<div class="oauth-title">
|
||||||
|
<div class="oauth-title-text">其他方式登录</div>
|
||||||
|
</div>
|
||||||
<div v-for="item in oauthList" :key="item.type">
|
<div v-for="item in oauthList" :key="item.type">
|
||||||
<div class="oauth-icon-button pointer" @click="goOauthLogin(item.type)">
|
<div class="oauth-icon-button pointer" @click="goOauthLogin(item.name)">
|
||||||
<el-icon :icon="item.icon" />
|
<div><fs-icon :icon="item.icon" class="text-blue-600 text-40" /></div>
|
||||||
<span>{{ item.name }}</span>
|
<div>{{ item.title }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from "vue";
|
import { onMounted, ref } from "vue";
|
||||||
import * as api from "./api";
|
import * as api from "./api";
|
||||||
|
|
||||||
const oauthList = ref([
|
const oauthList = ref([]);
|
||||||
{
|
|
||||||
name: "OIDC",
|
onMounted(async () => {
|
||||||
type: "oidc",
|
oauthList.value = await api.GetOauthProviders();
|
||||||
icon: "ion:oidc",
|
});
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
async function goOauthLogin(type: string) {
|
async function goOauthLogin(type: string) {
|
||||||
//获取第三方登录URL
|
//获取第三方登录URL
|
||||||
@@ -29,17 +30,56 @@ async function goOauthLogin(type: string) {
|
|||||||
</script>
|
</script>
|
||||||
<style lang="less">
|
<style lang="less">
|
||||||
.oauth-footer {
|
.oauth-footer {
|
||||||
|
width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
|
|
||||||
|
.oauth-title {
|
||||||
|
width: 100%;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #8c8c8c;
|
||||||
|
position: relative;
|
||||||
|
.oauth-title-text {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
text-align: center;
|
||||||
|
&::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 0;
|
||||||
|
width: 36%;
|
||||||
|
height: 0.5px;
|
||||||
|
background-color: #8c8c8c;
|
||||||
|
}
|
||||||
|
&::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
right: 0;
|
||||||
|
width: 36%;
|
||||||
|
height: 0.5px;
|
||||||
|
background-color: #8c8c8c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.oauth-icon-button {
|
.oauth-icon-button {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
padding: 8px 16px;
|
padding: 8px 8px;
|
||||||
border-radius: 100px;
|
border-radius: 100px;
|
||||||
|
.fs-icon {
|
||||||
|
font-size: 36px;
|
||||||
|
color: #006be6 !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ function onChange(value: string) {
|
|||||||
<style lang="less">
|
<style lang="less">
|
||||||
.page-sys-settings {
|
.page-sys-settings {
|
||||||
.sys-settings-form {
|
.sys-settings-form {
|
||||||
width: 800px;
|
width: 900px;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,30 +54,34 @@
|
|||||||
<div class="helper">{{ t("certd.saveThenTest") }}</div>
|
<div class="helper">{{ t("certd.saveThenTest") }}</div>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</template>
|
</template>
|
||||||
<a-form-item :label="t('certd.enableOauth')" :name="['public', 'oauthEnabled']">
|
<a-form-item :label="t('certd.sys.setting.enableOauth')" :name="['public', 'oauthEnabled']">
|
||||||
<div class="flex-o">
|
<div class="flex-o">
|
||||||
<a-switch v-model:checked="formState.public.oauthEnabled" :disabled="!settingsStore.isPlus" :title="t('certd.plusFeature')" />
|
<a-switch v-model:checked="formState.public.oauthEnabled" :disabled="!settingsStore.isPlus" :title="t('certd.plusFeature')" />
|
||||||
<vip-button class="ml-5" mode="plus"></vip-button>
|
<vip-button class="ml-5" mode="button"></vip-button>
|
||||||
</div>
|
</div>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item v-if="formState.public.oauthEnabled" :label="t('certd.oauthProviders')" :name="['public', 'oauthProviders']">
|
<a-form-item v-if="formState.public.oauthEnabled" :label="t('certd.sys.setting.oauthProviders')" :name="['public', 'oauthProviders']">
|
||||||
<div class="flex flex-wrap">
|
<div class="flex flex-wrap">
|
||||||
<table>
|
<table class="w-full table-auto border-collapse border border-gray-400">
|
||||||
<tr>
|
<thead>
|
||||||
<th>{{ t("certd.oauthType") }}</th>
|
<tr>
|
||||||
<th>{{ t("certd.oauthConfig") }}</th>
|
<th class="border border-gray-300 px-4 py-2 w-1/2">{{ t("certd.sys.setting.oauthType") }}</th>
|
||||||
</tr>
|
<th class="border border-gray-300 px-4 py-2 w-1/2">{{ t("certd.sys.setting.oauthConfig") }}</th>
|
||||||
<tr v-for="(item, key) of oauthProviders" :key="key">
|
</tr>
|
||||||
<td>
|
</thead>
|
||||||
<div class="flex items-center">
|
<tbody>
|
||||||
<fs-icon :icon="item.icon" />
|
<tr v-for="(item, key) of oauthProviders" :key="key">
|
||||||
{{ item.title }}
|
<td class="border border-gray-300 px-4 py-2">
|
||||||
</div>
|
<div class="flex items-center" :title="item.desc">
|
||||||
</td>
|
<fs-icon :icon="item.icon" class="mr-2 text-blue-600" />
|
||||||
<td>
|
{{ item.title }}
|
||||||
<AddonSelector v-model:model-value="item.addonId" addon-type="oauth" from="sys" :type="item.name" :placeholder="t('certd.clientIdPlaceholder')" />
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
<td class="border border-gray-300 px-4 py-2">
|
||||||
|
<AddonSelector v-model:model-value="item.addonId" addon-type="oauth" from="sys" :type="item.name" :placeholder="t('certd.sys.setting.oauthProviderSelectorPlaceholder')" />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
@@ -190,7 +194,6 @@ async function loadOauthProviders() {
|
|||||||
let list: any = await api.GetOauthProviders();
|
let list: any = await api.GetOauthProviders();
|
||||||
oauthProviders.value = list;
|
oauthProviders.value = list;
|
||||||
for (const item of list) {
|
for (const item of list) {
|
||||||
debugger;
|
|
||||||
const type = item.name;
|
const type = item.name;
|
||||||
const provider = formState.public.oauthProviders?.[type];
|
const provider = formState.public.oauthProviders?.[type];
|
||||||
if (provider) {
|
if (provider) {
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { addonRegistry, BaseController, Constants, SysInstallInfo, SysSettingsService } from "@certd/lib-server";
|
import { addonRegistry, BaseController, Constants, SysInstallInfo, SysSettingsService } from "@certd/lib-server";
|
||||||
import { ALL, Body, Controller, Inject, Post, Provide } from "@midwayjs/core";
|
import { ALL, Body, Controller, Get, Inject, Param, Post, Provide, Query } from "@midwayjs/core";
|
||||||
import { AddonGetterService } from "../../../modules/pipeline/service/addon-getter-service.js";
|
import { AddonGetterService } from "../../../modules/pipeline/service/addon-getter-service.js";
|
||||||
import { IOauthProvider } from "../../../plugins/plugin-oauth/api.js";
|
import { IOauthProvider } from "../../../plugins/plugin-oauth/api.js";
|
||||||
import { LoginService } from "../../../modules/login/service/login-service.js";
|
import { LoginService } from "../../../modules/login/service/login-service.js";
|
||||||
import { CodeService } from "../../../modules/basic/service/code-service.js";
|
import { CodeService } from "../../../modules/basic/service/code-service.js";
|
||||||
import { UserService } from "../../../modules/sys/authority/service/user-service.js";
|
import { UserService } from "../../../modules/sys/authority/service/user-service.js";
|
||||||
import { UserEntity } from "../../../modules/sys/authority/entity/user.js";
|
import { UserEntity } from "../../../modules/sys/authority/entity/user.js";
|
||||||
import { simpleNanoId } from "@certd/basic";
|
import { logger, simpleNanoId } from "@certd/basic";
|
||||||
import { OauthBoundService } from "../../../modules/login/service/oauth-bound-service.js";
|
import { OauthBoundService } from "../../../modules/login/service/oauth-bound-service.js";
|
||||||
import { OauthBoundEntity } from "../../../modules/login/entity/oauth-bound.js";
|
import { OauthBoundEntity } from "../../../modules/login/entity/oauth-bound.js";
|
||||||
|
|
||||||
@@ -56,33 +56,83 @@ export class ConnectController extends BaseController {
|
|||||||
const installInfo = await this.sysSettingsService.getSetting<SysInstallInfo>(SysInstallInfo);
|
const installInfo = await this.sysSettingsService.getSetting<SysInstallInfo>(SysInstallInfo);
|
||||||
const bindUrl = installInfo?.bindUrl || "";
|
const bindUrl = installInfo?.bindUrl || "";
|
||||||
//构造登录url
|
//构造登录url
|
||||||
const redirectUrl = `${bindUrl}#/oauth/callback/${body.type}`;
|
const redirectUrl = `${bindUrl}api/oauth/callback/${body.type}`;
|
||||||
const loginUrl = await addon.buildLoginUrl({ redirectUri: redirectUrl });
|
const { loginUrl, ticketValue } = await addon.buildLoginUrl({ redirectUri: redirectUrl });
|
||||||
return this.ok({loginUrl});
|
const ticket = this.codeService.setValidationValue(ticketValue)
|
||||||
|
this.ctx.cookies.set("oauth_ticket", ticket, {
|
||||||
|
httpOnly: true,
|
||||||
|
// secure: true,
|
||||||
|
// sameSite: "strict",
|
||||||
|
})
|
||||||
|
return this.ok({ loginUrl, ticket });
|
||||||
}
|
}
|
||||||
@Post('/callback', { summary: Constants.per.guest })
|
@Get('/callback/:type', { summary: Constants.per.guest })
|
||||||
public async callback(@Body(ALL) body: any) {
|
public async callback(@Param('type') type: string, @Query() query: Record<string, string>) {
|
||||||
//处理登录回调
|
//处理登录回调
|
||||||
const addon = await this.getOauthProvider(body.type);
|
const addon = await this.getOauthProvider(type);
|
||||||
const tokenRes = await addon.onCallback({
|
const request = this.ctx.request;
|
||||||
code: body.code,
|
// const ticketValue = this.codeService.getValidationValue(ticket);
|
||||||
state: body.state,
|
// if (!ticketValue) {
|
||||||
});
|
// throw new Error("登录ticket已过期");
|
||||||
|
// }
|
||||||
|
|
||||||
const userInfo = tokenRes.userInfo;
|
const ticket = this.ctx.cookies.get("oauth_ticket");
|
||||||
|
if (!ticket) {
|
||||||
|
throw new Error("ticket已过期");
|
||||||
|
}
|
||||||
|
const ticketValue = this.codeService.getValidationValue(ticket);
|
||||||
|
if (!ticketValue) {
|
||||||
|
throw new Error("ticketValue已过期");
|
||||||
|
}
|
||||||
|
|
||||||
const openId = userInfo.openId;
|
const installInfo = await this.sysSettingsService.getSetting<SysInstallInfo>(SysInstallInfo);
|
||||||
|
const bindUrl = installInfo?.bindUrl || "";
|
||||||
|
const currentUrl = `${bindUrl}api/oauth/callback/${type}?${request.querystring}`
|
||||||
|
try {
|
||||||
|
const tokenRes = await addon.onCallback({
|
||||||
|
code: query.code,
|
||||||
|
state: query.state,
|
||||||
|
ticketValue,
|
||||||
|
currentURL: new URL(currentUrl)
|
||||||
|
});
|
||||||
|
|
||||||
|
const userInfo = tokenRes.userInfo;
|
||||||
|
|
||||||
const loginRes = await this.loginService.loginByOpenId({ openId, type: body.type });
|
|
||||||
if (loginRes == null) {
|
|
||||||
// 用户还未绑定,让用户选择绑定已有账号还是自动注册新账号
|
|
||||||
const validationCode = await this.codeService.setValidationValue({
|
const validationCode = await this.codeService.setValidationValue({
|
||||||
type: body.type,
|
type,
|
||||||
userInfo,
|
userInfo,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const redirectUrl = `${bindUrl}#/oauth/callback/${type}?validationCode=${validationCode}`;
|
||||||
|
this.ctx.redirect(redirectUrl);
|
||||||
|
} catch (err) {
|
||||||
|
logger.error(err);
|
||||||
|
this.ctx.redirect(`${bindUrl}#/oauth/callback/${type}?error=${err.error_description || err.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Post('/token', { summary: Constants.per.guest })
|
||||||
|
public async token(@Body(ALL) body: { validationCode: string, type: string }) {
|
||||||
|
const validationValue = await this.codeService.getValidationValue(body.validationCode);
|
||||||
|
if (!validationValue) {
|
||||||
|
throw new Error("校验码错误");
|
||||||
|
}
|
||||||
|
|
||||||
|
const type = validationValue.type;
|
||||||
|
if (type !== body.type) {
|
||||||
|
throw new Error("校验码错误");
|
||||||
|
}
|
||||||
|
const userInfo = validationValue.userInfo;
|
||||||
|
const openId = userInfo.openId;
|
||||||
|
|
||||||
|
const loginRes = await this.loginService.loginByOpenId({ openId, type });
|
||||||
|
if (loginRes == null) {
|
||||||
|
|
||||||
return this.ok({
|
return this.ok({
|
||||||
bindRequired: true,
|
bindRequired: true,
|
||||||
validationCode,
|
validationCode: body.validationCode,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,11 +148,13 @@ export class ConnectController extends BaseController {
|
|||||||
if (!validationValue) {
|
if (!validationValue) {
|
||||||
throw new Error("校验码错误");
|
throw new Error("校验码错误");
|
||||||
}
|
}
|
||||||
|
const type = validationValue.type;
|
||||||
|
const userInfo = validationValue.userInfo;
|
||||||
|
const openId = userInfo.openId;
|
||||||
await this.oauthBoundService.bind({
|
await this.oauthBoundService.bind({
|
||||||
userId,
|
userId,
|
||||||
type: body.type,
|
type,
|
||||||
openId: validationValue.openId,
|
openId,
|
||||||
});
|
});
|
||||||
return this.ok(1);
|
return this.ok(1);
|
||||||
}
|
}
|
||||||
@@ -117,12 +169,12 @@ export class ConnectController extends BaseController {
|
|||||||
const userInfo = validationValue.userInfo;
|
const userInfo = validationValue.userInfo;
|
||||||
const oauthType = validationValue.type;
|
const oauthType = validationValue.type;
|
||||||
let newUser = new UserEntity()
|
let newUser = new UserEntity()
|
||||||
newUser.username = `${oauthType}:_${userInfo.nickName}_${simpleNanoId(6)}`;
|
newUser.username = `${oauthType}_${userInfo.nickName}_${simpleNanoId(6)}`;
|
||||||
newUser.avatar = userInfo.avatar;
|
newUser.avatar = userInfo.avatar;
|
||||||
newUser.nickName = userInfo.nickName;
|
newUser.nickName = userInfo.nickName || simpleNanoId(6);
|
||||||
|
|
||||||
newUser = await this.userService.register("username", newUser, async (txManager) => {
|
newUser = await this.userService.register("username", newUser, async (txManager) => {
|
||||||
const oauthBound : OauthBoundEntity = new OauthBoundEntity()
|
const oauthBound: OauthBoundEntity = new OauthBoundEntity()
|
||||||
oauthBound.userId = newUser.id;
|
oauthBound.userId = newUser.id;
|
||||||
oauthBound.type = oauthType;
|
oauthBound.type = oauthType;
|
||||||
oauthBound.openId = userInfo.openId;
|
oauthBound.openId = userInfo.openId;
|
||||||
|
|||||||
@@ -225,6 +225,7 @@ export class UserService extends BaseService<UserEntity> {
|
|||||||
|
|
||||||
await this.transaction(async txManager => {
|
await this.transaction(async txManager => {
|
||||||
newUser = await txManager.save(newUser);
|
newUser = await txManager.save(newUser);
|
||||||
|
user.id = newUser.id;
|
||||||
const userRole: UserRoleEntity = UserRoleEntity.of(newUser.id, Constants.role.defaultUser);
|
const userRole: UserRoleEntity = UserRoleEntity.of(newUser.id, Constants.role.defaultUser);
|
||||||
await txManager.save(userRole);
|
await txManager.save(userRole);
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
export type OnCallbackReq = {
|
export type OnCallbackReq = {
|
||||||
code: string;
|
code: string;
|
||||||
state: string;
|
state: string;
|
||||||
|
currentURL: URL;
|
||||||
|
ticketValue: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type OauthToken = {
|
export type OauthToken = {
|
||||||
@@ -30,8 +32,12 @@ export type OnBindReply = {
|
|||||||
message: string;
|
message: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type LoginUrlReply = {
|
||||||
|
loginUrl: string;
|
||||||
|
ticketValue: any;
|
||||||
|
}
|
||||||
|
|
||||||
export interface IOauthProvider {
|
export interface IOauthProvider {
|
||||||
buildLoginUrl: (params: { redirectUri: string }) => Promise<string>;
|
buildLoginUrl: (params: { redirectUri: string }) => Promise<LoginUrlReply>;
|
||||||
onCallback: (params: OnCallbackReq) => Promise<OauthToken>;
|
onCallback: (params: OnCallbackReq) => Promise<OauthToken>;
|
||||||
onBind: (params: OnBindReq) => Promise<OnBindReply>;
|
|
||||||
}
|
}
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
import { AddonInput, BaseAddon, IsAddon } from "@certd/lib-server";
|
import { AddonInput, BaseAddon, IsAddon } from "@certd/lib-server";
|
||||||
import { IOauthProvider, OnBindReq, OnCallbackReq } from "../api.js";
|
import { IOauthProvider, OnCallbackReq } from "../api.js";
|
||||||
|
|
||||||
@IsAddon({
|
@IsAddon({
|
||||||
addonType: "oauth",
|
addonType: "oauth",
|
||||||
name: 'oidc',
|
name: 'oidc',
|
||||||
title: 'OpenId connect 认证',
|
title: 'OIDC认证',
|
||||||
desc: '',
|
desc: 'OpenID Connect 认证,统一认证服务',
|
||||||
|
icon:"simple-icons:fusionauth",
|
||||||
showTest: false,
|
showTest: false,
|
||||||
})
|
})
|
||||||
export class OidcOauthProvider extends BaseAddon implements IOauthProvider {
|
export class OidcOauthProvider extends BaseAddon implements IOauthProvider {
|
||||||
@@ -54,41 +55,7 @@ export class OidcOauthProvider extends BaseAddon implements IOauthProvider {
|
|||||||
client
|
client
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async onCallback(req: OnCallbackReq) {
|
|
||||||
const { config, client } = await this.getClient()
|
|
||||||
|
|
||||||
const currentUrl = new URL("")
|
|
||||||
let tokens: any = await client.authorizationCodeGrant(
|
|
||||||
config,
|
|
||||||
currentUrl,
|
|
||||||
{
|
|
||||||
pkceCodeVerifier: req.code,
|
|
||||||
expectedState: req.state,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
console.log('Token Endpoint Response', tokens)
|
|
||||||
const claims = tokens.claims()
|
|
||||||
return {
|
|
||||||
token:{
|
|
||||||
accessToken: tokens.access_token,
|
|
||||||
refreshToken: tokens.refresh_token,
|
|
||||||
expiresIn: tokens.expires_in,
|
|
||||||
},
|
|
||||||
userInfo: {
|
|
||||||
openId: claims.sub,
|
|
||||||
nickName: claims.nickname,
|
|
||||||
avatar: claims.picture,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
};
|
|
||||||
async onBind(req: OnBindReq) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
message: '绑定失败',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
async buildLoginUrl(params: { redirectUri: string }) {
|
async buildLoginUrl(params: { redirectUri: string }) {
|
||||||
const { config, client } = await this.getClient()
|
const { config, client } = await this.getClient()
|
||||||
|
|
||||||
@@ -126,6 +93,40 @@ export class OidcOauthProvider extends BaseAddon implements IOauthProvider {
|
|||||||
|
|
||||||
// now redirect the user to redirectTo.href
|
// now redirect the user to redirectTo.href
|
||||||
console.log('redirecting to', redirectTo.href)
|
console.log('redirecting to', redirectTo.href)
|
||||||
return redirectTo.href;
|
return {
|
||||||
|
loginUrl: redirectTo.href,
|
||||||
|
ticketValue: {
|
||||||
|
codeVerifier: code_verifier,
|
||||||
|
},
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async onCallback(req: OnCallbackReq) {
|
||||||
|
const { config, client } = await this.getClient()
|
||||||
|
|
||||||
|
|
||||||
|
let tokens: any = await client.authorizationCodeGrant(
|
||||||
|
config,
|
||||||
|
req.currentURL,
|
||||||
|
{
|
||||||
|
expectedState: client.skipStateCheck ,
|
||||||
|
pkceCodeVerifier: req.ticketValue.codeVerifier,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
console.log('Token Endpoint Response', tokens)
|
||||||
|
const claims = tokens.claims()
|
||||||
|
return {
|
||||||
|
token:{
|
||||||
|
accessToken: tokens.access_token,
|
||||||
|
refreshToken: tokens.refresh_token,
|
||||||
|
expiresIn: tokens.expires_in,
|
||||||
|
},
|
||||||
|
userInfo: {
|
||||||
|
openId: claims.sub,
|
||||||
|
nickName: claims.nickname || claims.preferred_username || "",
|
||||||
|
avatar: claims.picture,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user