43 changed files with 1492 additions and 148 deletions
@ -1,5 +1,6 @@ |
|||||
export { useAccountApi } from './useAccountApi'; |
export { useAccountApi } from './useAccountApi'; |
||||
export { useExternalLoginsApi } from './useExternalLoginsApi'; |
export { useExternalLoginsApi } from './useExternalLoginsApi'; |
||||
|
export { useLinkUsersApi } from './useLinkUsersApi'; |
||||
export { useMySessionApi } from './useMySessionApi'; |
export { useMySessionApi } from './useMySessionApi'; |
||||
export { useProfileApi } from './useProfileApi'; |
export { useProfileApi } from './useProfileApi'; |
||||
export { useScanQrCodeApi } from './useScanQrCodeApi'; |
export { useScanQrCodeApi } from './useScanQrCodeApi'; |
||||
|
|||||
@ -0,0 +1,54 @@ |
|||||
|
import type { ListResultDto } from '@abp/core'; |
||||
|
|
||||
|
import type { LinkUserDto, UnLinkUserInput } from '../types/link-user'; |
||||
|
|
||||
|
import { useRequest } from '@abp/request'; |
||||
|
|
||||
|
export function useLinkUsersApi() { |
||||
|
const { cancel, request } = useRequest(); |
||||
|
|
||||
|
/** |
||||
|
* 查询关联用户列表 |
||||
|
* @returns { Promise<ListResultDto<LinkUserDto>> } 关联用户列表 |
||||
|
*/ |
||||
|
function getListApi(): Promise<ListResultDto<LinkUserDto>> { |
||||
|
return request<ListResultDto<LinkUserDto>>('api/account/link-users', { |
||||
|
method: 'GET', |
||||
|
}); |
||||
|
} |
||||
|
/** |
||||
|
* 生成关联用户Token |
||||
|
*/ |
||||
|
function generateLinkTokenApi(): Promise<string> { |
||||
|
return request(`api/account/link-users/generate-link-token`, { |
||||
|
method: 'POST', |
||||
|
}); |
||||
|
} |
||||
|
/** |
||||
|
* 生成关联用户登录Token |
||||
|
*/ |
||||
|
function generateLinkLoginTokenApi(): Promise<string> { |
||||
|
return request(`api/account/link-users/generate-link-login-token`, { |
||||
|
method: 'POST', |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 取消用户关联 |
||||
|
* @param input 参数 |
||||
|
*/ |
||||
|
function unLinkApi(input: UnLinkUserInput): Promise<void> { |
||||
|
return request(`api/account/link-users/unlink`, { |
||||
|
method: 'POST', |
||||
|
data: input, |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
return { |
||||
|
cancel, |
||||
|
getListApi, |
||||
|
generateLinkTokenApi, |
||||
|
generateLinkLoginTokenApi, |
||||
|
unLinkApi, |
||||
|
}; |
||||
|
} |
||||
@ -0,0 +1,192 @@ |
|||||
|
<script setup lang="ts"> |
||||
|
import type { VxeGridProps } from '@abp/ui'; |
||||
|
|
||||
|
import type { LinkUserDto } from '../types'; |
||||
|
|
||||
|
import { h, ref } from 'vue'; |
||||
|
|
||||
|
import { confirm, useVbenModal } from '@vben/common-ui'; |
||||
|
import { $t } from '@vben/locales'; |
||||
|
|
||||
|
import { sortby } from '@abp/core'; |
||||
|
import { useMessage, useVbenVxeGrid } from '@abp/ui'; |
||||
|
import { |
||||
|
CheckOutlined, |
||||
|
CloseOutlined, |
||||
|
DeleteOutlined, |
||||
|
LoginOutlined, |
||||
|
PlusOutlined, |
||||
|
} from '@ant-design/icons-vue'; |
||||
|
import { Button } from 'ant-design-vue'; |
||||
|
|
||||
|
import { useLinkUsersApi } from '../api/useLinkUsersApi'; |
||||
|
|
||||
|
const emit = defineEmits<{ |
||||
|
(event: 'link', token: string): void; |
||||
|
(event: 'login', userId: string, tenantId?: string): void; |
||||
|
}>(); |
||||
|
|
||||
|
const message = useMessage(); |
||||
|
const { getListApi, generateLinkTokenApi, unLinkApi } = useLinkUsersApi(); |
||||
|
|
||||
|
const linkUsers = ref<LinkUserDto[]>([]); |
||||
|
|
||||
|
const [Modal, modalApi] = useVbenModal({ |
||||
|
async onOpenChange(isOpen) { |
||||
|
if (isOpen) { |
||||
|
await onInit(); |
||||
|
} |
||||
|
}, |
||||
|
title: $t('AbpAccount.LinkUser'), |
||||
|
}); |
||||
|
const gridOptions: VxeGridProps<LinkUserDto> = { |
||||
|
columns: [ |
||||
|
{ |
||||
|
align: 'left', |
||||
|
field: 'linkUserName', |
||||
|
sortable: true, |
||||
|
title: $t('AbpAccount.DisplayName:LinkUserName'), |
||||
|
}, |
||||
|
{ |
||||
|
align: 'left', |
||||
|
field: 'directlyLinked', |
||||
|
slots: { default: 'directlyLinked' }, |
||||
|
sortable: true, |
||||
|
title: $t('AbpAccount.DisplayName:DirectlyLinked'), |
||||
|
width: 150, |
||||
|
}, |
||||
|
{ |
||||
|
field: 'actions', |
||||
|
fixed: 'right', |
||||
|
slots: { default: 'actions' }, |
||||
|
title: $t('AbpUi.Actions'), |
||||
|
width: 220, |
||||
|
}, |
||||
|
], |
||||
|
exportConfig: {}, |
||||
|
keepSource: true, |
||||
|
proxyConfig: { |
||||
|
ajax: { |
||||
|
query: async ({ page, sort }) => { |
||||
|
let items = sortby(linkUsers.value, sort.field); |
||||
|
if (sort.order === 'desc') { |
||||
|
items = items.toReversed(); |
||||
|
} |
||||
|
const result = { |
||||
|
totalCount: linkUsers.value.length, |
||||
|
items: items.slice( |
||||
|
(page.currentPage - 1) * page.pageSize, |
||||
|
page.currentPage * page.pageSize, |
||||
|
), |
||||
|
}; |
||||
|
return new Promise((resolve) => { |
||||
|
resolve(result); |
||||
|
}); |
||||
|
}, |
||||
|
}, |
||||
|
response: { |
||||
|
total: 'totalCount', |
||||
|
list: 'items', |
||||
|
}, |
||||
|
}, |
||||
|
sortConfig: { |
||||
|
remote: true, |
||||
|
allowBtn: true, |
||||
|
}, |
||||
|
toolbarConfig: { |
||||
|
custom: true, |
||||
|
export: true, |
||||
|
zoom: true, |
||||
|
}, |
||||
|
}; |
||||
|
|
||||
|
const [Grid, gridApi] = useVbenVxeGrid({ |
||||
|
gridOptions, |
||||
|
}); |
||||
|
|
||||
|
async function onInit() { |
||||
|
const { items } = await getListApi(); |
||||
|
linkUsers.value = items; |
||||
|
gridApi.query(); |
||||
|
} |
||||
|
|
||||
|
async function onCreatLink() { |
||||
|
confirm({ |
||||
|
async beforeClose(scope) { |
||||
|
if (scope.isConfirm) { |
||||
|
const token = await generateLinkTokenApi(); |
||||
|
emit('link', token); |
||||
|
} |
||||
|
}, |
||||
|
centered: true, |
||||
|
content: $t('AbpAccount.LinkAccountWillBeCreateWarning'), |
||||
|
title: $t('AbpUi.AreYouSure'), |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
function onLoginByLink(record: LinkUserDto) { |
||||
|
emit('login', record.linkUserId, record.linkTenantId); |
||||
|
modalApi.close(); |
||||
|
} |
||||
|
|
||||
|
async function onDeleteLink(record: LinkUserDto) { |
||||
|
confirm({ |
||||
|
async beforeClose(scope) { |
||||
|
if (scope.isConfirm) { |
||||
|
await unLinkApi({ |
||||
|
userId: record.linkUserId, |
||||
|
tenantId: record.linkTenantId, |
||||
|
}); |
||||
|
message.success($t('AbpUi.DeletedSuccessfully')); |
||||
|
await onInit(); |
||||
|
} |
||||
|
}, |
||||
|
centered: true, |
||||
|
content: $t('AbpAccount.LinkAccountWillBeDeleteWarning', [ |
||||
|
record.linkUserName, |
||||
|
]), |
||||
|
title: $t('AbpUi.AreYouSure'), |
||||
|
}); |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<template> |
||||
|
<Modal> |
||||
|
<Grid> |
||||
|
<template #toolbar-tools> |
||||
|
<Button :icon="h(PlusOutlined)" type="primary" @click="onCreatLink"> |
||||
|
{{ $t('AbpAccount.LinkUser:New') }} |
||||
|
</Button> |
||||
|
</template> |
||||
|
<template #directlyLinked="{ row }"> |
||||
|
<div class="flex flex-row justify-center"> |
||||
|
<CheckOutlined v-if="row.directlyLinked" class="text-green-500" /> |
||||
|
<CloseOutlined v-else class="text-red-500" /> |
||||
|
</div> |
||||
|
</template> |
||||
|
<template #actions="{ row }"> |
||||
|
<div class="flex flex-row"> |
||||
|
<Button |
||||
|
:icon="h(LoginOutlined)" |
||||
|
block |
||||
|
type="link" |
||||
|
@click="onLoginByLink(row)" |
||||
|
> |
||||
|
{{ $t('AbpAccount.LinkUser:Login') }} |
||||
|
</Button> |
||||
|
<Button |
||||
|
:icon="h(DeleteOutlined)" |
||||
|
block |
||||
|
danger |
||||
|
type="link" |
||||
|
@click="onDeleteLink(row)" |
||||
|
> |
||||
|
{{ $t('AbpUi.Delete') }} |
||||
|
</Button> |
||||
|
</div> |
||||
|
</template> |
||||
|
</Grid> |
||||
|
</Modal> |
||||
|
</template> |
||||
|
|
||||
|
<style scoped></style> |
||||
@ -1,3 +1,4 @@ |
|||||
export { default as MyProfile } from './MyProfile.vue'; |
export { default as MyProfile } from './MyProfile.vue'; |
||||
export { default as MySetting } from './MySetting.vue'; |
export { default as MySetting } from './MySetting.vue'; |
||||
export { default as QrCodeLogin } from './QrCodeLogin.vue'; |
export { default as QrCodeLogin } from './QrCodeLogin.vue'; |
||||
|
export { default as UserLinkModal } from './UserLinkModal.vue'; |
||||
|
|||||
@ -1,6 +1,8 @@ |
|||||
export * from './account'; |
export * from './account'; |
||||
export * from './bind'; |
export * from './bind'; |
||||
export * from './external-logins'; |
export * from './external-logins'; |
||||
|
export * from './link-user'; |
||||
export * from './profile'; |
export * from './profile'; |
||||
export * from './token'; |
export * from './token'; |
||||
export * from './user'; |
export * from './user'; |
||||
|
export type { SigninRedirectArgs } from 'oidc-client-ts'; |
||||
|
|||||
@ -0,0 +1,14 @@ |
|||||
|
interface LinkUserDto { |
||||
|
directlyLinked: boolean; |
||||
|
linkTenantId?: string; |
||||
|
linkTenantName?: string; |
||||
|
linkUserId: string; |
||||
|
linkUserName: string; |
||||
|
} |
||||
|
|
||||
|
interface UnLinkUserInput { |
||||
|
tenantId?: string; |
||||
|
userId: string; |
||||
|
} |
||||
|
|
||||
|
export type { LinkUserDto, UnLinkUserInput }; |
||||
@ -0,0 +1,15 @@ |
|||||
|
using System; |
||||
|
|
||||
|
namespace LINGYUN.Abp.Account.Dto; |
||||
|
|
||||
|
public abstract class LinkUserBaseDto |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// 关联用户Id
|
||||
|
/// </summary>
|
||||
|
public Guid UserId { get; set; } |
||||
|
/// <summary>
|
||||
|
/// 关联租户Id
|
||||
|
/// </summary>
|
||||
|
public Guid? TenantId { get; set; } |
||||
|
} |
||||
@ -0,0 +1,29 @@ |
|||||
|
using System; |
||||
|
|
||||
|
namespace LINGYUN.Abp.Account.Dto; |
||||
|
/// <summary>
|
||||
|
/// 关联用户Dto
|
||||
|
/// </summary>
|
||||
|
public class LinkUserDto |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// 关联用户Id
|
||||
|
/// </summary>
|
||||
|
public Guid LinkUserId { get; set; } |
||||
|
/// <summary>
|
||||
|
/// 关联用户名
|
||||
|
/// </summary>
|
||||
|
public string LinkUserName { get; set; } |
||||
|
/// <summary>
|
||||
|
/// 关联租户Id
|
||||
|
/// </summary>
|
||||
|
public Guid? LinkTenantId { get; set; } |
||||
|
/// <summary>
|
||||
|
/// 关联租户名
|
||||
|
/// </summary>
|
||||
|
public string LinkTenantName { get; set; } |
||||
|
/// <summary>
|
||||
|
/// 直接关联
|
||||
|
/// </summary>
|
||||
|
public bool DirectlyLinked { get; set; } |
||||
|
} |
||||
@ -0,0 +1,15 @@ |
|||||
|
using System; |
||||
|
using System.ComponentModel.DataAnnotations; |
||||
|
|
||||
|
namespace LINGYUN.Abp.Account.Dto; |
||||
|
/// <summary>
|
||||
|
/// 关联用户Dto
|
||||
|
/// </summary>
|
||||
|
public class LinkUserInput : LinkUserBaseDto |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// 关联用户Token
|
||||
|
/// </summary>
|
||||
|
[Required] |
||||
|
public string Token { get; set; } |
||||
|
} |
||||
@ -0,0 +1,7 @@ |
|||||
|
namespace LINGYUN.Abp.Account.Dto; |
||||
|
/// <summary>
|
||||
|
/// 取消关联用户Dto
|
||||
|
/// </summary>
|
||||
|
public class UnLinkUserInput : LinkUserBaseDto |
||||
|
{ |
||||
|
} |
||||
@ -0,0 +1,14 @@ |
|||||
|
using System.ComponentModel.DataAnnotations; |
||||
|
|
||||
|
namespace LINGYUN.Abp.Account.Dto; |
||||
|
/// <summary>
|
||||
|
/// 验证关联用户TokenDto
|
||||
|
/// </summary>
|
||||
|
public class VerifyLinkTokenInput : LinkUserBaseDto |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// 关联用户Token
|
||||
|
/// </summary>
|
||||
|
[Required] |
||||
|
public string Token { get; set; } |
||||
|
} |
||||
@ -0,0 +1,27 @@ |
|||||
|
using System; |
||||
|
|
||||
|
namespace LINGYUN.Abp.Account.Dto; |
||||
|
|
||||
|
public class VerifyLinkUserDto |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// 关联用户Id
|
||||
|
/// </summary>
|
||||
|
public Guid? LinkUserId { get; set; } |
||||
|
/// <summary>
|
||||
|
/// 关联用户名
|
||||
|
/// </summary>
|
||||
|
public string LinkUserName { get; set; } |
||||
|
/// <summary>
|
||||
|
/// 关联租户Id
|
||||
|
/// </summary>
|
||||
|
public Guid? LinkTenantId { get; set; } |
||||
|
/// <summary>
|
||||
|
/// 关联租户名
|
||||
|
/// </summary>
|
||||
|
public string LinkTenantName { get; set; } |
||||
|
/// <summary>
|
||||
|
/// 是否已关联
|
||||
|
/// </summary>
|
||||
|
public bool IsLinked { get; set; } |
||||
|
} |
||||
@ -0,0 +1,5 @@ |
|||||
|
namespace LINGYUN.Abp.Account.Dto; |
||||
|
|
||||
|
public class VerifyLinkUserInput : LinkUserBaseDto |
||||
|
{ |
||||
|
} |
||||
@ -0,0 +1,57 @@ |
|||||
|
using LINGYUN.Abp.Account.Dto; |
||||
|
using System.Threading.Tasks; |
||||
|
using Volo.Abp.Application.Dtos; |
||||
|
using Volo.Abp.Application.Services; |
||||
|
|
||||
|
namespace LINGYUN.Abp.Account; |
||||
|
/// <summary>
|
||||
|
/// 关联用户应用服务接口
|
||||
|
/// </summary>
|
||||
|
public interface IIdentityLinkUserAppService : IApplicationService |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// 关联用户
|
||||
|
/// </summary>
|
||||
|
/// <param name="input"></param>
|
||||
|
/// <returns></returns>
|
||||
|
Task LinkAsync(LinkUserInput input); |
||||
|
/// <summary>
|
||||
|
/// 取消关联用户
|
||||
|
/// </summary>
|
||||
|
/// <param name="input"></param>
|
||||
|
/// <returns></returns>
|
||||
|
Task UnlinkAsync(UnLinkUserInput input); |
||||
|
/// <summary>
|
||||
|
/// 生成关联用户Token
|
||||
|
/// </summary>
|
||||
|
/// <returns></returns>
|
||||
|
Task<string> GenerateLinkTokenAsync(); |
||||
|
/// <summary>
|
||||
|
/// 生成关联用户登录Token
|
||||
|
/// </summary>
|
||||
|
/// <returns></returns>
|
||||
|
Task<string> GenerateLinkLoginTokenAsync(); |
||||
|
/// <summary>
|
||||
|
/// 获取已关联用户列表
|
||||
|
/// </summary>
|
||||
|
/// <returns></returns>
|
||||
|
Task<ListResultDto<LinkUserDto>> GetListAsync(); |
||||
|
/// <summary>
|
||||
|
/// 验证关联用户
|
||||
|
/// </summary>
|
||||
|
/// <param name="input"></param>
|
||||
|
/// <returns></returns>
|
||||
|
Task<VerifyLinkUserDto> VerifyLinkUserAsync(VerifyLinkUserInput input); |
||||
|
/// <summary>
|
||||
|
/// 验证关联用户Token
|
||||
|
/// </summary>
|
||||
|
/// <param name="input"></param>
|
||||
|
/// <returns></returns>
|
||||
|
Task<bool> VerifyLinkTokenAsync(VerifyLinkTokenInput input); |
||||
|
/// <summary>
|
||||
|
/// 验证关联用户登录Token
|
||||
|
/// </summary>
|
||||
|
/// <param name="input"></param>
|
||||
|
/// <returns></returns>
|
||||
|
Task<bool> VerifyLinkLoginTokenAsync(VerifyLinkTokenInput input); |
||||
|
} |
||||
@ -0,0 +1,170 @@ |
|||||
|
using LINGYUN.Abp.Account.Dto; |
||||
|
using Microsoft.AspNetCore.Authorization; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Linq; |
||||
|
using System.Threading.Tasks; |
||||
|
using Volo.Abp; |
||||
|
using Volo.Abp.Application.Dtos; |
||||
|
using Volo.Abp.Identity; |
||||
|
using Volo.Abp.MultiTenancy; |
||||
|
using Volo.Abp.Users; |
||||
|
|
||||
|
namespace LINGYUN.Abp.Account; |
||||
|
|
||||
|
[Authorize] |
||||
|
public class IdentityLinkUserAppService : AccountApplicationServiceBase, IIdentityLinkUserAppService |
||||
|
{ |
||||
|
protected IdentityLinkUserManager IdentityLinkUserManager { get; } |
||||
|
|
||||
|
protected ITenantStore TenantStore { get; } |
||||
|
|
||||
|
public IdentityLinkUserAppService( |
||||
|
IdentityLinkUserManager identityLinkUserManager, |
||||
|
ITenantStore tenantStore) |
||||
|
{ |
||||
|
IdentityLinkUserManager = identityLinkUserManager; |
||||
|
TenantStore = tenantStore; |
||||
|
} |
||||
|
|
||||
|
public async virtual Task<string> GenerateLinkLoginTokenAsync() |
||||
|
{ |
||||
|
return await IdentityLinkUserManager.GenerateLinkTokenAsync( |
||||
|
new IdentityLinkUserInfo(CurrentUser.GetId(), CurrentTenant.Id), |
||||
|
LinkUserTokenProviderConsts.LinkUserLoginTokenPurpose); |
||||
|
} |
||||
|
|
||||
|
public async virtual Task<string> GenerateLinkTokenAsync() |
||||
|
{ |
||||
|
return await IdentityLinkUserManager.GenerateLinkTokenAsync( |
||||
|
new IdentityLinkUserInfo(CurrentUser.GetId(), CurrentTenant.Id), |
||||
|
LinkUserTokenProviderConsts.LinkUserTokenPurpose); |
||||
|
} |
||||
|
|
||||
|
public async virtual Task<ListResultDto<LinkUserDto>> GetListAsync() |
||||
|
{ |
||||
|
var identityLinkUserDtos = new List<LinkUserDto>(); |
||||
|
var identityLinkUserInfo = new IdentityLinkUserInfo(CurrentUser.GetId(), CurrentTenant.Id); |
||||
|
var identityLinkUsers = await IdentityLinkUserManager.GetListAsync(identityLinkUserInfo, includeIndirect: true); |
||||
|
|
||||
|
var currentUserId = CurrentUser.GetId(); |
||||
|
var currentTenantId = CurrentTenant.Id; |
||||
|
|
||||
|
var allActiveTenants = (await TenantStore.GetListAsync()).Where(x => x.IsActive); |
||||
|
var allIdentityLinkUserDtos = identityLinkUsers |
||||
|
.SelectMany(x => new[] |
||||
|
{ |
||||
|
new LinkUserDto |
||||
|
{ |
||||
|
LinkTenantId = x.TargetTenantId, |
||||
|
LinkUserId = x.TargetUserId, |
||||
|
DirectlyLinked = (x.SourceTenantId == currentTenantId && x.SourceUserId == currentUserId) |
||||
|
|| (x.TargetTenantId == currentTenantId && x.TargetUserId == currentUserId) |
||||
|
}, |
||||
|
new LinkUserDto |
||||
|
{ |
||||
|
LinkTenantId = x.SourceTenantId, |
||||
|
LinkUserId = x.SourceUserId, |
||||
|
DirectlyLinked = (x.SourceTenantId == currentTenantId && x.SourceUserId == currentUserId) |
||||
|
|| (x.TargetTenantId == currentTenantId && x.TargetUserId == currentUserId) |
||||
|
} |
||||
|
}) |
||||
|
.Where(x => x.LinkTenantId != currentTenantId || x.LinkUserId != currentUserId) |
||||
|
.GroupBy(x => new { x.LinkTenantId, x.LinkUserId }) |
||||
|
.Select(g => g.FirstOrDefault(x => x.DirectlyLinked) ?? g.First()) |
||||
|
.ToList(); |
||||
|
|
||||
|
foreach (var identityLinkUserDto in allIdentityLinkUserDtos) |
||||
|
{ |
||||
|
var linkTenant = allActiveTenants.FirstOrDefault(x => x.Id == identityLinkUserDto.LinkTenantId); |
||||
|
using (CurrentTenant.Change(linkTenant?.Id, linkTenant?.Name)) |
||||
|
{ |
||||
|
var linkUser = await UserManager.FindByIdAsync(identityLinkUserDto.LinkUserId.ToString()); |
||||
|
if (linkUser != null) |
||||
|
{ |
||||
|
identityLinkUserDto.LinkUserName = linkUser.UserName; |
||||
|
identityLinkUserDtos.Add( |
||||
|
new LinkUserDto |
||||
|
{ |
||||
|
LinkTenantId = linkTenant?.Id, |
||||
|
LinkTenantName = linkTenant?.Name, |
||||
|
LinkUserId = linkUser.Id, |
||||
|
LinkUserName = linkUser.UserName, |
||||
|
DirectlyLinked = identityLinkUserDto.DirectlyLinked, |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return new ListResultDto<LinkUserDto>(identityLinkUserDtos); |
||||
|
} |
||||
|
|
||||
|
public async virtual Task LinkAsync(LinkUserInput input) |
||||
|
{ |
||||
|
var identityLinkUserInfo = new IdentityLinkUserInfo(input.UserId, input.TenantId); |
||||
|
if (await IdentityLinkUserManager.VerifyLinkTokenAsync(identityLinkUserInfo, input.Token, LinkUserTokenProviderConsts.LinkUserTokenPurpose)) |
||||
|
{ |
||||
|
await IdentityLinkUserManager.LinkAsync( |
||||
|
new IdentityLinkUserInfo(CurrentUser.GetId(), CurrentTenant.Id), |
||||
|
identityLinkUserInfo); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
throw new BusinessException( |
||||
|
LINGYUN.Abp.Identity.IdentityErrorCodes.LinkUserTokenInValid, |
||||
|
"Link user token is invalid!"); |
||||
|
} |
||||
|
|
||||
|
public async virtual Task UnlinkAsync(UnLinkUserInput input) |
||||
|
{ |
||||
|
await IdentityLinkUserManager.UnlinkAsync( |
||||
|
new IdentityLinkUserInfo(CurrentUser.GetId(), CurrentTenant.Id), |
||||
|
new IdentityLinkUserInfo(input.UserId, input.TenantId)); |
||||
|
} |
||||
|
|
||||
|
public async virtual Task<VerifyLinkUserDto> VerifyLinkUserAsync(VerifyLinkUserInput input) |
||||
|
{ |
||||
|
if (await IdentityLinkUserManager.IsLinkedAsync( |
||||
|
new IdentityLinkUserInfo(CurrentUser.GetId(), CurrentTenant.Id), |
||||
|
new IdentityLinkUserInfo(input.UserId, input.TenantId), |
||||
|
includeIndirect: true)) |
||||
|
{ |
||||
|
using (CurrentTenant.Change(input.TenantId)) |
||||
|
{ |
||||
|
TenantConfiguration tenant = null; |
||||
|
if (input.TenantId.HasValue) |
||||
|
{ |
||||
|
tenant = await TenantStore.FindAsync(input.TenantId.Value); |
||||
|
} |
||||
|
var user = await UserManager.FindByIdAsync(input.UserId.ToString()); |
||||
|
|
||||
|
return new VerifyLinkUserDto |
||||
|
{ |
||||
|
LinkTenantId = tenant?.Id, |
||||
|
LinkTenantName = tenant?.Name, |
||||
|
LinkUserId = user?.Id, |
||||
|
LinkUserName = user?.UserName, |
||||
|
IsLinked = user != null |
||||
|
}; |
||||
|
} |
||||
|
} |
||||
|
return new VerifyLinkUserDto { IsLinked = false }; |
||||
|
} |
||||
|
|
||||
|
[AllowAnonymous] |
||||
|
public async virtual Task<bool> VerifyLinkLoginTokenAsync(VerifyLinkTokenInput input) |
||||
|
{ |
||||
|
return await IdentityLinkUserManager.VerifyLinkTokenAsync( |
||||
|
new IdentityLinkUserInfo(input.UserId, input.TenantId), |
||||
|
input.Token, |
||||
|
LinkUserTokenProviderConsts.LinkUserLoginTokenPurpose); |
||||
|
} |
||||
|
|
||||
|
[AllowAnonymous] |
||||
|
public async virtual Task<bool> VerifyLinkTokenAsync(VerifyLinkTokenInput input) |
||||
|
{ |
||||
|
return await IdentityLinkUserManager.VerifyLinkTokenAsync( |
||||
|
new IdentityLinkUserInfo(input.UserId, input.TenantId), |
||||
|
input.Token, |
||||
|
LinkUserTokenProviderConsts.LinkUserTokenPurpose); |
||||
|
} |
||||
|
} |
||||
@ -1,77 +1,77 @@ |
|||||
using Microsoft.AspNetCore.Mvc; |
using Microsoft.AspNetCore.Mvc; |
||||
using System.Threading.Tasks; |
using System.Threading.Tasks; |
||||
using Volo.Abp; |
using Volo.Abp; |
||||
using Volo.Abp.Account; |
using Volo.Abp.Account; |
||||
using Volo.Abp.Application.Dtos; |
using Volo.Abp.Application.Dtos; |
||||
using Volo.Abp.AspNetCore.Mvc; |
using Volo.Abp.AspNetCore.Mvc; |
||||
|
|
||||
namespace LINGYUN.Abp.Account; |
namespace LINGYUN.Abp.Account; |
||||
|
|
||||
[RemoteService(Name = AccountRemoteServiceConsts.RemoteServiceName)] |
[RemoteService(Name = AccountRemoteServiceConsts.RemoteServiceName)] |
||||
[Area("account")] |
[Area("account")] |
||||
[Route("api/account")] |
[Route("api/account")] |
||||
public class AccountController : AbpControllerBase, IAccountAppService |
public class AccountController : AbpControllerBase, IAccountAppService |
||||
{ |
{ |
||||
protected IAccountAppService AccountAppService { get; } |
protected IAccountAppService AccountAppService { get; } |
||||
public AccountController( |
public AccountController( |
||||
IAccountAppService accountAppService) |
IAccountAppService accountAppService) |
||||
{ |
{ |
||||
AccountAppService = accountAppService; |
AccountAppService = accountAppService; |
||||
} |
} |
||||
|
|
||||
[HttpPost] |
[HttpPost] |
||||
[Route("wechat/register")] |
[Route("wechat/register")] |
||||
public async virtual Task RegisterAsync(WeChatRegisterDto input) |
public async virtual Task RegisterAsync(WeChatRegisterDto input) |
||||
{ |
{ |
||||
await AccountAppService.RegisterAsync(input); |
await AccountAppService.RegisterAsync(input); |
||||
} |
} |
||||
|
|
||||
[HttpPost] |
[HttpPost] |
||||
[Route("phone/register")] |
[Route("phone/register")] |
||||
public async virtual Task RegisterAsync(PhoneRegisterDto input) |
public async virtual Task RegisterAsync(PhoneRegisterDto input) |
||||
{ |
{ |
||||
await AccountAppService.RegisterAsync(input); |
await AccountAppService.RegisterAsync(input); |
||||
} |
} |
||||
|
|
||||
[HttpPut] |
[HttpPut] |
||||
[Route("phone/reset-password")] |
[Route("phone/reset-password")] |
||||
public async virtual Task ResetPasswordAsync(PhoneResetPasswordDto input) |
public async virtual Task ResetPasswordAsync(PhoneResetPasswordDto input) |
||||
{ |
{ |
||||
await AccountAppService.ResetPasswordAsync(input); |
await AccountAppService.ResetPasswordAsync(input); |
||||
} |
} |
||||
|
|
||||
[HttpPost] |
[HttpPost] |
||||
[Route("phone/send-signin-code")] |
[Route("phone/send-signin-code")] |
||||
public async virtual Task SendPhoneSigninCodeAsync(SendPhoneSigninCodeDto input) |
public async virtual Task SendPhoneSigninCodeAsync(SendPhoneSigninCodeDto input) |
||||
{ |
{ |
||||
await AccountAppService.SendPhoneSigninCodeAsync(input); |
await AccountAppService.SendPhoneSigninCodeAsync(input); |
||||
} |
} |
||||
|
|
||||
[HttpPost] |
[HttpPost] |
||||
[Route("email/send-signin-code")] |
[Route("email/send-signin-code")] |
||||
public async virtual Task SendEmailSigninCodeAsync(SendEmailSigninCodeDto input) |
public async virtual Task SendEmailSigninCodeAsync(SendEmailSigninCodeDto input) |
||||
{ |
{ |
||||
await AccountAppService.SendEmailSigninCodeAsync(input); |
await AccountAppService.SendEmailSigninCodeAsync(input); |
||||
} |
} |
||||
|
|
||||
[HttpPost] |
[HttpPost] |
||||
[Route("phone/send-register-code")] |
[Route("phone/send-register-code")] |
||||
public async virtual Task SendPhoneRegisterCodeAsync(SendPhoneRegisterCodeDto input) |
public async virtual Task SendPhoneRegisterCodeAsync(SendPhoneRegisterCodeDto input) |
||||
{ |
{ |
||||
await AccountAppService.SendPhoneRegisterCodeAsync(input); |
await AccountAppService.SendPhoneRegisterCodeAsync(input); |
||||
} |
} |
||||
|
|
||||
[HttpPost] |
[HttpPost] |
||||
[Route("phone/send-password-reset-code")] |
[Route("phone/send-password-reset-code")] |
||||
public async virtual Task SendPhoneResetPasswordCodeAsync(SendPhoneResetPasswordCodeDto input) |
public async virtual Task SendPhoneResetPasswordCodeAsync(SendPhoneResetPasswordCodeDto input) |
||||
{ |
{ |
||||
await AccountAppService.SendPhoneResetPasswordCodeAsync(input); |
await AccountAppService.SendPhoneResetPasswordCodeAsync(input); |
||||
} |
} |
||||
|
|
||||
[HttpGet] |
[HttpGet] |
||||
[Route("two-factor-providers")] |
[Route("two-factor-providers")] |
||||
public async virtual Task<ListResultDto<NameValue>> GetTwoFactorProvidersAsync(GetTwoFactorProvidersInput input) |
public async virtual Task<ListResultDto<NameValue>> GetTwoFactorProvidersAsync(GetTwoFactorProvidersInput input) |
||||
{ |
{ |
||||
return await AccountAppService.GetTwoFactorProvidersAsync(input); |
return await AccountAppService.GetTwoFactorProvidersAsync(input); |
||||
} |
} |
||||
} |
} |
||||
|
|||||
@ -0,0 +1,70 @@ |
|||||
|
using LINGYUN.Abp.Account.Dto; |
||||
|
using Microsoft.AspNetCore.Mvc; |
||||
|
using System.Threading.Tasks; |
||||
|
using Volo.Abp; |
||||
|
using Volo.Abp.Account; |
||||
|
using Volo.Abp.Application.Dtos; |
||||
|
using Volo.Abp.AspNetCore.Mvc; |
||||
|
|
||||
|
namespace LINGYUN.Abp.Account; |
||||
|
|
||||
|
[RemoteService(Name = AccountRemoteServiceConsts.RemoteServiceName)] |
||||
|
[Area("account")] |
||||
|
[Route("api/account/link-users")] |
||||
|
public class IdentityLinkUserController(IIdentityLinkUserAppService _service) : AbpControllerBase, IIdentityLinkUserAppService |
||||
|
{ |
||||
|
[HttpPost] |
||||
|
[Route("generate-link-login-token")] |
||||
|
public virtual Task<string> GenerateLinkLoginTokenAsync() |
||||
|
{ |
||||
|
return _service.GenerateLinkLoginTokenAsync(); |
||||
|
} |
||||
|
|
||||
|
[HttpPost] |
||||
|
[Route("generate-link-token")] |
||||
|
public virtual Task<string> GenerateLinkTokenAsync() |
||||
|
{ |
||||
|
return _service.GenerateLinkTokenAsync(); |
||||
|
} |
||||
|
|
||||
|
[HttpGet] |
||||
|
public virtual Task<ListResultDto<LinkUserDto>> GetListAsync() |
||||
|
{ |
||||
|
return _service.GetListAsync(); |
||||
|
} |
||||
|
|
||||
|
[HttpPost] |
||||
|
[Route("link")] |
||||
|
public virtual Task LinkAsync(LinkUserInput input) |
||||
|
{ |
||||
|
return _service.LinkAsync(input); |
||||
|
} |
||||
|
|
||||
|
[HttpPost] |
||||
|
[Route("unlink")] |
||||
|
public virtual Task UnlinkAsync(UnLinkUserInput input) |
||||
|
{ |
||||
|
return _service.UnlinkAsync(input); |
||||
|
} |
||||
|
|
||||
|
[HttpPost] |
||||
|
[Route("verify-link-user")] |
||||
|
public virtual Task<VerifyLinkUserDto> VerifyLinkUserAsync(VerifyLinkUserInput input) |
||||
|
{ |
||||
|
return _service.VerifyLinkUserAsync(input); |
||||
|
} |
||||
|
|
||||
|
[HttpPost] |
||||
|
[Route("verify-link-login-token")] |
||||
|
public virtual Task<bool> VerifyLinkLoginTokenAsync(VerifyLinkTokenInput input) |
||||
|
{ |
||||
|
return _service.VerifyLinkLoginTokenAsync(input); |
||||
|
} |
||||
|
|
||||
|
[HttpPost] |
||||
|
[Route("verify-link-token")] |
||||
|
public virtual Task<bool> VerifyLinkTokenAsync(VerifyLinkTokenInput input) |
||||
|
{ |
||||
|
return _service.VerifyLinkTokenAsync(input); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,26 @@ |
|||||
|
@page |
||||
|
@using Microsoft.AspNetCore.Mvc.Localization; |
||||
|
@using Volo.Abp.Account.Localization; |
||||
|
@using Volo.Abp.MultiTenancy; |
||||
|
@using Volo.Abp.Users; |
||||
|
@model LINGYUN.Abp.Account.Web.Pages.Account.LinkLoggedModel |
||||
|
@inject Volo.Abp.AspNetCore.Mvc.UI.Layout.IPageLayout PageLayout |
||||
|
@inject IHtmlLocalizer<AccountResource> L |
||||
|
@inject ICurrentUser CurrentUser |
||||
|
@inject ICurrentTenant CurrentTenant |
||||
|
@{ |
||||
|
PageLayout.Content.Title = L["LinkLogged"].Value; |
||||
|
} |
||||
|
|
||||
|
<div> |
||||
|
<div class="gap-2 d-grid"> |
||||
|
<a href="@Url.Content(await Model.GetReturnUrlAsync(Model.ReturnUrl, Model.ReturnUrlHash))" class="mb-3 btn btn-primary"> @L["StayWithCurrentAccount"]</a> |
||||
|
</div> |
||||
|
<form method="post" asp-page="/Account/LinkLogged" id="linkUserLoginForm" asp-route-returnUrl="@Model.ReturnUrl" asp-route-returnUrlHash="@Model.ReturnUrlHash"> |
||||
|
<input asp-for="LinkTenantId"> |
||||
|
<input asp-for="LinkUserId"> |
||||
|
<div class="gap-2 d-grid"> |
||||
|
<abp-button class="btn-secondary">@L["LogInUsingYourProviderAccount", Model.LinkTenantAndUserName] →</abp-button> |
||||
|
</div> |
||||
|
</form> |
||||
|
</div> |
||||
@ -0,0 +1,106 @@ |
|||||
|
using Microsoft.AspNetCore.Authentication; |
||||
|
using Microsoft.AspNetCore.Authorization; |
||||
|
using Microsoft.AspNetCore.Identity; |
||||
|
using Microsoft.AspNetCore.Mvc; |
||||
|
using System; |
||||
|
using System.Threading.Tasks; |
||||
|
using Volo.Abp.Account.Web.Pages.Account; |
||||
|
using Volo.Abp.Identity; |
||||
|
using Volo.Abp.Security.Claims; |
||||
|
|
||||
|
namespace LINGYUN.Abp.Account.Web.Pages.Account; |
||||
|
|
||||
|
[Authorize] |
||||
|
public class LinkLoggedModel : AccountPageModel |
||||
|
{ |
||||
|
[BindProperty(SupportsGet = true)] |
||||
|
public string ReturnUrl { get; set; } |
||||
|
|
||||
|
[BindProperty(SupportsGet = true)] |
||||
|
public string ReturnUrlHash { get; set; } |
||||
|
|
||||
|
[HiddenInput] |
||||
|
[BindProperty(SupportsGet = true)] |
||||
|
public Guid LinkUserId { get; set; } |
||||
|
|
||||
|
[HiddenInput] |
||||
|
[BindProperty(SupportsGet = true)] |
||||
|
public Guid? LinkTenantId { get; set; } |
||||
|
|
||||
|
public string LinkTenantAndUserName { get; set; } |
||||
|
|
||||
|
protected ICurrentPrincipalAccessor CurrentPrincipalAccessor => LazyServiceProvider.LazyGetRequiredService<ICurrentPrincipalAccessor>(); |
||||
|
public IIdentityLinkUserAppService IdentityLinkUserAppService => LazyServiceProvider.LazyGetRequiredService<IIdentityLinkUserAppService>(); |
||||
|
public IdentityDynamicClaimsPrincipalContributorCache IdentityDynamicClaimsPrincipalContributorCache => LazyServiceProvider.LazyGetRequiredService<IdentityDynamicClaimsPrincipalContributorCache>(); |
||||
|
|
||||
|
public virtual async Task<IActionResult> OnGetAsync() |
||||
|
{ |
||||
|
var validLinkUser = await IdentityLinkUserAppService.VerifyLinkUserAsync( |
||||
|
new Dto.VerifyLinkUserInput |
||||
|
{ |
||||
|
UserId = LinkUserId, |
||||
|
TenantId = LinkTenantId, |
||||
|
}); |
||||
|
if (!validLinkUser.IsLinked) |
||||
|
{ |
||||
|
return await RedirectToLoginPageAsync(); |
||||
|
} |
||||
|
LinkTenantAndUserName = !validLinkUser.LinkTenantName.IsNullOrWhiteSpace() |
||||
|
? $"{validLinkUser.LinkTenantName}\\{validLinkUser.LinkUserName}" |
||||
|
: validLinkUser.LinkUserName; |
||||
|
|
||||
|
return Page(); |
||||
|
} |
||||
|
|
||||
|
public async virtual Task<IActionResult> OnPostAsync() |
||||
|
{ |
||||
|
if (LinkUserId == CurrentUser.Id && LinkTenantId == CurrentTenant.Id) |
||||
|
{ |
||||
|
return await RedirectSafelyAsync(ReturnUrl, ReturnUrlHash); |
||||
|
} |
||||
|
|
||||
|
using (CurrentTenant.Change(LinkTenantId)) |
||||
|
{ |
||||
|
var sourceUser = await UserManager.GetByIdAsync(LinkUserId); |
||||
|
using (CurrentPrincipalAccessor.Change(await SignInManager.CreateUserPrincipalAsync(sourceUser))) |
||||
|
{ |
||||
|
var validLinkUser = await IdentityLinkUserAppService.VerifyLinkUserAsync( |
||||
|
new Dto.VerifyLinkUserInput |
||||
|
{ |
||||
|
UserId = LinkUserId, |
||||
|
TenantId = LinkTenantId, |
||||
|
}); |
||||
|
if (validLinkUser.IsLinked) |
||||
|
{ |
||||
|
var isPersistent = (await HttpContext.AuthenticateAsync(IdentityConstants.ApplicationScheme))?.Properties?.IsPersistent ?? false; |
||||
|
await SignInManager.SignOutAsync(); |
||||
|
using (CurrentTenant.Change(LinkTenantId)) |
||||
|
{ |
||||
|
var targetUser = await UserManager.GetByIdAsync(LinkUserId); |
||||
|
await SignInManager.SignInAsync(targetUser, isPersistent); |
||||
|
await IdentityDynamicClaimsPrincipalContributorCache.ClearAsync(targetUser.Id, targetUser.TenantId); |
||||
|
} |
||||
|
|
||||
|
return await RedirectSafelyAsync(ReturnUrl, ReturnUrlHash); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
Alerts.Warning(L["Volo.Abp.Identity:InvalidToken"]); |
||||
|
return Page(); |
||||
|
} |
||||
|
|
||||
|
public virtual Task<string> GetReturnUrlAsync(string returnUrl, string returnUrlHash) |
||||
|
{ |
||||
|
return base.GetRedirectUrlAsync(returnUrl, returnUrlHash); |
||||
|
} |
||||
|
|
||||
|
protected virtual Task<IActionResult> RedirectToLoginPageAsync() |
||||
|
{ |
||||
|
return Task.FromResult<IActionResult>(RedirectToPage("./Login", new |
||||
|
{ |
||||
|
ReturnUrl, |
||||
|
ReturnUrlHash |
||||
|
})); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,37 @@ |
|||||
|
using Microsoft.AspNetCore; |
||||
|
using OpenIddict.Server; |
||||
|
using System.Linq; |
||||
|
using System.Threading.Tasks; |
||||
|
using static OpenIddict.Server.OpenIddictServerEvents; |
||||
|
|
||||
|
namespace LINGYUN.Abp.OpenIddict.LinkUser; |
||||
|
|
||||
|
public class LinkUserAuthorizationHandler : IOpenIddictServerHandler<ValidateAuthorizationRequestContext> |
||||
|
{ |
||||
|
public static OpenIddictServerHandlerDescriptor Descriptor { get; } |
||||
|
= OpenIddictServerHandlerDescriptor.CreateBuilder<ValidateAuthorizationRequestContext>() |
||||
|
.UseScopedHandler<LinkUserAuthorizationHandler>() |
||||
|
.SetOrder(2000) |
||||
|
.SetType(OpenIddictServerHandlerType.Custom) |
||||
|
.Build(); |
||||
|
|
||||
|
public async ValueTask HandleAsync(ValidateAuthorizationRequestContext context) |
||||
|
{ |
||||
|
var request = context.Transaction.GetHttpRequest(); |
||||
|
if (request == null) |
||||
|
{ |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
var linkUserId = request.Query["LinkUserId"].FirstOrDefault(); |
||||
|
var linkToken = request.Query["LinkToken"].FirstOrDefault(); |
||||
|
|
||||
|
if (string.IsNullOrEmpty(linkUserId) || string.IsNullOrEmpty(linkToken)) |
||||
|
{ |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
context.Transaction.SetProperty("LinkUserId", linkUserId); |
||||
|
context.Transaction.SetProperty("LinkToken", linkToken); |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue