391 lines
10 KiB
Go
391 lines
10 KiB
Go
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
|
||
}
|