package application import ( "context" "crypto/rand" "encoding/base64" "errors" "io" "time" "git.coopgo.io/coopgo-platform/groups-management/grpcapi" mobilityaccounts "git.coopgo.io/coopgo-platform/mobility-accounts/grpcapi" ma "git.coopgo.io/coopgo-platform/mobility-accounts/storage" groupsstorage "git.coopgo.io/coopgo-platform/groups-management/storage" "github.com/coreos/go-oidc/v3/oidc" "github.com/rs/zerolog/log" ) type OAuth2CallbackResult struct { RedirectURL string IDToken string } func (h *ApplicationHandler) ProcessOAuth2Callback(code string, redirectSession string) (*OAuth2CallbackResult, error) { oauth2Token, err := h.idp.OAuth2Config.Exchange(context.Background(), code) if err != nil { log.Error().Err(err).Msg("Exchange error") return nil, err } // Extract the ID Token from OAuth2 token. rawIDToken, ok := oauth2Token.Extra("id_token").(string) if !ok { log.Error().Msg("Cannot retrieve ID token") return nil, errors.New("cannot retrieve ID token") } _, err = h.idp.TokenVerifier.Verify(context.Background(), rawIDToken) if err != nil { log.Error().Err(err).Msg("Not able to verify token") return nil, err } redirect := "/app/" if redirectSession != "" { redirect = redirectSession } return &OAuth2CallbackResult{ RedirectURL: redirect, IDToken: rawIDToken, }, nil } type LostPasswordInitResult struct { Success bool } func (h *ApplicationHandler) InitiateLostPassword(email string) (*LostPasswordInitResult, error) { account, err := h.services.GRPC.MobilityAccounts.GetAccountUsername(context.TODO(), &mobilityaccounts.GetAccountUsernameRequest{ Username: email, Namespace: "parcoursmob", }) if err != nil { return nil, err } b := make([]byte, 16) if _, err := io.ReadFull(rand.Reader, b); err != nil { return nil, err } key := base64.RawURLEncoding.EncodeToString(b) passwordretrieval := map[string]any{ "username": email, "account_id": account.Account.Id, "key": key, } h.cache.PutWithTTL("retrieve-password/"+key, passwordretrieval, 72*time.Hour) if err := h.emailing.Send("auth.retrieve_password", email, passwordretrieval); err != nil { return nil, err } return &LostPasswordInitResult{Success: true}, nil } type LostPasswordRecoverResult struct { Success bool } func (h *ApplicationHandler) RecoverLostPassword(key, newPassword string) (*LostPasswordRecoverResult, error) { recover, err := h.cache.Get("retrieve-password/" + key) if err != nil { return nil, err } if newPassword == "" { return nil, errors.New("password is empty") } _, err = h.services.GRPC.MobilityAccounts.ChangePassword(context.TODO(), &mobilityaccounts.ChangePasswordRequest{ Id: recover.(map[string]any)["account_id"].(string), Password: newPassword, }) if err != nil { return nil, err } err = h.cache.Delete("retrieve-password/" + key) if err != nil { log.Error().Err(err).Msg("Failed to delete password recovery key") } return &LostPasswordRecoverResult{Success: true}, nil } func (h *ApplicationHandler) GetPasswordRecoveryData(key string) (map[string]any, error) { recover, err := h.cache.Get("retrieve-password/" + key) if err != nil { return nil, err } return recover.(map[string]any), nil } type OnboardingResult struct { Success bool } func (h *ApplicationHandler) CompleteOnboarding(key, password, firstName, lastName string) (*OnboardingResult, error) { onboarding, err := h.cache.Get("onboarding/" + key) if err != nil { return nil, err } onboardingmap := onboarding.(map[string]any) if password == "" { return nil, errors.New("password is empty") } groups := []string{ onboardingmap["group"].(string), } if onboardingmap["admin"].(bool) { groups = append(groups, onboardingmap["group"].(string)+":admin") } display_name := firstName + " " + lastName account := &ma.Account{ Authentication: ma.AccountAuth{ Local: ma.LocalAuth{ Username: onboardingmap["username"].(string), Password: password, }, }, Namespace: "parcoursmob", Data: map[string]any{ "display_name": display_name, "first_name": firstName, "last_name": lastName, "email": onboardingmap["username"], "groups": groups, }, } acc, err := mobilityaccounts.AccountFromStorageType(account) if err != nil { return nil, err } request := &mobilityaccounts.RegisterRequest{ Account: acc, } _, err = h.services.GRPC.MobilityAccounts.Register(context.TODO(), request) if err != nil { return nil, err } err = h.cache.Delete("onboarding/" + key) if err != nil { log.Error().Err(err).Msg("Failed to delete onboarding key") } return &OnboardingResult{Success: true}, nil } func (h *ApplicationHandler) GetOnboardingData(key string) (map[string]any, error) { onboarding, err := h.cache.Get("onboarding/" + key) if err != nil { return nil, err } return onboarding.(map[string]any), nil } type UserGroupsResult struct { Groups []groupsstorage.Group } func (h *ApplicationHandler) GetUserGroups(idtoken *oidc.IDToken) (*UserGroupsResult, error) { var claims map[string]any err := idtoken.Claims(&claims) if err != nil { return nil, err } g := claims["groups"] groups_interface, ok := g.([]any) if !ok { return nil, errors.New("invalid groups format") } groups := []string{} for _, v := range groups_interface { groups = append(groups, v.(string)) } request := &grpcapi.GetGroupsBatchRequest{ Groupids: groups, } resp, err := h.services.GRPC.GroupsManagement.GetGroupsBatch(context.TODO(), request) if err != nil { return nil, err } var groupsresponse []groupsstorage.Group for _, group := range resp.Groups { if group.Namespace != "parcoursmob_organizations" { continue } g := group.ToStorageType() groupsresponse = append(groupsresponse, g) } return &UserGroupsResult{Groups: groupsresponse}, nil }