feat(auth): enhance workspace handling and error feedback (#9118)
Add support for setting a user's default workspace during sign-in if a target workspace subdomain exists. Enhance error feedback by displaying authentication error messages using a Snackbar in the front-end and improving redirect logic for workspace-specific errors.
This commit is contained in:
@ -6,10 +6,16 @@ import { useIsLogged } from '@/auth/hooks/useIsLogged';
|
|||||||
import { isAppWaitingForFreshObjectMetadataState } from '@/object-metadata/states/isAppWaitingForFreshObjectMetadataState';
|
import { isAppWaitingForFreshObjectMetadataState } from '@/object-metadata/states/isAppWaitingForFreshObjectMetadataState';
|
||||||
import { AppPath } from '@/types/AppPath';
|
import { AppPath } from '@/types/AppPath';
|
||||||
import { useSetRecoilState } from 'recoil';
|
import { useSetRecoilState } from 'recoil';
|
||||||
|
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||||
|
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
||||||
|
import { isDefined } from 'twenty-ui';
|
||||||
|
|
||||||
export const VerifyEffect = () => {
|
export const VerifyEffect = () => {
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
const loginToken = searchParams.get('loginToken');
|
const loginToken = searchParams.get('loginToken');
|
||||||
|
const errorMessage = searchParams.get('errorMessage');
|
||||||
|
|
||||||
|
const { enqueueSnackBar } = useSnackBar();
|
||||||
|
|
||||||
const isLogged = useIsLogged();
|
const isLogged = useIsLogged();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@ -22,6 +28,11 @@ export const VerifyEffect = () => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const getTokens = async () => {
|
const getTokens = async () => {
|
||||||
|
if (isDefined(errorMessage)) {
|
||||||
|
enqueueSnackBar(errorMessage, {
|
||||||
|
variant: SnackBarVariant.Error,
|
||||||
|
});
|
||||||
|
}
|
||||||
if (!loginToken) {
|
if (!loginToken) {
|
||||||
navigate(AppPath.SignInUp);
|
navigate(AppPath.SignInUp);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@ -109,7 +109,9 @@ export class GoogleAuthController {
|
|||||||
if (err instanceof AuthException) {
|
if (err instanceof AuthException) {
|
||||||
return res.redirect(
|
return res.redirect(
|
||||||
this.domainManagerService.computeRedirectErrorUrl({
|
this.domainManagerService.computeRedirectErrorUrl({
|
||||||
subdomain: this.environmentService.get('DEFAULT_SUBDOMAIN'),
|
subdomain:
|
||||||
|
req.user.targetWorkspaceSubdomain ??
|
||||||
|
this.environmentService.get('DEFAULT_SUBDOMAIN'),
|
||||||
errorMessage: err.message,
|
errorMessage: err.message,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|||||||
@ -19,6 +19,7 @@ import {
|
|||||||
Workspace,
|
Workspace,
|
||||||
WorkspaceActivationStatus,
|
WorkspaceActivationStatus,
|
||||||
} from 'src/engine/core-modules/workspace/workspace.entity';
|
} from 'src/engine/core-modules/workspace/workspace.entity';
|
||||||
|
import { UserService } from 'src/engine/core-modules/user/services/user.service';
|
||||||
|
|
||||||
jest.mock('bcrypt');
|
jest.mock('bcrypt');
|
||||||
|
|
||||||
@ -34,6 +35,7 @@ const EnvironmentServiceGetMock = jest.fn();
|
|||||||
const WorkspaceCountMock = jest.fn();
|
const WorkspaceCountMock = jest.fn();
|
||||||
const WorkspaceCreateMock = jest.fn();
|
const WorkspaceCreateMock = jest.fn();
|
||||||
const WorkspaceSaveMock = jest.fn();
|
const WorkspaceSaveMock = jest.fn();
|
||||||
|
const WorkspaceFindOneMock = jest.fn();
|
||||||
|
|
||||||
describe('SignInUpService', () => {
|
describe('SignInUpService', () => {
|
||||||
let service: SignInUpService;
|
let service: SignInUpService;
|
||||||
@ -56,6 +58,7 @@ describe('SignInUpService', () => {
|
|||||||
count: WorkspaceCountMock,
|
count: WorkspaceCountMock,
|
||||||
create: WorkspaceCreateMock,
|
create: WorkspaceCreateMock,
|
||||||
save: WorkspaceSaveMock,
|
save: WorkspaceSaveMock,
|
||||||
|
findOne: WorkspaceFindOneMock,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -119,6 +122,12 @@ describe('SignInUpService', () => {
|
|||||||
generateSubdomain: jest.fn().mockReturnValue('testSubDomain'),
|
generateSubdomain: jest.fn().mockReturnValue('testSubDomain'),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
provide: UserService,
|
||||||
|
useValue: {
|
||||||
|
saveDefaultWorkspaceIfUserHasAccessOrThrow: jest.fn(),
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
}).compile();
|
}).compile();
|
||||||
|
|
||||||
@ -170,6 +179,9 @@ describe('SignInUpService', () => {
|
|||||||
workspaceInvitationFindInvitationByWorkspaceSubdomainAndUserEmailMock.mockReturnValueOnce(
|
workspaceInvitationFindInvitationByWorkspaceSubdomainAndUserEmailMock.mockReturnValueOnce(
|
||||||
undefined,
|
undefined,
|
||||||
);
|
);
|
||||||
|
WorkspaceFindOneMock.mockReturnValueOnce({
|
||||||
|
id: 'another-workspace',
|
||||||
|
});
|
||||||
|
|
||||||
const result = await service.signInUp({
|
const result = await service.signInUp({
|
||||||
email,
|
email,
|
||||||
@ -374,6 +386,10 @@ describe('SignInUpService', () => {
|
|||||||
|
|
||||||
EnvironmentServiceGetMock.mockReturnValueOnce(false);
|
EnvironmentServiceGetMock.mockReturnValueOnce(false);
|
||||||
|
|
||||||
|
WorkspaceFindOneMock.mockReturnValueOnce({
|
||||||
|
id: 'another-workspace',
|
||||||
|
});
|
||||||
|
|
||||||
(bcrypt.compare as jest.Mock).mockReturnValueOnce(true);
|
(bcrypt.compare as jest.Mock).mockReturnValueOnce(true);
|
||||||
|
|
||||||
await service.signInUp({
|
await service.signInUp({
|
||||||
|
|||||||
@ -33,6 +33,7 @@ import {
|
|||||||
import { workspaceValidator } from 'src/engine/core-modules/workspace/workspace.validate';
|
import { workspaceValidator } from 'src/engine/core-modules/workspace/workspace.validate';
|
||||||
import { getImageBufferFromUrl } from 'src/utils/image';
|
import { getImageBufferFromUrl } from 'src/utils/image';
|
||||||
import { WorkspaceAuthProvider } from 'src/engine/core-modules/workspace/types/workspace.type';
|
import { WorkspaceAuthProvider } from 'src/engine/core-modules/workspace/types/workspace.type';
|
||||||
|
import { UserService } from 'src/engine/core-modules/user/services/user.service';
|
||||||
|
|
||||||
export type SignInUpServiceInput = {
|
export type SignInUpServiceInput = {
|
||||||
email: string;
|
email: string;
|
||||||
@ -62,6 +63,7 @@ export class SignInUpService {
|
|||||||
private readonly httpService: HttpService,
|
private readonly httpService: HttpService,
|
||||||
private readonly environmentService: EnvironmentService,
|
private readonly environmentService: EnvironmentService,
|
||||||
private readonly domainManagerService: DomainManagerService,
|
private readonly domainManagerService: DomainManagerService,
|
||||||
|
private readonly userService: UserService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async signInUp({
|
async signInUp({
|
||||||
@ -177,6 +179,26 @@ export class SignInUpService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (targetWorkspaceSubdomain) {
|
||||||
|
const workspace = await this.workspaceRepository.findOne({
|
||||||
|
where: { subdomain: targetWorkspaceSubdomain },
|
||||||
|
select: ['id'],
|
||||||
|
});
|
||||||
|
|
||||||
|
workspaceValidator.assertIsExist(
|
||||||
|
workspace,
|
||||||
|
new AuthException(
|
||||||
|
'Workspace not found',
|
||||||
|
AuthExceptionCode.FORBIDDEN_EXCEPTION,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await this.userService.saveDefaultWorkspaceIfUserHasAccessOrThrow(
|
||||||
|
existingUser.id,
|
||||||
|
workspace.id,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return existingUser;
|
return existingUser;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user