package keycloak import ( "context" "errors" "time" "github.com/golang-jwt/jwt/v5" "git-molva.ru/Molva/molva-backend/services/api_gateway/internal/auth" "github.com/Nerzal/gocloak/v13" ) func (k *Client) RegisterUser(ctx context.Context, request auth.RegisterUserRequest) (*auth.RegisterUserResponse, error) { attrs := make(map[string][]string) if request.User.Patronymic != nil { attrs[PatronymicAttr] = []string{*request.User.Patronymic} } for key, value := range request.User.Permissions { attrs[key] = []string{value} } for _, perm := range permissionsSlice { if _, ok := attrs[perm]; !ok { return nil, errors.Join(ErrBadRequest, ErrMissingPermission) } } client, err := k.client.LoginClient(ctx, k.clientId, k.clientSecret, k.realm) if err != nil { return nil, k.handleError(err) } userGroup, err := k.getUserGroupByType(request.UserType) if err != nil { return nil, err } emailVerified := false if k.env != "production" { emailVerified = true } userId, err := k.client.CreateUser( ctx, client.AccessToken, k.realm, gocloak.User{ Email: gocloak.StringP(request.User.Email), EmailVerified: &emailVerified, FirstName: gocloak.StringP(request.User.FirstName), LastName: gocloak.StringP(request.User.SecondName), Groups: &[]string{userGroup}, Enabled: gocloak.BoolP(true), Attributes: &attrs, }) if err != nil { return nil, k.handleError(err) } if err = k.client.SetPassword( ctx, client.AccessToken, userId, k.realm, request.User.Password, false, ); err != nil { return nil, k.handleError(err) } return &auth.RegisterUserResponse{ UserId: userId, }, nil } func (k *Client) getUserGroupByType(userType int32) (string, error) { switch userType { case UserAgentType: return UserGroupAgents, nil case UserDistributorType: return UserGroupDistributor, nil default: return "", ErrUnknownUserType } } func (k *Client) LoginUser(ctx context.Context, request auth.LoginUserRequest) (*auth.LoginUserResponse, error) { token, err := k.client.Login(ctx, k.clientId, k.clientSecret, k.realm, request.Email, request.Password) if err != nil { return nil, k.handleError(err) } userInfo, err := k.jwtManager.GetUserInfoFromToken(token.AccessToken) if err != nil { return nil, k.handleError(err) } return &auth.LoginUserResponse{ UserId: userInfo.UserId, AccessToken: token.AccessToken, RefreshToken: token.RefreshToken, }, nil } func (k *Client) GetNewAccessToken(ctx context.Context, request auth.GetNewAccessTokenRequest) (*auth.GetNewAccessTokenResponse, error) { token, err := k.client.RefreshToken(ctx, request.RefreshToken, k.clientId, k.clientSecret, k.realm) if err != nil { return nil, k.handleError(err) } return &auth.GetNewAccessTokenResponse{ AccessToken: token.AccessToken, RefreshToken: token.RefreshToken, }, nil } func (k *Client) CheckTokenIsValid(ctx context.Context, token string) (bool, error) { resp, err := k.client.RetrospectToken(ctx, token, k.clientId, k.clientSecret, k.realm) if err != nil { return false, k.handleError(err) } if resp.Active == nil { return false, nil } return *resp.Active, nil } func (k *Client) LogoutUser(ctx context.Context, request auth.LogoutUserRequest) error { if err := k.client.Logout(ctx, k.clientId, k.clientSecret, k.realm, request.RefreshToken); err != nil { return k.handleError(err) } if err := k.client.RevokeToken(ctx, k.realm, k.clientId, k.clientSecret, request.RefreshToken); err != nil { return k.handleError(err) } return nil } func (k *Client) GetUserInfo(ctx context.Context, id string) (*auth.UserInfo, error) { client, err := k.client.LoginClient(ctx, k.clientId, k.clientSecret, k.realm) if err != nil { return nil, k.handleError(err) } user, err := k.client.GetUserByID(ctx, client.AccessToken, k.realm, id) if err != nil { return nil, k.handleError(err) } if user.Attributes == nil { return nil, ErrNoAttributes } groups, err := k.client.GetUserGroups(ctx, client.AccessToken, k.realm, id, gocloak.GetGroupsParams{}) if err != nil { return nil, k.handleError(err) } if len(groups) == 0 { return nil, ErrUnknownUserType } if groups[0].Name == nil { return nil, ErrUnknownUserType } userType, err := k.getUserTypeByUserGroup(*groups[0].Name) if err != nil { return nil, err } return &auth.UserInfo{ Email: *user.Email, FirstName: *user.FirstName, SecondName: *user.LastName, Patronymic: k.getUserPatronymicFromAttributes(*user.Attributes), Permissions: k.getUserPermissionsFromAttributes(*user.Attributes), UserType: userType, }, nil } func (k *Client) getUserPatronymicFromAttributes(attr attributes) *string { item, ok := attr[PatronymicAttr] if !ok || len(item) == 0 { return nil } return &item[0] } func (k *Client) getUserPermissionsFromAttributes(attr attributes) permissions { resp := make(map[string]string) for _, permission := range permissionsSlice { if item, ok := attr[permission]; ok { if len(item) != 0 { resp[permission] = item[0] } else { resp[permission] = "" // todo: подумать над тем как правильно делать в таких ситуациях } } else { resp[permission] = "" // todo: подумать над тем как правильно делать в таких ситуациях } } return resp } func (k *Client) getUserTypeByUserGroup(attr string) (int32, error) { switch attr { case UserGroupAgents: return UserAgentType, nil case UserGroupDistributor: return UserDistributorType, nil default: return -1, ErrUnknownUserType } } func (k *Client) GetPermissionsByUsersId(ctx context.Context, id string) (*auth.GetPermissionsByUsersIdResponse, error) { userInfo, err := k.GetUserInfo(ctx, id) if err != nil { return nil, err } return &auth.GetPermissionsByUsersIdResponse{ Balance: userInfo.Permissions[PermissionBalance], Company: userInfo.Permissions[PermissionCompany], Employees: userInfo.Permissions[PermissionEmployees], Profile: userInfo.Permissions[PermissionProfile], Submissions: userInfo.Permissions[PermissionSubmissions], Vacancies: userInfo.Permissions[PermissionVacancies], }, nil } func (k *Client) CheckPermissions(ctx context.Context, request auth.CheckPermissionsRequest) (bool, error) { userInfo, err := k.GetUserInfo(ctx, request.UserId) if err != nil { return false, err } if userInfo.Permissions == nil { return false, ErrInternal } if val, ok := userInfo.Permissions[request.RequiredPermission]; ok { return k.getPermissionLevel(val) >= k.getPermissionLevel(request.RequiredPermissionLevel), nil } return false, ErrInternal } func (k *Client) getPermissionLevel(permission string) int8 { switch permission { case PermissionLevelCanEdit: return 2 case PermissionLevelCanView: return 1 case PermissionLevelNoPermission: return 0 default: return -1 } } func (k *Client) VerifyEmail(ctx context.Context, userID string, token string) error { if err := k.jwtManager.VerifyEmailToken(token, userID); err != nil { return err } // Get admin token adminToken, err := k.client.LoginClient(ctx, k.clientId, k.clientSecret, k.realm) if err != nil { return k.handleError(err) } // Get user user, err := k.client.GetUserByID(ctx, adminToken.AccessToken, k.realm, userID) if err != nil { return k.handleError(err) } // Set email as verified user.EmailVerified = gocloak.BoolP(true) // Update user if err := k.client.UpdateUser(ctx, adminToken.AccessToken, k.realm, *user); err != nil { return k.handleError(err) } return nil } func (k *Client) GetEmailVerificationStatus(ctx context.Context, userID string) (*auth.GetEmailVerificationResponse, error) { // Get admin token adminToken, err := k.client.LoginClient(ctx, k.clientId, k.clientSecret, k.realm) if err != nil { return nil, k.handleError(err) } // Get user user, err := k.client.GetUserByID(ctx, adminToken.AccessToken, k.realm, userID) if err != nil { return nil, k.handleError(err) } // Check if email is verified verified := false if user.EmailVerified != nil { verified = *user.EmailVerified } return &auth.GetEmailVerificationResponse{ EmailVerified: verified, }, nil } func (k *Client) GetUserEmailVerificationToken(ctx context.Context, uid string) (*auth.GetUserTokenResponse, error) { // Получение токена администратора adminToken, err := k.client.LoginClient(ctx, k.clientId, k.clientSecret, k.realm) if err != nil { return nil, k.handleError(err) } // Получаем пользователя, чтобы убедиться в его существовании _, err = k.client.GetUserByID(ctx, adminToken.AccessToken, k.realm, uid) if err != nil { return nil, k.handleError(err) } // Создаём кастомный JWT-токен для верификации email сроком на 24 часа claims := jwt.MapClaims{ "sub": uid, // Subject (user ID) "iat": time.Now().Unix(), // Issued at "exp": time.Now().Add(24 * time.Hour).Unix(), // Expires in 24 hours "purpose": "email_verification", // Custom claim to identify purpose "iss": "molva-api-gateway", // Issuer } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) // Используем клиентский секрет как ключ подписи signedToken, err := token.SignedString([]byte(k.clientSecret)) if err != nil { return nil, ErrInternal } return &auth.GetUserTokenResponse{ AccessToken: signedToken, }, nil } func (k *Client) ResetPassword(ctx context.Context, request auth.ResetPasswordRequest) error { token, err := k.client.LoginClient(ctx, k.clientId, k.clientSecret, k.realm) if err != nil { return k.handleError(err) } user, err := k.client.GetUsers( ctx, token.AccessToken, k.realm, gocloak.GetUsersParams{ Username: gocloak.StringP(request.Email), }, ) if err != nil { return k.handleError(err) } if len(user) == 0 { return ErrNotFound } if err := k.client.SetPassword( ctx, token.AccessToken, *user[0].ID, k.realm, request.NewPassword, false, ); err != nil { return k.handleError(err) } return nil }