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:
Antoine Moreaux 2024-12-18 16:46:25 +01:00 committed by GitHub
parent 01fc70da0f
commit 550756c2bf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 52 additions and 1 deletions

View File

@ -6,10 +6,16 @@ import { useIsLogged } from '@/auth/hooks/useIsLogged';
import { isAppWaitingForFreshObjectMetadataState } from '@/object-metadata/states/isAppWaitingForFreshObjectMetadataState';
import { AppPath } from '@/types/AppPath';
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 = () => {
const [searchParams] = useSearchParams();
const loginToken = searchParams.get('loginToken');
const errorMessage = searchParams.get('errorMessage');
const { enqueueSnackBar } = useSnackBar();
const isLogged = useIsLogged();
const navigate = useNavigate();
@ -22,6 +28,11 @@ export const VerifyEffect = () => {
useEffect(() => {
const getTokens = async () => {
if (isDefined(errorMessage)) {
enqueueSnackBar(errorMessage, {
variant: SnackBarVariant.Error,
});
}
if (!loginToken) {
navigate(AppPath.SignInUp);
} else {

View File

@ -109,7 +109,9 @@ export class GoogleAuthController {
if (err instanceof AuthException) {
return res.redirect(
this.domainManagerService.computeRedirectErrorUrl({
subdomain: this.environmentService.get('DEFAULT_SUBDOMAIN'),
subdomain:
req.user.targetWorkspaceSubdomain ??
this.environmentService.get('DEFAULT_SUBDOMAIN'),
errorMessage: err.message,
}),
);

View File

@ -19,6 +19,7 @@ import {
Workspace,
WorkspaceActivationStatus,
} from 'src/engine/core-modules/workspace/workspace.entity';
import { UserService } from 'src/engine/core-modules/user/services/user.service';
jest.mock('bcrypt');
@ -34,6 +35,7 @@ const EnvironmentServiceGetMock = jest.fn();
const WorkspaceCountMock = jest.fn();
const WorkspaceCreateMock = jest.fn();
const WorkspaceSaveMock = jest.fn();
const WorkspaceFindOneMock = jest.fn();
describe('SignInUpService', () => {
let service: SignInUpService;
@ -56,6 +58,7 @@ describe('SignInUpService', () => {
count: WorkspaceCountMock,
create: WorkspaceCreateMock,
save: WorkspaceSaveMock,
findOne: WorkspaceFindOneMock,
},
},
{
@ -119,6 +122,12 @@ describe('SignInUpService', () => {
generateSubdomain: jest.fn().mockReturnValue('testSubDomain'),
},
},
{
provide: UserService,
useValue: {
saveDefaultWorkspaceIfUserHasAccessOrThrow: jest.fn(),
},
},
],
}).compile();
@ -170,6 +179,9 @@ describe('SignInUpService', () => {
workspaceInvitationFindInvitationByWorkspaceSubdomainAndUserEmailMock.mockReturnValueOnce(
undefined,
);
WorkspaceFindOneMock.mockReturnValueOnce({
id: 'another-workspace',
});
const result = await service.signInUp({
email,
@ -374,6 +386,10 @@ describe('SignInUpService', () => {
EnvironmentServiceGetMock.mockReturnValueOnce(false);
WorkspaceFindOneMock.mockReturnValueOnce({
id: 'another-workspace',
});
(bcrypt.compare as jest.Mock).mockReturnValueOnce(true);
await service.signInUp({

View File

@ -33,6 +33,7 @@ import {
import { workspaceValidator } from 'src/engine/core-modules/workspace/workspace.validate';
import { getImageBufferFromUrl } from 'src/utils/image';
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 = {
email: string;
@ -62,6 +63,7 @@ export class SignInUpService {
private readonly httpService: HttpService,
private readonly environmentService: EnvironmentService,
private readonly domainManagerService: DomainManagerService,
private readonly userService: UserService,
) {}
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;
}