parcoursmob/core/application/administration.go

659 lines
19 KiB
Go
Executable File

package application
import (
"context"
"crypto/rand"
"encoding/base64"
"fmt"
"io"
"net/http"
"sort"
"sync"
"time"
"git.coopgo.io/coopgo-apps/parcoursmob/core/utils/identification"
"git.coopgo.io/coopgo-apps/parcoursmob/core/utils/sorting"
agenda "git.coopgo.io/coopgo-platform/agenda/grpcapi"
agendastorage "git.coopgo.io/coopgo-platform/agenda/storage"
fleets "git.coopgo.io/coopgo-platform/fleets/grpcapi"
fleetsstorage "git.coopgo.io/coopgo-platform/fleets/storage"
groupsmanagement "git.coopgo.io/coopgo-platform/groups-management/grpcapi"
"git.coopgo.io/coopgo-platform/groups-management/storage"
groupstorage "git.coopgo.io/coopgo-platform/groups-management/storage"
accounts "git.coopgo.io/coopgo-platform/mobility-accounts/grpcapi"
mobilityaccounts "git.coopgo.io/coopgo-platform/mobility-accounts/grpcapi"
mobilityaccountsstorage "git.coopgo.io/coopgo-platform/mobility-accounts/storage"
"github.com/google/uuid"
"github.com/rs/zerolog/log"
"google.golang.org/protobuf/types/known/structpb"
)
type AdministrationDataResult struct {
Accounts []mobilityaccountsstorage.Account
Beneficiaries []mobilityaccountsstorage.Account
Groups []groupstorage.Group
Bookings []fleetsstorage.Booking
Events []agendastorage.Event
}
type AdminVehiclesStatsResult struct {
Vehicles []fleetsstorage.Vehicle
Bookings []fleetsstorage.Booking
Groups map[string]any
}
type AdminBookingsStatsResult struct {
Vehicles map[string]fleetsstorage.Vehicle
Bookings []fleetsstorage.Booking
Groups map[string]any
BeneficiariesMap map[string]any
}
type AdminBeneficiariesStatsResult struct {
Beneficiaries []mobilityaccountsstorage.Account
CacheID string
}
type AdminEventsStatsResult struct {
Events []agendastorage.Event
Groups map[string]any
}
// GetAdministrationData retrieves all data needed for the administration dashboard
func (h *ApplicationHandler) GetAdministrationData(ctx context.Context) (*AdministrationDataResult, error) {
var (
wg sync.WaitGroup
accounts, beneficiaries []mobilityaccountsstorage.Account
bookings []fleetsstorage.Booking
accountsErr, beneficiariesErr, bookingsErr, groupsResponseErr, eventsResponseErr, groupsBatchErr error
groups = []groupstorage.Group{}
responses = []agendastorage.Event{}
groupsResponse *groupsmanagement.GetGroupsResponse
eventsResponse *agenda.GetEventsResponse
groupids = []string{}
)
// Retrieve accounts in a goroutine
wg.Add(1)
go func() {
defer wg.Done()
accounts, accountsErr = h.services.GetAccounts()
}()
// Retrieve beneficiaries in a goroutine
wg.Add(1)
go func() {
defer wg.Done()
beneficiaries, beneficiariesErr = h.services.GetBeneficiaries()
}()
// Retrieve bookings in a goroutine
wg.Add(1)
go func() {
defer wg.Done()
bookings, bookingsErr = h.services.GetBookings()
}()
// Retrieve groups in a goroutine
wg.Add(1)
go func() {
defer wg.Done()
request := &groupsmanagement.GetGroupsRequest{
Namespaces: []string{"parcoursmob_organizations"},
}
groupsResponse, groupsResponseErr = h.services.GRPC.GroupsManagement.GetGroups(ctx, request)
if groupsResponseErr == nil {
for _, group := range groupsResponse.Groups {
g := group.ToStorageType()
groups = append(groups, g)
}
sort.Sort(sorting.GroupsByName(groups))
}
}()
// Retrieve Events in a goroutine
wg.Add(1)
go func() {
defer wg.Done()
eventsResponse, eventsResponseErr = h.services.GRPC.Agenda.GetEvents(ctx, &agenda.GetEventsRequest{
Namespaces: []string{"parcoursmob_dispositifs"},
})
if eventsResponseErr == nil {
for _, e := range eventsResponse.Events {
groupids = append(groupids, e.Owners...)
responses = append(responses, e.ToStorageType())
}
sort.Sort(sorting.EventsByStartdate(responses))
}
}()
wg.Wait()
// Check for errors
if accountsErr != nil || beneficiariesErr != nil || bookingsErr != nil || groupsResponseErr != nil || eventsResponseErr != nil {
log.Error().
Any("accounts error", accountsErr).
Any("beneficiaries error", beneficiariesErr).
Any("bookings error", bookingsErr).
Any("groups response error", groupsResponseErr).
Any("events response error", eventsResponseErr).
Any("groups batch error", groupsBatchErr).
Msg("Error in retrieving administration data")
return nil, fmt.Errorf("error retrieving administration data")
}
return &AdministrationDataResult{
Accounts: accounts,
Beneficiaries: beneficiaries,
Groups: groups,
Bookings: bookings,
Events: responses,
}, nil
}
// CreateAdministrationGroup creates a new administration group
func (h *ApplicationHandler) CreateAdministrationGroup(ctx context.Context, name string, modules map[string]any) (string, error) {
groupid := uuid.NewString()
dataMap := map[string]any{
"name": name,
"modules": modules,
}
data, err := structpb.NewValue(dataMap)
if err != nil {
log.Error().Err(err).Msg("Cannot create PB struct from data map")
return "", fmt.Errorf("failed to create group data: %w", err)
}
request_organization := &groupsmanagement.AddGroupRequest{
Group: &groupsmanagement.Group{
Id: groupid,
Namespace: "parcoursmob_organizations",
Data: data.GetStructValue(),
},
}
request_role := &groupsmanagement.AddGroupRequest{
Group: &groupsmanagement.Group{
Id: groupid + ":admin",
Namespace: "parcoursmob_roles",
},
}
// Create organization group
_, err = h.services.GRPC.GroupsManagement.AddGroup(ctx, request_organization)
if err != nil {
log.Error().Err(err).Msg("Issue in Groups management service - AddGroup")
return "", fmt.Errorf("failed to create organization group: %w", err)
}
// Create admin role for the organization
_, err = h.services.GRPC.GroupsManagement.AddGroup(ctx, request_role)
if err != nil {
log.Error().Err(err).Msg("Issue in Groups management service - AddGroup")
return "", fmt.Errorf("failed to create admin role: %w", err)
}
return groupid, nil
}
type AdministrationGroupDataResult struct {
Group groupstorage.Group
Members []mobilityaccountsstorage.Account
Admins []mobilityaccountsstorage.Account
}
// GetAdministrationGroupData retrieves data for a specific administration group
func (h *ApplicationHandler) GetAdministrationGroupData(ctx context.Context, groupID string) (*AdministrationGroupDataResult, error) {
request := &groupsmanagement.GetGroupRequest{
Id: groupID,
}
resp, err := h.services.GRPC.GroupsManagement.GetGroup(ctx, request)
if err != nil {
log.Error().Err(err).Msg("Issue in Groups management service - GetGroup")
return nil, fmt.Errorf("failed to get group: %w", err)
}
groupmembers, admins, err := h.groupmembers(groupID)
if err != nil {
log.Error().Err(err).Msg("issue retrieving group members")
return nil, fmt.Errorf("failed to get group members: %w", err)
}
return &AdministrationGroupDataResult{
Group: resp.Group.ToStorageType(),
Members: groupmembers,
Admins: admins,
}, nil
}
// InviteAdministrationGroupAdmin invites a user as admin to an administration group
func (h *ApplicationHandler) InviteAdministrationGroupAdmin(ctx context.Context, groupID, username string) error {
// Get group info
groupResp, err := h.services.GRPC.GroupsManagement.GetGroup(ctx, &groupsmanagement.GetGroupRequest{
Id: groupID,
})
if err != nil {
return fmt.Errorf("failed to get group: %w", err)
}
group := groupResp.Group.ToStorageType()
accountresp, err := h.services.GRPC.MobilityAccounts.GetAccountUsername(ctx, &mobilityaccounts.GetAccountUsernameRequest{
Username: username,
Namespace: "parcoursmob",
})
if err == nil {
// Account already exists: adding the existing account to group as admin
account := accountresp.Account.ToStorageType()
if account.Data["groups"] == nil {
account.Data["groups"] = []any{}
}
account.Data["groups"] = append(account.Data["groups"].([]any), groupID+":admin")
as, err := mobilityaccounts.AccountFromStorageType(&account)
if err != nil {
return fmt.Errorf("failed to convert account: %w", err)
}
_, err = h.services.GRPC.MobilityAccounts.UpdateData(ctx, &mobilityaccounts.UpdateDataRequest{
Account: as,
})
if err != nil {
return fmt.Errorf("failed to update account: %w", err)
}
data := map[string]any{
"group": group.Data["name"],
"baseUrl": h.config.GetString("base_url"),
}
if err := h.emailing.Send("onboarding.existing_administrator", username, data); err != nil {
log.Warn().Err(err).Msg("failed to send existing admin email")
}
} else {
// Create onboarding for new admin
onboarding := map[string]any{
"username": username,
"group": groupID,
"admin": true,
}
b := make([]byte, 16)
if _, err := io.ReadFull(rand.Reader, b); err != nil {
return fmt.Errorf("failed to generate random key: %w", err)
}
key := base64.RawURLEncoding.EncodeToString(b)
h.cache.PutWithTTL("onboarding/"+key, onboarding, 72*time.Hour)
data := map[string]any{
"group": group.Data["name"],
"key": key,
"baseUrl": h.config.GetString("base_url"),
}
if err := h.emailing.Send("onboarding.new_administrator", username, data); err != nil {
return fmt.Errorf("failed to send new admin email: %w", err)
}
}
return nil
}
// InviteAdministrationGroupMember invites a user as member to an administration group
func (h *ApplicationHandler) InviteAdministrationGroupMember(ctx context.Context, groupID, username string) error {
// Get group info
groupResp, err := h.services.GRPC.GroupsManagement.GetGroup(ctx, &groupsmanagement.GetGroupRequest{
Id: groupID,
})
if err != nil {
return fmt.Errorf("failed to get group: %w", err)
}
group := groupResp.Group.ToStorageType()
accountresp, err := h.services.GRPC.MobilityAccounts.GetAccountUsername(ctx, &mobilityaccounts.GetAccountUsernameRequest{
Username: username,
Namespace: "parcoursmob",
})
if err == nil {
// Account already exists: adding the existing account to group
account := accountresp.Account.ToStorageType()
if account.Data["groups"] == nil {
account.Data["groups"] = []any{}
}
account.Data["groups"] = append(account.Data["groups"].([]any), groupID)
as, err := mobilityaccounts.AccountFromStorageType(&account)
if err != nil {
return fmt.Errorf("failed to convert account: %w", err)
}
_, err = h.services.GRPC.MobilityAccounts.UpdateData(ctx, &mobilityaccounts.UpdateDataRequest{
Account: as,
})
if err != nil {
return fmt.Errorf("failed to update account: %w", err)
}
data := map[string]any{
"group": group.Data["name"],
"baseUrl": h.config.GetString("base_url"),
}
if err := h.emailing.Send("onboarding.existing_member", username, data); err != nil {
log.Warn().Err(err).Msg("failed to send existing member email")
}
} else {
// Create onboarding for new member
onboarding := map[string]any{
"username": username,
"group": groupID,
"admin": false,
}
b := make([]byte, 16)
if _, err := io.ReadFull(rand.Reader, b); err != nil {
return fmt.Errorf("failed to generate random key: %w", err)
}
key := base64.RawURLEncoding.EncodeToString(b)
h.cache.PutWithTTL("onboarding/"+key, onboarding, 72*time.Hour)
data := map[string]any{
"group": group.Data["name"],
"key": key,
"baseUrl": h.config.GetString("base_url"),
}
if err := h.emailing.Send("onboarding.new_member", username, data); err != nil {
return fmt.Errorf("failed to send new member email: %w", err)
}
}
return nil
}
func filteVehicle(r *http.Request, v *fleets.Vehicle) bool {
g := r.Context().Value(identification.GroupKey)
if g == nil {
return false
}
group := g.(storage.Group)
for _, n := range v.Administrators {
if n == group.ID {
return true
}
}
return false
}
func (h *ApplicationHandler) GetVehiclesStats() (*AdminVehiclesStatsResult, error) {
bookings := []fleetsstorage.Booking{}
administrators := []string{}
request := &fleets.GetVehiclesRequest{
Namespaces: []string{"parcoursmob"},
}
resp, err := h.services.GRPC.Fleets.GetVehicles(context.TODO(), request)
if err != nil {
return nil, err
}
vehicles := []fleetsstorage.Vehicle{}
for _, vehicle := range resp.Vehicles {
v := vehicle.ToStorageType()
adminfound := false
for _, a := range administrators {
if len(v.Administrators) > 0 && a == v.Administrators[0] {
adminfound = true
break
}
}
if !adminfound && len(v.Administrators) > 0 {
administrators = append(administrators, v.Administrators[0])
}
vehicleBookings := []fleetsstorage.Booking{}
for _, b := range v.Bookings {
if b.Unavailableto.After(time.Now()) {
vehicleBookings = append(vehicleBookings, b)
}
}
v.Bookings = vehicleBookings
vehicles = append(vehicles, v)
}
groups := map[string]any{}
if len(administrators) > 0 {
admingroups, err := h.services.GRPC.GroupsManagement.GetGroupsBatch(context.TODO(), &groupsmanagement.GetGroupsBatchRequest{
Groupids: administrators,
})
if err != nil {
return nil, err
}
for _, g := range admingroups.Groups {
groups[g.Id] = g.ToStorageType()
}
}
sort.Sort(sorting.VehiclesByLicencePlate(vehicles))
sort.Sort(sorting.BookingsByStartdate(bookings))
return &AdminVehiclesStatsResult{
Vehicles: vehicles,
Bookings: bookings,
Groups: groups,
}, nil
}
func (h *ApplicationHandler) GetBookingsStats(status, startDate, endDate string) (*AdminBookingsStatsResult, error) {
vehicles := map[string]fleetsstorage.Vehicle{}
bookings := []fleetsstorage.Booking{}
// Parse start date filter
var startdate time.Time
if startDate != "" {
if parsed, err := time.Parse("2006-01-02", startDate); err == nil {
startdate = parsed
}
}
// Parse end date filter
var enddate time.Time
if endDate != "" {
if parsed, err := time.Parse("2006-01-02", endDate); err == nil {
enddate = parsed.Add(24 * time.Hour) // End of day
}
}
request := &fleets.GetVehiclesRequest{
Namespaces: []string{"parcoursmob"},
IncludeDeleted: true,
}
resp, err := h.services.GRPC.Fleets.GetVehicles(context.TODO(), request)
if err != nil {
return nil, err
}
beneficiaries_ids := []string{}
for _, vehicle := range resp.Vehicles {
v := vehicle.ToStorageType()
for _, b := range v.Bookings {
// Apply status filter
if status != "" {
bookingStatus := b.Status()
statusInt := 0
if b.Deleted {
statusInt = -2 // Use -2 for cancelled to distinguish from terminated
} else {
statusInt = bookingStatus
}
// Map status string to int
var filterStatusInt int
switch status {
case "FORTHCOMING":
filterStatusInt = 1
case "ONGOING":
filterStatusInt = 0
case "TERMINATED":
filterStatusInt = -1
case "CANCELLED":
filterStatusInt = -2
default:
filterStatusInt = 999 // Invalid status, won't match anything
}
if statusInt != filterStatusInt {
continue
}
}
// Apply date filter (on startdate)
if !startdate.IsZero() && b.Startdate.Before(startdate) {
continue
}
if !enddate.IsZero() && b.Startdate.After(enddate) {
continue
}
bookings = append(bookings, b)
beneficiaries_ids = append(beneficiaries_ids, b.Driver)
}
vehicles[v.ID] = v
}
groups := map[string]any{}
admingroups, err := h.services.GRPC.GroupsManagement.GetGroups(context.TODO(), &groupsmanagement.GetGroupsRequest{
Namespaces: []string{"parcoursmob_organizations"},
})
if err != nil {
return nil, err
}
for _, g := range admingroups.Groups {
groups[g.Id] = g.ToStorageType()
}
beneficiaries, err := h.services.GRPC.MobilityAccounts.GetAccountsBatch(context.TODO(), &accounts.GetAccountsBatchRequest{
Accountids: beneficiaries_ids,
})
if err != nil {
return nil, err
}
beneficiaries_map := map[string]any{}
for _, ben := range beneficiaries.Accounts {
beneficiaries_map[ben.Id] = ben.ToStorageType()
}
return &AdminBookingsStatsResult{
Vehicles: vehicles,
Bookings: bookings,
Groups: groups,
BeneficiariesMap: beneficiaries_map,
}, nil
}
func (h *ApplicationHandler) GetBeneficiariesStats() (*AdminBeneficiariesStatsResult, error) {
beneficiaries, err := h.services.GetBeneficiaries()
if err != nil {
return nil, err
}
cacheid := uuid.New().String()
h.cache.Put(cacheid, beneficiaries)
return &AdminBeneficiariesStatsResult{
Beneficiaries: beneficiaries,
CacheID: cacheid,
}, nil
}
func (h *ApplicationHandler) GetEventsStats() (*AdminEventsStatsResult, error) {
resp, err := h.services.GRPC.Agenda.GetEvents(context.TODO(), &agenda.GetEventsRequest{
Namespaces: []string{"parcoursmob_dispositifs"},
})
if err != nil {
return nil, err
}
responses := []agendastorage.Event{}
groupids := []string{}
for _, event := range resp.Events {
responses = append(responses, event.ToStorageType())
groupids = append(groupids, event.Owners...)
}
groupsResponse, err := h.services.GRPC.GroupsManagement.GetGroupsBatch(context.TODO(), &groupsmanagement.GetGroupsBatchRequest{
Groupids: groupids,
})
if err != nil {
return nil, err
}
groups := map[string]any{}
for _, g := range groupsResponse.Groups {
groups[g.Id] = g.ToStorageType()
}
return &AdminEventsStatsResult{
Events: responses,
Groups: groups,
}, nil
}
func (h *ApplicationHandler) members() ([]*accounts.Account, error) {
resp, err := h.services.GRPC.MobilityAccounts.GetAccounts(context.TODO(), &accounts.GetAccountsRequest{
Namespaces: []string{"parcoursmob"},
})
if err != nil {
return nil, err
}
return resp.Accounts, nil
}
func (h *ApplicationHandler) groupmembers(groupid string) (groupmembers []mobilityaccountsstorage.Account, admins []mobilityaccountsstorage.Account, err error) {
members, err := h.members()
if err != nil {
if err != nil {
log.Error().Err(err).Msg("Cannot get members")
return nil, nil, err
}
}
groupmembers = []mobilityaccountsstorage.Account{}
admins = []mobilityaccountsstorage.Account{}
for _, m := range members {
mm := m.ToStorageType()
for _, g := range mm.Data["groups"].([]any) {
if g.(string) == groupid {
groupmembers = append(groupmembers, mm)
}
if g.(string) == groupid+":admin" {
admins = append(admins, mm)
}
}
}
return groupmembers, admins, err
}