1
This commit is contained in:
390
internal/auth/keycloak/auth.go
Normal file
390
internal/auth/keycloak/auth.go
Normal file
@@ -0,0 +1,390 @@
|
||||
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
|
||||
}
|
Reference in New Issue
Block a user