feat: 个人中心-安全设置,支持绑定、解绑三方账号
This commit is contained in:
parent
36d52d3e15
commit
efe455736c
@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package top.charles7c.cnadmin.common.enums;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
/**
|
||||
* 第三方账号平台枚举
|
||||
*
|
||||
* @author Charles7c
|
||||
* @since 2023/10/19 21:22
|
||||
*/
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
public enum SocialSourceEnum {
|
||||
|
||||
/** 码云 */
|
||||
GITEE("码云"),
|
||||
|
||||
/** GitHub */
|
||||
GITHUB("GitHub"),;
|
||||
|
||||
private final String description;
|
||||
}
|
@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package top.charles7c.cnadmin.system.model.vo;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
/**
|
||||
* 第三方账号绑定信息
|
||||
*
|
||||
* @author Charles7c
|
||||
* @since 2023/10/19 21:29
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "第三方账号绑定信息")
|
||||
public class UserSocialBindVO implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 来源
|
||||
*/
|
||||
@Schema(description = "来源", example = "GITEE")
|
||||
private String source;
|
||||
|
||||
/**
|
||||
* 描述
|
||||
*/
|
||||
@Schema(description = "描述", example = "码云")
|
||||
private String description;
|
||||
}
|
@ -16,8 +16,12 @@
|
||||
|
||||
package top.charles7c.cnadmin.system.service;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import top.charles7c.cnadmin.system.model.entity.UserSocialDO;
|
||||
|
||||
import me.zhyd.oauth.model.AuthUser;
|
||||
|
||||
/**
|
||||
* 用户社会化关联业务接口
|
||||
*
|
||||
@ -44,4 +48,33 @@ public interface UserSocialService {
|
||||
* 用户社会化关联信息
|
||||
*/
|
||||
void saveOrUpdate(UserSocialDO userSocial);
|
||||
|
||||
/**
|
||||
* 根据用户 ID 查询
|
||||
*
|
||||
* @param userId
|
||||
* 用户 ID
|
||||
* @return 用户社会化关联信息
|
||||
*/
|
||||
List<UserSocialDO> listByUserId(Long userId);
|
||||
|
||||
/**
|
||||
* 绑定
|
||||
*
|
||||
* @param authUser
|
||||
* 社交身份信息
|
||||
* @param userId
|
||||
* 用户 ID
|
||||
*/
|
||||
void bind(AuthUser authUser, Long userId);
|
||||
|
||||
/**
|
||||
* 根据来源和用户 ID 删除
|
||||
*
|
||||
* @param source
|
||||
* 来源
|
||||
* @param userId
|
||||
* 用户 ID
|
||||
*/
|
||||
void deleteBySourceAndUserId(String source, Long userId);
|
||||
}
|
@ -16,15 +16,25 @@
|
||||
|
||||
package top.charles7c.cnadmin.system.service.impl;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import cn.hutool.json.JSONUtil;
|
||||
|
||||
import top.charles7c.cnadmin.common.util.validate.CheckUtils;
|
||||
import top.charles7c.cnadmin.system.mapper.UserSocialMapper;
|
||||
import top.charles7c.cnadmin.system.model.entity.UserSocialDO;
|
||||
import top.charles7c.cnadmin.system.service.UserSocialService;
|
||||
|
||||
import me.zhyd.oauth.model.AuthUser;
|
||||
|
||||
/**
|
||||
* 用户社会化关联业务实现
|
||||
*
|
||||
@ -54,4 +64,32 @@ public class UserSocialServiceImpl implements UserSocialService {
|
||||
.update();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<UserSocialDO> listByUserId(Long userId) {
|
||||
return baseMapper.lambdaQuery().eq(UserSocialDO::getUserId, userId).list();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bind(AuthUser authUser, Long userId) {
|
||||
String source = authUser.getSource();
|
||||
String openId = authUser.getUuid();
|
||||
List<UserSocialDO> userSocialList = this.listByUserId(userId);
|
||||
Set<String> boundSocialSet = userSocialList.stream().map(UserSocialDO::getSource).collect(Collectors.toSet());
|
||||
CheckUtils.throwIf(boundSocialSet.contains(source), "您已经绑定过了 [{}] 平台,请先解绑");
|
||||
UserSocialDO userSocial = this.getBySourceAndOpenId(source, openId);
|
||||
CheckUtils.throwIfNotNull(userSocial, "[{}] 平台账号 [{}] 已被其他用户绑定", source, authUser.getUsername());
|
||||
userSocial = new UserSocialDO();
|
||||
userSocial.setUserId(userId);
|
||||
userSocial.setSource(source);
|
||||
userSocial.setOpenId(openId);
|
||||
userSocial.setMetaJson(JSONUtil.toJsonStr(authUser));
|
||||
userSocial.setLastLoginTime(LocalDateTime.now());
|
||||
baseMapper.insert(userSocial);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteBySourceAndUserId(String source, Long userId) {
|
||||
baseMapper.lambdaUpdate().eq(UserSocialDO::getSource, source).eq(UserSocialDO::getUserId, userId).remove();
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import axios from 'axios';
|
||||
|
||||
const BASE_URL = '/system/user/center';
|
||||
const BASE_URL = '/system/user';
|
||||
|
||||
export interface BasicInfoModel {
|
||||
username: string;
|
||||
@ -43,3 +43,20 @@ export interface UpdateEmailReq {
|
||||
export function updateEmail(req: UpdateEmailReq) {
|
||||
return axios.patch(`${BASE_URL}/email`, req);
|
||||
}
|
||||
|
||||
export interface UserSocialBindRecord {
|
||||
source: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export function listSocial() {
|
||||
return axios.get<UserSocialBindRecord[]>(`${BASE_URL}/social`);
|
||||
}
|
||||
|
||||
export function bindSocial(source: string, req: any) {
|
||||
return axios.post(`${BASE_URL}/social/${source}`, req);
|
||||
}
|
||||
|
||||
export function unbindSocial(source: string) {
|
||||
return axios.delete(`${BASE_URL}/social/${source}`);
|
||||
}
|
||||
|
@ -34,7 +34,7 @@
|
||||
<div v-else class="account app" @click="toggleLoginMode">
|
||||
<icon-user /> {{ $t('login.account.txt') }}
|
||||
</div>
|
||||
<a-tooltip content="Gitee" mini>
|
||||
<a-tooltip content="码云" mini>
|
||||
<a-link class="app" @click="handleSocialAuth('gitee')">
|
||||
<svg
|
||||
class="icon"
|
||||
|
@ -5,6 +5,7 @@ export default {
|
||||
'login.email': 'Email Login',
|
||||
'login.other': 'Other Login',
|
||||
'login.ing': 'Login...',
|
||||
'bind.ing': 'Bind...',
|
||||
|
||||
'login.account.placeholder.username': 'Please enter username',
|
||||
'login.account.placeholder.password': 'Please enter password',
|
||||
|
@ -5,6 +5,7 @@ export default {
|
||||
'login.email': '邮箱登录',
|
||||
'login.other': '其他登录方式',
|
||||
'login.ing': '登录中...',
|
||||
'bind.ing': '绑定中...',
|
||||
|
||||
'login.account.placeholder.username': '请输入用户名',
|
||||
'login.account.placeholder.password': '请输入密码',
|
||||
|
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<a-spin :loading="loading" :tip="$t('login.ing')">
|
||||
<a-spin :loading="loading" :tip="isLogin() ? $t('bind.ing') : $t('login.ing')">
|
||||
<div></div>
|
||||
</a-spin>
|
||||
</template>
|
||||
@ -9,6 +9,8 @@
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { useUserStore } from '@/store';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { isLogin } from '@/utils/auth';
|
||||
import { bindSocial } from '@/api/system/user-center';
|
||||
|
||||
const { proxy } = getCurrentInstance() as any;
|
||||
const { t } = useI18n();
|
||||
@ -45,7 +47,42 @@
|
||||
loading.value = false;
|
||||
});
|
||||
};
|
||||
handleSocialLogin();
|
||||
|
||||
/**
|
||||
* 绑定第三方账号
|
||||
*/
|
||||
const handleBindSocial = () => {
|
||||
if (loading.value) return;
|
||||
loading.value = true;
|
||||
const { redirect, ...othersQuery } = router.currentRoute.value.query;
|
||||
bindSocial(source, othersQuery)
|
||||
.then((res) => {
|
||||
router.push({
|
||||
name: 'UserCenter',
|
||||
query: {
|
||||
tab: 'security-setting',
|
||||
},
|
||||
});
|
||||
proxy.$message.success(res.msg);
|
||||
})
|
||||
.catch(() => {
|
||||
router.push({
|
||||
name: 'UserCenter',
|
||||
query: {
|
||||
tab: 'security-setting',
|
||||
},
|
||||
});
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
};
|
||||
|
||||
if (isLogin()) {
|
||||
handleBindSocial();
|
||||
} else {
|
||||
handleSocialLogin();
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
|
@ -9,6 +9,9 @@
|
||||
<a-list-item>
|
||||
<UpdateEmail />
|
||||
</a-list-item>
|
||||
<a-list-item>
|
||||
<BindSocial />
|
||||
</a-list-item>
|
||||
</a-list>
|
||||
</template>
|
||||
|
||||
@ -16,6 +19,7 @@
|
||||
import UpdatePwd from './security-settings/update-pwd.vue';
|
||||
import UpdatePhone from './security-settings/update-phone.vue';
|
||||
import UpdateEmail from './security-settings/update-email.vue';
|
||||
import BindSocial from './security-settings/bind-social.vue';
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
@ -25,7 +29,8 @@
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.arco-list-item-meta-avatar {
|
||||
margin-bottom: 1px;
|
||||
width: 70px;
|
||||
margin-right: 24px;
|
||||
}
|
||||
.arco-list-item-meta {
|
||||
padding: 0;
|
||||
@ -37,10 +42,13 @@
|
||||
border-bottom: 1px solid var(--color-neutral-3);
|
||||
.arco-list-item-meta-description {
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
justify-content: space-between;
|
||||
.tip {
|
||||
width: 50%;
|
||||
color: rgb(var(--gray-6));
|
||||
margin-right: 24px;
|
||||
}
|
||||
.content {
|
||||
flex: 1 1 0;
|
||||
}
|
||||
.operation {
|
||||
margin-right: 6px;
|
||||
|
@ -0,0 +1,165 @@
|
||||
<template>
|
||||
<a-list-item-meta>
|
||||
<template #avatar>
|
||||
<a-typography-paragraph>
|
||||
{{ $t('userCenter.securitySettings.social.label') }}
|
||||
</a-typography-paragraph>
|
||||
</template>
|
||||
<template #description>
|
||||
<div class="tip">
|
||||
{{ $t('userCenter.securitySettings.social.tip') }}
|
||||
</div>
|
||||
<div class="content">
|
||||
<a-typography-paragraph>
|
||||
<span v-if="socialBinds.length > 0">
|
||||
{{ socialBinds.map((item) => item.description).join('、') }}
|
||||
</span>
|
||||
<span v-else class="tip">
|
||||
{{ $t('userCenter.securitySettings.social.content') }}
|
||||
</span>
|
||||
</a-typography-paragraph>
|
||||
</div>
|
||||
<div class="operation">
|
||||
<a-tooltip content="码云" mini>
|
||||
<a-link @click="handleBind('GITEE', '码云')">
|
||||
<svg
|
||||
v-if="giteeSocial"
|
||||
class="icon"
|
||||
style="fill: #c71d23"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M11.984 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0a12 12 0 0 0-.016 0zm6.09 5.333c.328 0 .593.266.592.593v1.482a.594.594 0 0 1-.593.592H9.777c-.982 0-1.778.796-1.778 1.778v5.63c0 .327.266.592.593.592h5.63c.982 0 1.778-.796 1.778-1.778v-.296a.593.593 0 0 0-.592-.593h-4.15a.592.592 0 0 1-.592-.592v-1.482a.593.593 0 0 1 .593-.592h6.815c.327 0 .593.265.593.592v3.408a4 4 0 0 1-4 4H5.926a.593.593 0 0 1-.593-.593V9.778a4.444 4.444 0 0 1 4.445-4.444h8.296Z"
|
||||
/>
|
||||
</svg>
|
||||
<svg
|
||||
v-else
|
||||
class="icon GITEE"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M11.984 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0a12 12 0 0 0-.016 0zm6.09 5.333c.328 0 .593.266.592.593v1.482a.594.594 0 0 1-.593.592H9.777c-.982 0-1.778.796-1.778 1.778v5.63c0 .327.266.592.593.592h5.63c.982 0 1.778-.796 1.778-1.778v-.296a.593.593 0 0 0-.592-.593h-4.15a.592.592 0 0 1-.592-.592v-1.482a.593.593 0 0 1 .593-.592h6.815c.327 0 .593.265.593.592v3.408a4 4 0 0 1-4 4H5.926a.593.593 0 0 1-.593-.593V9.778a4.444 4.444 0 0 1 4.445-4.444h8.296Z"
|
||||
/>
|
||||
</svg>
|
||||
</a-link>
|
||||
</a-tooltip>
|
||||
<a-tooltip content="GitHub" mini>
|
||||
<a-link @click="handleBind('GITHUB', 'GitHub')">
|
||||
<svg
|
||||
v-if="githubSocial"
|
||||
class="icon"
|
||||
style="fill: #181717"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"
|
||||
/>
|
||||
</svg>
|
||||
<svg
|
||||
v-else
|
||||
class="icon GITHUB"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"
|
||||
/>
|
||||
</svg>
|
||||
</a-link>
|
||||
</a-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
</a-list-item-meta>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { getCurrentInstance, ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import {
|
||||
UserSocialBindRecord,
|
||||
listSocial,
|
||||
unbindSocial,
|
||||
} from '@/api/system/user-center';
|
||||
import { socialAuth } from '@/api/auth/login';
|
||||
|
||||
const { proxy } = getCurrentInstance() as any;
|
||||
const { t } = useI18n();
|
||||
const socialBinds = ref<UserSocialBindRecord[]>([]);
|
||||
const giteeSocial = ref<UserSocialBindRecord>();
|
||||
const githubSocial = ref<UserSocialBindRecord>();
|
||||
|
||||
/**
|
||||
* 查询绑定的第三方账号
|
||||
*/
|
||||
const list = () => {
|
||||
listSocial().then((res) => {
|
||||
socialBinds.value = res.data;
|
||||
giteeSocial.value = socialBinds.value.find(
|
||||
(item) => item.source === 'GITEE'
|
||||
);
|
||||
githubSocial.value = socialBinds.value.find(
|
||||
(item) => item.source === 'GITHUB'
|
||||
);
|
||||
});
|
||||
};
|
||||
list();
|
||||
|
||||
/**
|
||||
* 绑定或解绑
|
||||
*
|
||||
* @param source 来源
|
||||
* @param sourceDescription 来源描述
|
||||
*/
|
||||
const handleBind = (source: string, sourceDescription: string) => {
|
||||
const isBind = socialBinds.value.some((item) => item.source === source);
|
||||
if (isBind) {
|
||||
proxy.$modal.warning({
|
||||
title: `确认解除和${sourceDescription}平台的三方账号绑定吗?`,
|
||||
titleAlign: 'start',
|
||||
content: '解除绑定后,将无法使用该第三方账户登录到此账号',
|
||||
hideCancel: false,
|
||||
onOk: () => {
|
||||
unbindSocial(source).then((res) => {
|
||||
list();
|
||||
proxy.$message.success(res.msg);
|
||||
});
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
proxy.$modal.info({
|
||||
title: '提示',
|
||||
titleAlign: 'start',
|
||||
content: `确认和${sourceDescription}平台的三方账号绑定吗?`,
|
||||
hideCancel: false,
|
||||
onOk: () => {
|
||||
socialAuth(source).then((res) => {
|
||||
window.location.href = res.data;
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
:deep(.arco-link) {
|
||||
padding: 1px 2px;
|
||||
}
|
||||
|
||||
.icon {
|
||||
width: 21px;
|
||||
height: 20px;
|
||||
fill: rgb(170, 170, 170);
|
||||
}
|
||||
|
||||
.icon:hover.GITEE {
|
||||
fill: #c71d23;
|
||||
}
|
||||
|
||||
.icon:hover.GITHUB {
|
||||
fill: #181717;
|
||||
}
|
||||
</style>
|
@ -2,24 +2,19 @@
|
||||
<a-list-item-meta>
|
||||
<template #avatar>
|
||||
<a-typography-paragraph>
|
||||
{{ $t('userCenter.securitySettings.updateEmail.label.email') }}
|
||||
{{ $t('userCenter.securitySettings.email.label') }}
|
||||
</a-typography-paragraph>
|
||||
</template>
|
||||
<template #description>
|
||||
<div class="tip">
|
||||
{{ $t('userCenter.securitySettings.email.tip') }}
|
||||
</div>
|
||||
<div class="content">
|
||||
<a-typography-paragraph v-if="userStore.email">
|
||||
{{
|
||||
$t(
|
||||
'userCenter.securitySettings.updateEmail.placeholder.success.email'
|
||||
)
|
||||
}}:{{ userStore.email }}
|
||||
{{ userStore.email }}
|
||||
</a-typography-paragraph>
|
||||
<a-typography-paragraph v-else class="tip">
|
||||
{{
|
||||
$t(
|
||||
'userCenter.securitySettings.updateEmail.placeholder.error.email'
|
||||
)
|
||||
}}
|
||||
{{ $t('userCenter.securitySettings.email.content') }}
|
||||
</a-typography-paragraph>
|
||||
</div>
|
||||
<div class="operation">
|
||||
|
@ -2,28 +2,23 @@
|
||||
<a-list-item-meta>
|
||||
<template #avatar>
|
||||
<a-typography-paragraph>
|
||||
{{ $t('userCenter.securitySettings.updatePhone.label.phone') }}
|
||||
{{ $t('userCenter.securitySettings.phone.label') }}
|
||||
</a-typography-paragraph>
|
||||
</template>
|
||||
<template #description>
|
||||
<div class="tip">
|
||||
{{ $t('userCenter.securitySettings.phone.tip') }}
|
||||
</div>
|
||||
<div class="content">
|
||||
<a-typography-paragraph v-if="userStore.phone">
|
||||
{{
|
||||
$t(
|
||||
'userCenter.securitySettings.updatePhone.placeholder.success.phone'
|
||||
)
|
||||
}}:{{ userStore.phone }}
|
||||
{{ userStore.phone }}
|
||||
</a-typography-paragraph>
|
||||
<a-typography-paragraph v-else class="tip">
|
||||
{{
|
||||
$t(
|
||||
'userCenter.securitySettings.updatePhone.placeholder.error.phone'
|
||||
)
|
||||
}}
|
||||
{{ $t('userCenter.securitySettings.phone.content') }}
|
||||
</a-typography-paragraph>
|
||||
</div>
|
||||
<div class="operation">
|
||||
<a-link :title="$t('userCenter.securitySettings.button.update')">
|
||||
<a-link disabled :title="$t('userCenter.securitySettings.button.update')">
|
||||
{{ $t('userCenter.securitySettings.button.update') }}
|
||||
</a-link>
|
||||
</div>
|
||||
|
@ -2,24 +2,19 @@
|
||||
<a-list-item-meta>
|
||||
<template #avatar>
|
||||
<a-typography-paragraph>
|
||||
{{ $t('userCenter.securitySettings.updatePwd.label.password') }}
|
||||
{{ $t('userCenter.securitySettings.password.label') }}
|
||||
</a-typography-paragraph>
|
||||
</template>
|
||||
<template #description>
|
||||
<div class="tip">
|
||||
{{ $t('userCenter.securitySettings.password.tip') }}
|
||||
</div>
|
||||
<div class="content">
|
||||
<a-typography-paragraph v-if="userStore.pwdResetTime">
|
||||
{{
|
||||
$t(
|
||||
'userCenter.securitySettings.updatePwd.placeholder.success.password'
|
||||
)
|
||||
}}
|
||||
{{ $t('userCenter.securitySettings.content.hasBeenSet') }}
|
||||
</a-typography-paragraph>
|
||||
<a-typography-paragraph v-else class="tip">
|
||||
{{
|
||||
$t(
|
||||
'userCenter.securitySettings.updatePwd.placeholder.error.password'
|
||||
)
|
||||
}}
|
||||
{{ $t('userCenter.securitySettings.password.content') }}
|
||||
</a-typography-paragraph>
|
||||
</div>
|
||||
<div class="operation">
|
||||
|
@ -8,7 +8,11 @@
|
||||
</a-row>
|
||||
<a-row class="wrapper">
|
||||
<a-col :span="24">
|
||||
<a-tabs default-active-key="1" type="rounded">
|
||||
<a-tabs
|
||||
v-model:active-key="activeKey"
|
||||
default-active-key="1"
|
||||
type="rounded"
|
||||
>
|
||||
<a-tab-pane key="1" :title="$t('userCenter.tab.basicInfo')">
|
||||
<BasicInfo />
|
||||
</a-tab-pane>
|
||||
@ -25,10 +29,22 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import UserPanel from './components/user-panel.vue';
|
||||
import BasicInfo from './components/basic-info.vue';
|
||||
import SecuritySettings from './components/security-settings.vue';
|
||||
import OperationLog from './components/operation-log.vue';
|
||||
|
||||
const route = useRoute();
|
||||
const activeKey = ref('1');
|
||||
const tab = route.query.tab as string;
|
||||
|
||||
onMounted(() => {
|
||||
if (tab === 'security-setting') {
|
||||
activeKey.value = '2';
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
|
@ -35,11 +35,10 @@ export default {
|
||||
|
||||
// security-settings
|
||||
// update-pwd
|
||||
'userCenter.securitySettings.updatePwd.label.password': 'Login Password',
|
||||
'userCenter.securitySettings.updatePwd.placeholder.success.password':
|
||||
'Has been set',
|
||||
'userCenter.securitySettings.updatePwd.placeholder.error.password':
|
||||
'You have not set a password yet. The password must contain at least six letters, digits, and special characters except Spaces.',
|
||||
'userCenter.securitySettings.password.label': 'Login Password',
|
||||
'userCenter.securitySettings.password.tip':
|
||||
'The password you need to enter when logging in to your account',
|
||||
'userCenter.securitySettings.password.content': 'Not set',
|
||||
|
||||
'userCenter.securitySettings.updatePwd.modal.title': 'Update login password',
|
||||
'userCenter.securitySettings.updatePwd.form.label.oldPassword':
|
||||
@ -70,18 +69,16 @@ export default {
|
||||
'Two passwords are different',
|
||||
|
||||
// update-phone
|
||||
'userCenter.securitySettings.updatePhone.label.phone': 'Phone',
|
||||
'userCenter.securitySettings.updatePhone.placeholder.success.phone':
|
||||
'Has been bound',
|
||||
'userCenter.securitySettings.updatePhone.placeholder.error.phone':
|
||||
'You have not set a phone yet. The phone binding can be used to retrieve passwords and receive notifications and SMS login.',
|
||||
'userCenter.securitySettings.phone.label': 'Phone',
|
||||
'userCenter.securitySettings.phone.tip':
|
||||
'It is used to receive messages, verify identity, and support mobile phone verification code login after binding',
|
||||
'userCenter.securitySettings.phone.content': 'Unbound',
|
||||
|
||||
// update-email
|
||||
'userCenter.securitySettings.updateEmail.label.email': 'Email',
|
||||
'userCenter.securitySettings.updateEmail.placeholder.success.email':
|
||||
'Has been bound',
|
||||
'userCenter.securitySettings.updateEmail.placeholder.error.email':
|
||||
'You have not set a mailbox yet. The mailbox binding can be used to retrieve passwords and receive notifications.',
|
||||
'userCenter.securitySettings.email.label': 'Email',
|
||||
'userCenter.securitySettings.email.tip':
|
||||
'Used to receive messages, verify identity',
|
||||
'userCenter.securitySettings.email.content': 'Unbound',
|
||||
|
||||
'userCenter.securitySettings.updateEmail.modal.title': 'Update email',
|
||||
'userCenter.securitySettings.updateEmail.form.label.newEmail': 'New email',
|
||||
@ -112,5 +109,12 @@ export default {
|
||||
'userCenter.securitySettings.updateEmail.form.error.required.currentPassword':
|
||||
'Please enter current password',
|
||||
|
||||
// bind-social
|
||||
'userCenter.securitySettings.social.label': 'Three-party login',
|
||||
'userCenter.securitySettings.social.tip':
|
||||
'Support quick login of third-party accounts',
|
||||
'userCenter.securitySettings.social.content': 'Unbound',
|
||||
|
||||
'userCenter.securitySettings.content.hasBeenSet': 'Has been set',
|
||||
'userCenter.securitySettings.button.update': 'Update',
|
||||
};
|
||||
|
@ -35,11 +35,9 @@ export default {
|
||||
|
||||
// security-settings
|
||||
// update-pwd
|
||||
'userCenter.securitySettings.updatePwd.label.password': '登录密码',
|
||||
'userCenter.securitySettings.updatePwd.placeholder.success.password':
|
||||
'已设置',
|
||||
'userCenter.securitySettings.updatePwd.placeholder.error.password':
|
||||
'您暂未设置密码,密码至少6位字符,支持数字、字母和除空格外的特殊字符。',
|
||||
'userCenter.securitySettings.password.label': '登录密码',
|
||||
'userCenter.securitySettings.password.tip': '登录账号时需要输入的密码',
|
||||
'userCenter.securitySettings.password.content': '未设置',
|
||||
|
||||
'userCenter.securitySettings.updatePwd.modal.title': '修改登录密码',
|
||||
'userCenter.securitySettings.updatePwd.form.label.oldPassword': '当前密码',
|
||||
@ -67,16 +65,15 @@ export default {
|
||||
'两次输入的密码不一致',
|
||||
|
||||
// update-phone
|
||||
'userCenter.securitySettings.updatePhone.label.phone': '安全手机',
|
||||
'userCenter.securitySettings.updatePhone.placeholder.success.phone': '已绑定',
|
||||
'userCenter.securitySettings.updatePhone.placeholder.error.phone':
|
||||
'您暂未设置手机号,绑定手机号可以用来找回密码、接收通知、短信登录等。',
|
||||
'userCenter.securitySettings.phone.label': '安全手机',
|
||||
'userCenter.securitySettings.phone.tip':
|
||||
'用于接收消息、验证身份,绑定后可支持手机验证码登录',
|
||||
'userCenter.securitySettings.phone.content': '未绑定',
|
||||
|
||||
// update-email
|
||||
'userCenter.securitySettings.updateEmail.label.email': '安全邮箱',
|
||||
'userCenter.securitySettings.updateEmail.placeholder.success.email': '已绑定',
|
||||
'userCenter.securitySettings.updateEmail.placeholder.error.email':
|
||||
'您暂未设置邮箱,绑定邮箱可以用来找回密码、接收通知等。',
|
||||
'userCenter.securitySettings.email.label': '安全邮箱',
|
||||
'userCenter.securitySettings.email.tip': '用于接收消息、验证身份',
|
||||
'userCenter.securitySettings.email.content': '未绑定',
|
||||
|
||||
'userCenter.securitySettings.updateEmail.modal.title': '修改邮箱',
|
||||
'userCenter.securitySettings.updateEmail.form.label.newEmail': '新邮箱',
|
||||
@ -106,5 +103,11 @@ export default {
|
||||
'userCenter.securitySettings.updateEmail.form.error.required.currentPassword':
|
||||
'请输入当前密码',
|
||||
|
||||
// bind-social
|
||||
'userCenter.securitySettings.social.label': '三方登录',
|
||||
'userCenter.securitySettings.social.tip': '支持三方账号快速登录',
|
||||
'userCenter.securitySettings.social.content': '未绑定',
|
||||
|
||||
'userCenter.securitySettings.content.hasBeenSet': '已设置',
|
||||
'userCenter.securitySettings.button.update': '修改',
|
||||
};
|
||||
|
@ -72,6 +72,9 @@ public class SocialAuthController {
|
||||
@Parameter(name = "source", description = "来源", example = "gitee", in = ParameterIn.PATH)
|
||||
@PostMapping("/{source}")
|
||||
public LoginVO login(@PathVariable String source, @RequestBody AuthCallback callback) {
|
||||
if (StpUtil.isLogin()) {
|
||||
StpUtil.logout();
|
||||
}
|
||||
AuthRequest authRequest = this.getAuthRequest(source);
|
||||
AuthResponse<AuthUser> response = authRequest.login(callback);
|
||||
ValidationUtils.throwIf(!response.ok(), response.getMsg());
|
||||
@ -82,9 +85,6 @@ public class SocialAuthController {
|
||||
|
||||
private AuthRequest getAuthRequest(String source) {
|
||||
try {
|
||||
if (StpUtil.isLogin()) {
|
||||
StpUtil.logout();
|
||||
}
|
||||
return authRequestFactory.get(source);
|
||||
} catch (Exception e) {
|
||||
throw new BadRequestException(String.format("暂不支持 [%s] 登录", source));
|
||||
|
@ -16,32 +16,48 @@
|
||||
|
||||
package top.charles7c.cnadmin.webapi.controller.system;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javax.validation.constraints.NotNull;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.enums.ParameterIn;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import com.xkcoding.justauth.AuthRequestFactory;
|
||||
|
||||
import cn.hutool.core.util.ReUtil;
|
||||
|
||||
import top.charles7c.cnadmin.common.constant.CacheConsts;
|
||||
import top.charles7c.cnadmin.common.constant.RegexConsts;
|
||||
import top.charles7c.cnadmin.common.enums.SocialSourceEnum;
|
||||
import top.charles7c.cnadmin.common.model.vo.R;
|
||||
import top.charles7c.cnadmin.common.util.ExceptionUtils;
|
||||
import top.charles7c.cnadmin.common.util.RedisUtils;
|
||||
import top.charles7c.cnadmin.common.util.SecureUtils;
|
||||
import top.charles7c.cnadmin.common.util.helper.LoginHelper;
|
||||
import top.charles7c.cnadmin.common.util.validate.ValidationUtils;
|
||||
import top.charles7c.cnadmin.system.model.entity.UserSocialDO;
|
||||
import top.charles7c.cnadmin.system.model.request.UpdateBasicInfoRequest;
|
||||
import top.charles7c.cnadmin.system.model.request.UpdateEmailRequest;
|
||||
import top.charles7c.cnadmin.system.model.request.UpdatePasswordRequest;
|
||||
import top.charles7c.cnadmin.system.model.vo.AvatarVO;
|
||||
import top.charles7c.cnadmin.system.model.vo.UserSocialBindVO;
|
||||
import top.charles7c.cnadmin.system.service.UserService;
|
||||
import top.charles7c.cnadmin.system.service.UserSocialService;
|
||||
|
||||
import me.zhyd.oauth.model.AuthCallback;
|
||||
import me.zhyd.oauth.model.AuthResponse;
|
||||
import me.zhyd.oauth.model.AuthUser;
|
||||
import me.zhyd.oauth.request.AuthRequest;
|
||||
|
||||
/**
|
||||
* 个人中心 API
|
||||
@ -53,10 +69,12 @@ import top.charles7c.cnadmin.system.service.UserService;
|
||||
@Validated
|
||||
@RestController
|
||||
@RequiredArgsConstructor
|
||||
@RequestMapping("/system/user/center")
|
||||
@RequestMapping("/system/user")
|
||||
public class UserCenterController {
|
||||
|
||||
private final UserService userService;
|
||||
private final UserSocialService userSocialService;
|
||||
private final AuthRequestFactory authRequestFactory;
|
||||
|
||||
@Operation(summary = "上传头像", description = "用户上传个人头像")
|
||||
@PostMapping("/avatar")
|
||||
@ -94,14 +112,45 @@ public class UserCenterController {
|
||||
String rawCurrentPassword =
|
||||
ExceptionUtils.exToNull(() -> SecureUtils.decryptByRsaPrivateKey(updateEmailRequest.getCurrentPassword()));
|
||||
ValidationUtils.throwIfBlank(rawCurrentPassword, "当前密码解密失败");
|
||||
|
||||
String captchaKey = RedisUtils.formatKey(CacheConsts.CAPTCHA_KEY_PREFIX, updateEmailRequest.getNewEmail());
|
||||
String captcha = RedisUtils.getCacheObject(captchaKey);
|
||||
ValidationUtils.throwIfBlank(captcha, "验证码已失效");
|
||||
ValidationUtils.throwIfNotEqualIgnoreCase(updateEmailRequest.getCaptcha(), captcha, "验证码错误");
|
||||
RedisUtils.deleteCacheObject(captchaKey);
|
||||
|
||||
userService.updateEmail(updateEmailRequest.getNewEmail(), rawCurrentPassword, LoginHelper.getUserId());
|
||||
return R.ok("修改成功");
|
||||
}
|
||||
|
||||
@Operation(summary = "查询绑定的第三方账号", description = "查询绑定的第三方账号")
|
||||
@GetMapping("/social")
|
||||
public List<UserSocialBindVO> listSocial() {
|
||||
List<UserSocialDO> userSocialList = userSocialService.listByUserId(LoginHelper.getUserId());
|
||||
return userSocialList.stream().map(userSocial -> {
|
||||
String source = userSocial.getSource();
|
||||
UserSocialBindVO userSocialBind = new UserSocialBindVO();
|
||||
userSocialBind.setSource(source);
|
||||
userSocialBind.setDescription(SocialSourceEnum.valueOf(source).getDescription());
|
||||
return userSocialBind;
|
||||
}).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Operation(summary = "绑定第三方账号", description = "绑定第三方账号")
|
||||
@Parameter(name = "source", description = "来源", example = "gitee", in = ParameterIn.PATH)
|
||||
@PostMapping("/social/{source}")
|
||||
public R bindSocial(@PathVariable String source, @RequestBody AuthCallback callback) {
|
||||
AuthRequest authRequest = authRequestFactory.get(source);
|
||||
AuthResponse<AuthUser> response = authRequest.login(callback);
|
||||
ValidationUtils.throwIf(!response.ok(), response.getMsg());
|
||||
AuthUser authUser = response.getData();
|
||||
userSocialService.bind(authUser, LoginHelper.getUserId());
|
||||
return R.ok("绑定成功");
|
||||
}
|
||||
|
||||
@Operation(summary = "解绑第三方账号", description = "解绑第三方账号")
|
||||
@Parameter(name = "source", description = "来源", example = "gitee", in = ParameterIn.PATH)
|
||||
@DeleteMapping("/social/{source}")
|
||||
public R unbindSocial(@PathVariable String source) {
|
||||
userSocialService.deleteBySourceAndUserId(source, LoginHelper.getUserId());
|
||||
return R.ok("解绑成功");
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user