You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
104 lines
5.8 KiB
104 lines
5.8 KiB
package com.mydreamjob.auth.identity;
|
|
|
|
import com.mydreamjob.audit.AuditContext;
|
|
import com.mydreamjob.audit.AuditService;
|
|
import com.mydreamjob.auth.*;
|
|
import com.mydreamjob.auth.company.CompanyRegistrationProperties;
|
|
import com.mydreamjob.auth.company.CompanyRegistrationRequest.CompanyDetails;
|
|
import com.mydreamjob.company.Company;
|
|
import com.mydreamjob.company.CompanyRepository;
|
|
import com.mydreamjob.company.CompanyOnboardingService;
|
|
import com.mydreamjob.deletion.DeletionStatus;
|
|
import com.mydreamjob.common.exception.BusinessException;
|
|
import com.mydreamjob.common.exception.ErrorCode;
|
|
import com.mydreamjob.deletion.AccountDeletionRepository;
|
|
import jakarta.validation.Validator;
|
|
import lombok.RequiredArgsConstructor;
|
|
import org.springframework.dao.DataAccessException;
|
|
import org.springframework.dao.DataIntegrityViolationException;
|
|
import org.springframework.stereotype.Service;
|
|
import org.springframework.transaction.annotation.Transactional;
|
|
import java.time.Clock;
|
|
import java.time.Instant;
|
|
import java.time.LocalDateTime;
|
|
import java.util.HashSet;
|
|
import java.util.Set;
|
|
|
|
@Service
|
|
@RequiredArgsConstructor
|
|
public class IdentitySelectionService {
|
|
private final UserRepository users;
|
|
private final CompanyRepository companies;
|
|
private final AuthSessionService sessions;
|
|
private final AuditService audit;
|
|
private final AccountDeletionRepository deletions;
|
|
private final CompanyRegistrationProperties properties;
|
|
private final CompanyOnboardingService onboarding;
|
|
private final Validator validator;
|
|
private final Clock clock;
|
|
|
|
@Transactional
|
|
public IdentitySelectionResponse select(Long userId, String sessionId, IdentitySelectionRequest request,
|
|
AuditContext context) {
|
|
if (request == null || !validator.validate(request).isEmpty()) {
|
|
throw new BusinessException(ErrorCode.BAD_REQUEST);
|
|
}
|
|
try {
|
|
// 与刷新/密码变更相同:users -> auth_tokens;锁内重新确认会话,关闭 TOCTOU。
|
|
User user = users.findByIdForUpdate(userId).orElseThrow(InvalidTokenException::new);
|
|
Instant now = clock.instant();
|
|
if (!Boolean.TRUE.equals(user.getActive()) || !sessions.isSessionActive(userId, sessionId, now)) {
|
|
throw new InvalidTokenException();
|
|
}
|
|
if (deletions.findByUserId(userId).filter(d -> d.getStatus() != DeletionStatus.NONE).isPresent()) {
|
|
throw new BusinessException(ErrorCode.IDENTITY_SELECTION_CONFLICT);
|
|
}
|
|
User.Role target = request.getIdentity() == IdentitySelectionRequest.Identity.STUDENT
|
|
? User.Role.STUDENT : User.Role.COMPANY;
|
|
if (user.getRole() != User.Role.UNASSIGNED) {
|
|
if (user.getRole() != target || user.getRoles() == null || !user.getRoles().equals(Set.of(target))) {
|
|
throw new BusinessException(ErrorCode.IDENTITY_SELECTION_CONFLICT);
|
|
}
|
|
Company company = target == User.Role.COMPANY ? companies.findByOwnerUserId(userId)
|
|
.orElseThrow(() -> new BusinessException(ErrorCode.IDENTITY_SELECTION_CONFLICT)) : null;
|
|
// 不重复更改资料/颁发会话/审计;客户端可使用已有凭证或重新登录。
|
|
return response(user, company, null, true);
|
|
}
|
|
if (user.getRoles() == null || !user.getRoles().isEmpty() || companies.existsByOwnerUserId(userId)) {
|
|
throw new BusinessException(ErrorCode.IDENTITY_SELECTION_CONFLICT);
|
|
}
|
|
if (target == User.Role.COMPANY && !properties.isEnabled()) {
|
|
throw new BusinessException(ErrorCode.COMPANY_REGISTRATION_DISABLED);
|
|
}
|
|
user.setRole(target);
|
|
user.setRoles(new HashSet<>(Set.of(target)));
|
|
users.saveAndFlush(user);
|
|
Company company = null;
|
|
if (target == User.Role.COMPANY) {
|
|
CompanyDetails d = request.getCompany();
|
|
company = companies.saveAndFlush(Company.builder().ownerUserId(userId).name(d.getName())
|
|
.logoUrl(d.getLogoUrl()).industry(d.getIndustry()).description(d.getDescription())
|
|
.website(d.getWebsite()).address(d.getAddress()).verified(false).build());
|
|
}
|
|
// modifying 会清理 persistence context;上面的角色与资料已 flush,锁仍持有。
|
|
sessions.revokeAllUserSessions(userId, "IDENTITY_SELECTED", now);
|
|
AuthSessionService.SessionTokens tokens = sessions.createSession(user, now, context);
|
|
audit.logRequired(userId, "IDENTITY_SELECTED", "USER", userId,
|
|
"{\"result\":\"SUCCESS\"}", context, LocalDateTime.ofInstant(now, AuthSessionService.BUSINESS_ZONE));
|
|
return response(user, company, tokens, false);
|
|
} catch (DataIntegrityViolationException e) {
|
|
throw new BusinessException(ErrorCode.IDENTITY_SELECTION_CONFLICT);
|
|
} catch (DataAccessException e) {
|
|
throw new BusinessException(ErrorCode.IDENTITY_SELECTION_DEPENDENCY_UNAVAILABLE);
|
|
}
|
|
}
|
|
|
|
private IdentitySelectionResponse response(User user, Company company, AuthSessionService.SessionTokens tokens,
|
|
boolean reauthenticate) {
|
|
return new IdentitySelectionResponse(user.getId(), user.getRole().name(), false,
|
|
company == null ? null : company.getId(), company == null ? null :
|
|
(tokens == null ? onboarding.get(user.getId()).onboardingStatus() : "UNVERIFIED"),
|
|
tokens == null ? null : tokens.accessToken(), tokens == null ? null : tokens.refreshToken(),
|
|
tokens == null ? null : AuthService.EXPIRES_IN_SECONDS, reauthenticate);
|
|
}
|
|
}
|
|
|