diff --git a/.gitignore b/.gitignore index de54a0e..e9f8faa 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ themes/* .vscode __debug_bin parcoursmob +public_themes/ .idea diff --git a/Dockerfile b/Dockerfile index 71b5486..abff915 100755 --- a/Dockerfile +++ b/Dockerfile @@ -30,6 +30,7 @@ FROM scratch COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo COPY --from=builder /themes/ /themes/ +COPY --from=builder /public_themes/ /public_themes/ COPY --from=builder /server / EXPOSE 8080 diff --git a/config.go b/config.go index baeeaf7..7d47cfe 100755 --- a/config.go +++ b/config.go @@ -24,9 +24,10 @@ func ReadConfig() (*viper.Viper, error) { "listen": "0.0.0.0:8081", }, "publicweb": map[string]any{ - "enabled": false, - "listen": "0.0.0.0:8082", - "root_dir": "public_themes/default", + "enabled": false, + "listen": "0.0.0.0:8082", + "root_dir": "public_themes/default", + "contact_email": "contact@example.com", }, }, "identification": map[string]any{ diff --git a/core/application/administration.go b/core/application/administration.go index 0e7871d..9060c1f 100755 --- a/core/application/administration.go +++ b/core/application/administration.go @@ -77,14 +77,14 @@ func (h *ApplicationHandler) GetAdministrationData(ctx context.Context) (*Admini wg.Add(1) go func() { defer wg.Done() - accounts, accountsErr = h.services.GetAccounts() + accounts, accountsErr = h.services.GetAccounts(ctx) }() // Retrieve beneficiaries in a goroutine wg.Add(1) go func() { defer wg.Done() - beneficiaries, beneficiariesErr = h.services.GetBeneficiaries() + beneficiaries, beneficiariesErr = h.services.GetBeneficiaries(ctx) }() // Retrieve bookings in a goroutine @@ -570,8 +570,8 @@ func (h *ApplicationHandler) GetBookingsStats(status, startDate, endDate string) }, nil } -func (h *ApplicationHandler) GetBeneficiariesStats() (*AdminBeneficiariesStatsResult, error) { - beneficiaries, err := h.services.GetBeneficiaries() +func (h *ApplicationHandler) GetBeneficiariesStats(ctx context.Context) (*AdminBeneficiariesStatsResult, error) { + beneficiaries, err := h.services.GetBeneficiaries(ctx) if err != nil { return nil, err } diff --git a/core/application/group_module.go b/core/application/group_module.go index 0e22bb4..c9cabf6 100755 --- a/core/application/group_module.go +++ b/core/application/group_module.go @@ -179,7 +179,7 @@ func (h *ApplicationHandler) DisplayGroupModule(ctx context.Context, groupID str h.cache.PutWithTTL(cacheID, accounts, 1*time.Hour) // Get beneficiaries in current user's group - accountsBeneficiaire, err := h.services.GetBeneficiariesInGroup(currentUserGroup) + accountsBeneficiaire, err := h.services.GetBeneficiariesInGroup(ctx, currentUserGroup) if err != nil { return nil, fmt.Errorf("failed to get beneficiaries in group: %w", err) } diff --git a/core/application/journeys.go b/core/application/journeys.go index cc99e7e..fbaa093 100755 --- a/core/application/journeys.go +++ b/core/application/journeys.go @@ -91,7 +91,7 @@ func (h *ApplicationHandler) SearchJourneys( // SOLIDARITY TRANSPORT var err error - drivers, err = h.services.GetAccountsInNamespacesMap([]string{"solidarity_drivers", "organized_carpool_drivers"}) + drivers, err = h.services.GetAccountsInNamespacesMap(ctx, []string{"solidarity_drivers", "organized_carpool_drivers"}) if err != nil { drivers = map[string]mobilityaccountsstorage.Account{} } @@ -99,30 +99,20 @@ func (h *ApplicationHandler) SearchJourneys( protodep, _ := transformers.GeoJsonToProto(departureGeo) protodest, _ := transformers.GeoJsonToProto(destinationGeo) - // Get driver IDs to exclude based on group_id (drivers who already have bookings in this group) - excludedDriverIds := make(map[string]bool) - if solidarityExcludeGroupId != "" { - bookingsResp, err := h.services.GRPC.SolidarityTransport.GetSolidarityTransportBookings(ctx, &gen.GetSolidarityTransportBookingsRequest{ - StartDate: timestamppb.New(time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)), - EndDate: timestamppb.New(time.Date(9999, 12, 31, 0, 0, 0, 0, time.UTC)), - }) - if err == nil { - for _, booking := range bookingsResp.Bookings { - if booking.GroupId == solidarityExcludeGroupId { - excludedDriverIds[booking.DriverId] = true - } - } - } - } - if solidarityTransportEnabled { log.Debug().Time("departure time", departureDateTime).Msg("calling driver journeys with ...") - res, err := h.services.GRPC.SolidarityTransport.GetDriverJourneys(ctx, &gen.GetDriverJourneysRequest{ + req := &gen.GetDriverJourneysRequest{ Departure: protodep, Arrival: protodest, DepartureDate: timestamppb.New(departureDateTime), - }) + } + // Pass exclude_group_id to the service to filter out drivers with bookings in this group + if solidarityExcludeGroupId != "" { + req.ExcludeGroupId = &solidarityExcludeGroupId + } + + res, err := h.services.GRPC.SolidarityTransport.GetDriverJourneys(ctx, req) if err != nil { log.Error().Err(err).Msg("error in grpc call to GetDriverJourneys") } else { @@ -138,10 +128,6 @@ func (h *ApplicationHandler) SearchJourneys( if dj.DriverId == solidarityTransportExcludeDriver { continue } - // Skip drivers who already have bookings in the same group - if excludedDriverIds[dj.DriverId] { - continue - } if !yield(dj) { return } diff --git a/core/application/members.go b/core/application/members.go index f926282..73b3ef5 100755 --- a/core/application/members.go +++ b/core/application/members.go @@ -21,7 +21,7 @@ type MembersResult struct { } func (h *ApplicationHandler) GetMembers(ctx context.Context) (*MembersResult, error) { - accounts, err := h.services.GetAccounts() + accounts, err := h.services.GetAccounts(ctx) if err != nil { return nil, err } diff --git a/core/application/organized-carpool.go b/core/application/organized-carpool.go index b7ff56b..cca9247 100644 --- a/core/application/organized-carpool.go +++ b/core/application/organized-carpool.go @@ -212,7 +212,7 @@ func (h *ApplicationHandler) GetOrganizedCarpoolOverview(ctx context.Context, st } } - beneficiariesMap, err := h.services.GetBeneficiariesMap() + beneficiariesMap, err := h.services.GetBeneficiariesMap(ctx) if err != nil { beneficiariesMap = map[string]mobilityaccountsstorage.Account{} } @@ -403,12 +403,12 @@ func (h *ApplicationHandler) GetOrganizedCarpoolBookingData(ctx context.Context, return nil, fmt.Errorf("carpool booking not found") } - driver, err := h.services.GetAccount(resp.Booking.Driver.Id) + driver, err := h.services.GetAccount(ctx, resp.Booking.Driver.Id) if err != nil { return nil, fmt.Errorf("driver retrieval issue: %w", err) } - passenger, err := h.services.GetAccount(resp.Booking.Passenger.Id) + passenger, err := h.services.GetAccount(ctx, resp.Booking.Passenger.Id) if err != nil { return nil, fmt.Errorf("passenger retrieval issue: %w", err) } @@ -585,7 +585,7 @@ type OrganizedCarpoolDriverDataResult struct { func (h *ApplicationHandler) GetOrganizedCarpoolDriverData(ctx context.Context, driverID string) (*OrganizedCarpoolDriverDataResult, error) { documents := h.filestorage.List(filestorage.PREFIX_ORGANIZED_CARPOOL_DRIVERS + "/" + driverID) - driver, err := h.services.GetAccount(driverID) + driver, err := h.services.GetAccount(ctx, driverID) if err != nil { return nil, fmt.Errorf("issue retrieving driver account: %w", err) } @@ -686,7 +686,7 @@ type OrganizedCarpoolDriverResult struct { } func (h *ApplicationHandler) GetOrganizedCarpoolDriver(ctx context.Context, driverID string) (*OrganizedCarpoolDriverResult, error) { - driver, err := h.services.GetAccount(driverID) + driver, err := h.services.GetAccount(ctx, driverID) if err != nil { return nil, fmt.Errorf("issue retrieving driver account: %w", err) } @@ -703,7 +703,7 @@ func (h *ApplicationHandler) GetOrganizedCarpoolDriver(ctx context.Context, driv func (h *ApplicationHandler) UpdateOrganizedCarpoolDriver(ctx context.Context, driverID, firstName, lastName, email string, birthdate *time.Time, phoneNumber, fileNumber string, address, addressDestination any, gender, otherProperties string) (string, error) { // Security check: verify the account exists and is an organized carpool driver - driver, err := h.services.GetAccount(driverID) + driver, err := h.services.GetAccount(ctx, driverID) if err != nil { return "", fmt.Errorf("issue retrieving driver account: %w", err) } @@ -786,7 +786,7 @@ func (h *ApplicationHandler) UpdateOrganizedCarpoolDriver(ctx context.Context, d func (h *ApplicationHandler) ArchiveOrganizedCarpoolDriver(ctx context.Context, driverID string) error { // Security check: verify the account exists and is an organized carpool driver - driver, err := h.services.GetAccount(driverID) + driver, err := h.services.GetAccount(ctx, driverID) if err != nil { return fmt.Errorf("issue retrieving driver account: %w", err) } @@ -815,7 +815,7 @@ func (h *ApplicationHandler) ArchiveOrganizedCarpoolDriver(ctx context.Context, func (h *ApplicationHandler) UnarchiveOrganizedCarpoolDriver(ctx context.Context, driverID string) error { // Security check: verify the account exists and is an organized carpool driver - driver, err := h.services.GetAccount(driverID) + driver, err := h.services.GetAccount(ctx, driverID) if err != nil { return fmt.Errorf("issue retrieving driver account: %w", err) } @@ -844,7 +844,7 @@ func (h *ApplicationHandler) UnarchiveOrganizedCarpoolDriver(ctx context.Context func (h *ApplicationHandler) AddOrganizedCarpoolDriverDocument(ctx context.Context, driverID string, file io.Reader, filename string, fileSize int64, documentType, documentName string) error { // Security check: verify the account exists and is an organized carpool driver - driver, err := h.services.GetAccount(driverID) + driver, err := h.services.GetAccount(ctx, driverID) if err != nil { return fmt.Errorf("issue retrieving driver account: %w", err) } @@ -876,7 +876,7 @@ func (h *ApplicationHandler) DeleteOrganizedCarpoolDriverDocument(ctx context.Co func (h *ApplicationHandler) AddOrganizedCarpoolTrip(ctx context.Context, driverID, outwardtime, returntime string, departure, destination *geojson.Feature, days map[string]bool) error { // Security check: verify the account exists and is an organized carpool driver - driver, err := h.services.GetAccount(driverID) + driver, err := h.services.GetAccount(ctx, driverID) if err != nil { return fmt.Errorf("issue retrieving driver account: %w", err) } @@ -911,14 +911,18 @@ func (h *ApplicationHandler) AddOrganizedCarpoolTrip(ctx context.Context, driver for day, enabled := range days { if enabled { dayCode := dayMap[day] - outwardschedules = append(outwardschedules, map[string]any{ - "day": dayCode, - "time_of_day": outwardtime, - }) - returnschedules = append(returnschedules, map[string]any{ - "day": dayCode, - "time_of_day": returntime, - }) + if outwardtime != "" { + outwardschedules = append(outwardschedules, map[string]any{ + "day": dayCode, + "time_of_day": outwardtime, + }) + } + if returntime != "" { + returnschedules = append(returnschedules, map[string]any{ + "day": dayCode, + "time_of_day": returntime, + }) + } } } @@ -962,12 +966,24 @@ func (h *ApplicationHandler) AddOrganizedCarpoolTrip(ctx context.Context, driver return fmt.Errorf("failed marshaling return geojson: %w", err) } - trips = append(trips, &proto.CarpoolFeatureCollection{ - Serialized: string(outwardtrip), - }) - trips = append(trips, &proto.CarpoolFeatureCollection{ - Serialized: string(returntrip), - }) + // Only add outward trip if outward time is provided + if outwardtime != "" { + trips = append(trips, &proto.CarpoolFeatureCollection{ + Serialized: string(outwardtrip), + }) + } + + // Only add return trip if return time is provided + if returntime != "" { + trips = append(trips, &proto.CarpoolFeatureCollection{ + Serialized: string(returntrip), + }) + } + + // If no trips to create, return early + if len(trips) == 0 { + return fmt.Errorf("at least one time (outward or return) must be provided") + } req := &proto.CreateRegularRoutesRequest{ Routes: trips, @@ -1016,21 +1032,21 @@ func (h *ApplicationHandler) GetOrganizedCarpoolJourneyData(ctx context.Context, return nil, fmt.Errorf("could not unmarshal carpool journey: %w", err) } - driver, err := h.services.GetAccount(driverID) + driver, err := h.services.GetAccount(ctx, driverID) if err != nil { return nil, fmt.Errorf("could not get driver: %w", err) } var passenger mobilityaccountsstorage.Account if passengerID != "" { - passenger, err = h.services.GetAccount(passengerID) + passenger, err = h.services.GetAccount(ctx, passengerID) if err != nil { return nil, fmt.Errorf("could not get passenger account: %w", err) } } // Get beneficiaries in current user's group - beneficiaries, err := h.services.GetBeneficiariesInGroup(currentUserGroup) + beneficiaries, err := h.services.GetBeneficiariesInGroup(ctx, currentUserGroup) if err != nil { return nil, fmt.Errorf("could not get beneficiaries: %w", err) } @@ -1192,7 +1208,7 @@ func (h *ApplicationHandler) CreateOrganizedCarpoolJourneyBooking(ctx context.Co // Get passenger account and calculate pricing var passenger mobilityaccountsstorage.Account if passengerID != "" { - passenger, err = h.services.GetAccount(passengerID) + passenger, err = h.services.GetAccount(ctx, passengerID) if err != nil { return "", fmt.Errorf("could not get passenger account: %w", err) } @@ -1268,7 +1284,7 @@ func (h *ApplicationHandler) CreateOrganizedCarpoolJourneyBooking(ctx context.Co if message != "" && !doNotSend { send_message := strings.ReplaceAll(message, "{booking_id}", bookingRes.Booking.Id) log.Debug().Str("message", send_message).Msg("Carpool booking created: sending message") - h.GenerateSMS(driverID, send_message) + h.GenerateSMS(ctx, driverID, send_message) } return bookingRes.Booking.Id, nil @@ -1403,7 +1419,7 @@ func (h *ApplicationHandler) GetOrganizedCarpoolBookings(ctx context.Context, st } // Get beneficiaries - beneficiariesMap, err := h.services.GetBeneficiariesMap() + beneficiariesMap, err := h.services.GetBeneficiariesMap(ctx) if err != nil { beneficiariesMap = map[string]mobilityaccountsstorage.Account{} } diff --git a/core/application/sms.go b/core/application/sms.go index 91292bc..5eba1ef 100644 --- a/core/application/sms.go +++ b/core/application/sms.go @@ -8,11 +8,11 @@ import ( ) func (h *ApplicationHandler) SendSMS(ctx context.Context, beneficiaryID, message string) error { - return h.GenerateSMS(beneficiaryID, message) + return h.GenerateSMS(ctx, beneficiaryID, message) } -func (h *ApplicationHandler) GenerateSMS(recipientid string, message string) error { - recipient, err := h.services.GetAccount(recipientid) +func (h *ApplicationHandler) GenerateSMS(ctx context.Context, recipientid string, message string) error { + recipient, err := h.services.GetAccount(ctx, recipientid) if err != nil { log.Error().Err(err).Msg("user not found") return err diff --git a/core/application/solidarity-transport.go b/core/application/solidarity-transport.go index 5b3ac23..4fffa80 100644 --- a/core/application/solidarity-transport.go +++ b/core/application/solidarity-transport.go @@ -183,7 +183,7 @@ func filterBookingsByGeography(bookings []*solidaritytypes.Booking, departurePol func (h *ApplicationHandler) GetSolidarityTransportOverview(ctx context.Context, status, driverID, startDate, endDate, departureGeoLayer, departureGeoCode, destinationGeoLayer, destinationGeoCode, passengerAddressGeoLayer, passengerAddressGeoCode, histStatus, histDriverID, histStartDate, histEndDate, histDepartureGeoLayer, histDepartureGeoCode, histDestinationGeoLayer, histDestinationGeoCode, histPassengerAddressGeoLayer, histPassengerAddressGeoCode string, archivedFilter bool, driverAddressGeoLayer, driverAddressGeoCode string) (*SolidarityTransportOverviewResult, error) { // Get ALL drivers for the accountsMap (used in bookings display) - allDrivers, err := h.solidarityDrivers("", false) + allDrivers, err := h.solidarityDrivers(ctx, "", false) if err != nil { log.Error().Err(err).Msg("issue getting all solidarity drivers") allDrivers = []mobilityaccountsstorage.Account{} @@ -196,7 +196,7 @@ func (h *ApplicationHandler) GetSolidarityTransportOverview(ctx context.Context, } // Get filtered drivers for the drivers tab display - accounts, err := h.solidarityDrivers("", archivedFilter) + accounts, err := h.solidarityDrivers(ctx, "", archivedFilter) if err != nil { log.Error().Err(err).Msg("issue getting solidarity drivers") accounts = []mobilityaccountsstorage.Account{} @@ -241,7 +241,7 @@ func (h *ApplicationHandler) GetSolidarityTransportOverview(ctx context.Context, return strings.Compare(firstNameA, firstNameB) }) - beneficiariesMap, err := h.services.GetBeneficiariesMap() + beneficiariesMap, err := h.services.GetBeneficiariesMap(ctx) if err != nil { beneficiariesMap = map[string]mobilityaccountsstorage.Account{} } @@ -435,7 +435,7 @@ type SolidarityTransportBookingsResult struct { func (h *ApplicationHandler) GetSolidarityTransportBookings(ctx context.Context, startDate, endDate *time.Time, status, driverID, departureGeoLayer, departureGeoCode, destinationGeoLayer, destinationGeoCode, passengerAddressGeoLayer, passengerAddressGeoCode string) (*SolidarityTransportBookingsResult, error) { // Get all drivers - drivers, err := h.solidarityDrivers("", false) + drivers, err := h.solidarityDrivers(ctx, "", false) if err != nil { log.Error().Err(err).Msg("issue getting solidarity drivers") drivers = []mobilityaccountsstorage.Account{} @@ -447,7 +447,7 @@ func (h *ApplicationHandler) GetSolidarityTransportBookings(ctx context.Context, } // Get beneficiaries - beneficiariesMap, err := h.services.GetBeneficiariesMap() + beneficiariesMap, err := h.services.GetBeneficiariesMap(ctx) if err != nil { beneficiariesMap = map[string]mobilityaccountsstorage.Account{} } @@ -1030,7 +1030,7 @@ func (h *ApplicationHandler) GetSolidarityTransportJourneyData(ctx context.Conte // Get passenger account var passenger mobilityaccountsstorage.Account if passengerID != "" { - passengerResp, err := h.services.GetAccount(passengerID) + passengerResp, err := h.services.GetAccount(ctx, passengerID) if err != nil { return nil, fmt.Errorf("could not get passenger account: %w", err) } @@ -1048,7 +1048,7 @@ func (h *ApplicationHandler) GetSolidarityTransportJourneyData(ctx context.Conte } // Get beneficiaries in current user's group - beneficiaries, err := h.services.GetBeneficiariesInGroup(currentUserGroup) + beneficiaries, err := h.services.GetBeneficiariesInGroup(ctx, currentUserGroup) if err != nil { return nil, fmt.Errorf("could not get beneficiaries: %w", err) } @@ -1093,7 +1093,7 @@ func (h *ApplicationHandler) CreateSolidarityTransportJourneyBooking(ctx context // Get passenger account for pricing var passenger mobilityaccountsstorage.Account if passengerID != "" { - passengerResp, err := h.services.GetAccount(passengerID) + passengerResp, err := h.services.GetAccount(ctx, passengerID) if err != nil { return "", fmt.Errorf("could not get passenger account: %w", err) } @@ -1171,7 +1171,7 @@ func (h *ApplicationHandler) CreateSolidarityTransportJourneyBooking(ctx context // Send SMS if not disabled if !doNotSend && message != "" { send_message := strings.ReplaceAll(message, "{booking_id}", resp.Booking.Id) - if err := h.GenerateSMS(driverID, send_message); err != nil { + if err := h.GenerateSMS(ctx, driverID, send_message); err != nil { log.Error().Err(err).Msg("failed to send SMS") } } @@ -1248,12 +1248,12 @@ func (h *ApplicationHandler) GetSolidarityTransportBookingData(ctx context.Conte }, nil } -func (h *ApplicationHandler) solidarityDrivers(searchFilter string, archivedFilter bool) ([]mobilityaccountsstorage.Account, error) { +func (h *ApplicationHandler) solidarityDrivers(ctx context.Context, searchFilter string, archivedFilter bool) ([]mobilityaccountsstorage.Account, error) { request := &mobilityaccounts.GetAccountsRequest{ Namespaces: []string{"solidarity_drivers"}, } - resp, err := h.services.GRPC.MobilityAccounts.GetAccounts(context.TODO(), request) + resp, err := h.services.GRPC.MobilityAccounts.GetAccounts(ctx, request) if err != nil { return nil, err } @@ -1320,7 +1320,7 @@ func (h *ApplicationHandler) CalculateSolidarityTransportPricing(ctx context.Con // Get passenger account var passenger mobilityaccountsstorage.Account if passengerID != "" { - passengerResp, err := h.services.GetAccount(passengerID) + passengerResp, err := h.services.GetAccount(ctx, passengerID) if err != nil { return nil, fmt.Errorf("could not get passenger account: %w", err) } @@ -1376,6 +1376,17 @@ func (h *ApplicationHandler) calculateSolidarityTransportPricing(ctx context.Con } } } + if pst, ok := op_map["previous_solidarity_transport_count"]; ok { + if pst_str, ok := pst.(string); ok { + if pst_str != "" { + if n, err := strconv.Atoi(pst_str); err == nil { + history = history + n + } else { + log.Error().Err(err).Str("n", pst_str).Msg("string to int conversion error") + } + } + } + } } } @@ -1459,7 +1470,7 @@ func (h *ApplicationHandler) UpdateSolidarityTransportBookingStatus(ctx context. if previousStatus != "VALIDATED" && status == "VALIDATED" { if message != "" { send_message := strings.ReplaceAll(message, "{booking_id}", bookingID) - h.GenerateSMS(passenger.ID, send_message) + h.GenerateSMS(ctx, passenger.ID, send_message) } if err := h.CreditWallet(ctx, passenger.ID, -1*booking.Journey.Price.Amount, "Transport solidaire", "Débit transport solidaire"); err != nil { return fmt.Errorf("could not debit passenger wallet: %w", err) @@ -1467,6 +1478,32 @@ func (h *ApplicationHandler) UpdateSolidarityTransportBookingStatus(ctx context. if err := h.CreditWallet(ctx, driver.ID, booking.DriverCompensationAmount, "Transport solidaire", "Crédit transport solidaire"); err != nil { return fmt.Errorf("could not credit driver wallet: %w", err) } + + if notify { + groupsrequest := &groupsmanagement.GetGroupsRequest{ + Namespaces: []string{"parcoursmob_organizations"}, + Member: booking.PassengerId, + } + + groupsresp, err := h.services.GRPC.GroupsManagement.GetGroups(ctx, groupsrequest) + if err != nil { + log.Error().Err(err).Msg("") + } else if len(groupsresp.Groups) > 0 { + members, _, err := h.groupmembers(groupsresp.Groups[0].Id) + if err != nil { + log.Error().Err(err).Msg("could not retrieve groupe members") + } else { + for _, m := range members { + if email, ok := m.Data["email"].(string); ok { + h.emailing.Send("solidarity_transport.booking_driver_accept", email, map[string]string{ + "bookingid": booking.Id, + "baseUrl": h.config.GetString("base_url"), + }) + } + } + } + } + } } // Credit passenger / debit driver when previous status was VALIDATED and new status is not VALIDATED anymore diff --git a/core/application/vehicles-management.go b/core/application/vehicles-management.go index d5b7ef8..5dd32b3 100755 --- a/core/application/vehicles-management.go +++ b/core/application/vehicles-management.go @@ -62,7 +62,7 @@ func (h *ApplicationHandler) GetVehiclesManagementOverview(ctx context.Context, } } - driversMap, _ := h.services.GetBeneficiariesMap() + driversMap, _ := h.services.GetBeneficiariesMap(ctx) sort.Sort(sorting.VehiclesByLicencePlate(vehicles)) sort.Sort(sorting.BookingsByStartdate(bookings)) @@ -180,7 +180,7 @@ func (h *ApplicationHandler) GetVehiclesManagementBookingsList(ctx context.Conte cacheID := uuid.NewString() h.cache.PutWithTTL(cacheID, bookings, 1*time.Hour) - driversMap, _ := h.services.GetBeneficiariesMap() + driversMap, _ := h.services.GetBeneficiariesMap(ctx) return &VehiclesManagementBookingsListResult{ VehiclesMap: vehiclesMap, @@ -263,7 +263,7 @@ func (h *ApplicationHandler) GetVehicleDisplay(ctx context.Context, vehicleID st return nil, fmt.Errorf("failed to get vehicle: %w", err) } - beneficiaries, err := h.services.GetBeneficiariesMap() + beneficiaries, err := h.services.GetBeneficiariesMap(ctx) if err != nil { return nil, fmt.Errorf("failed to get beneficiaries: %w", err) } diff --git a/core/application/vehicles.go b/core/application/vehicles.go index a5a64b7..6ad30c2 100755 --- a/core/application/vehicles.go +++ b/core/application/vehicles.go @@ -119,7 +119,7 @@ func (h *ApplicationHandler) SearchVehicles(ctx context.Context, beneficiaryID s beneficiarydocuments = h.filestorage.List(filestorage.PREFIX_BENEFICIARIES + "/" + beneficiary.ID) } - accounts, err := h.services.GetBeneficiariesMap() + accounts, err := h.services.GetBeneficiariesMap(ctx) if err != nil { return nil, fmt.Errorf("failed to get beneficiaries: %w", err) } diff --git a/core/application/wallets.go b/core/application/wallets.go index f7045b4..6efcc3f 100644 --- a/core/application/wallets.go +++ b/core/application/wallets.go @@ -10,7 +10,7 @@ import ( ) func (h *ApplicationHandler) CreditWallet(ctx context.Context, userid string, amount float64, paymentMethod string, description string) error { - account, err := h.services.GetAccount(userid) + account, err := h.services.GetAccount(ctx, userid) if err != nil { log.Error().Err(err).Msg("could not retrieve account") return err diff --git a/go.mod b/go.mod index 4f0f8ce..457c1ab 100755 --- a/go.mod +++ b/go.mod @@ -57,7 +57,7 @@ require ( git.coopgo.io/coopgo-platform/routing-service v0.0.0-20250304234521-faabcc54f536 git.coopgo.io/coopgo-platform/saved-search v0.0.0-20251008070953-efccea3f6463 git.coopgo.io/coopgo-platform/sms v0.0.0-20250523074631-1f1e7fc6b7af - git.coopgo.io/coopgo-platform/solidarity-transport v0.0.0-20251016152145-e0882db1bcbc + git.coopgo.io/coopgo-platform/solidarity-transport v0.0.0-20260114093602-8875adbcbbee github.com/arran4/golang-ical v0.3.1 github.com/coreos/go-oidc/v3 v3.11.0 github.com/go-viper/mapstructure/v2 v2.4.0 @@ -164,6 +164,7 @@ require ( go.etcd.io/etcd/api/v3 v3.5.12 // indirect go.etcd.io/etcd/client/pkg/v3 v3.5.12 // indirect go.mongodb.org/mongo-driver v1.17.4 // indirect + go.mongodb.org/mongo-driver/v2 v2.1.0 // indirect go.uber.org/atomic v1.11.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.21.0 // indirect diff --git a/go.sum b/go.sum index a3e7644..d38c0ce 100644 --- a/go.sum +++ b/go.sum @@ -28,6 +28,8 @@ git.coopgo.io/coopgo-platform/sms v0.0.0-20250523074631-1f1e7fc6b7af h1:KxHim1dF git.coopgo.io/coopgo-platform/sms v0.0.0-20250523074631-1f1e7fc6b7af/go.mod h1:mad9D+WICDdpJzB+8H/wEVVbllK2mU6VLVByrppc9x0= git.coopgo.io/coopgo-platform/solidarity-transport v0.0.0-20251016152145-e0882db1bcbc h1:NLU5DUo5Kt3jkPhV3KkqQMahTHIrGildBvYlHwJ6JmM= git.coopgo.io/coopgo-platform/solidarity-transport v0.0.0-20251016152145-e0882db1bcbc/go.mod h1:iaFXcIn7DYtKlLrSYs9C47Dt7eeMGYkpx+unLCx8TpQ= +git.coopgo.io/coopgo-platform/solidarity-transport v0.0.0-20260114093602-8875adbcbbee h1:aoXSugsrZrM8E3WhqOCM+bLgGdxdf7dZAxx/vfbYzWQ= +git.coopgo.io/coopgo-platform/solidarity-transport v0.0.0-20260114093602-8875adbcbbee/go.mod h1:iaFXcIn7DYtKlLrSYs9C47Dt7eeMGYkpx+unLCx8TpQ= github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= github.com/DATA-DOG/go-sqlmock v1.3.2/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU= @@ -360,6 +362,8 @@ go.etcd.io/etcd/client/v3 v3.5.12/go.mod h1:tSbBCakoWmmddL+BKVAJHa9km+O/E+bumDe9 go.mongodb.org/mongo-driver v1.11.4/go.mod h1:PTSz5yu21bkT/wXpkS7WR5f0ddqw5quethTUn9WM+2g= go.mongodb.org/mongo-driver v1.17.4 h1:jUorfmVzljjr0FLzYQsGP8cgN/qzzxlY9Vh0C9KFXVw= go.mongodb.org/mongo-driver v1.17.4/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ= +go.mongodb.org/mongo-driver/v2 v2.1.0 h1:/ELnVNjmfUKDsoBisXxuJL0noR9CfeUIrP7Yt3R+egg= +go.mongodb.org/mongo-driver/v2 v2.1.0/go.mod h1:AWiLRShSrk5RHQS3AEn3RL19rqOzVq49MCpWQ3x/huI= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= diff --git a/main.go b/main.go index 350f072..f573452 100755 --- a/main.go +++ b/main.go @@ -86,7 +86,7 @@ func main() { wg.Add(1) go func() { defer wg.Done() - publicweb.Run(cfg, svc, applicationHandler, kv, filestorage) + publicweb.Run(cfg, svc, applicationHandler, kv, filestorage, emailing) }() } diff --git a/renderer/renderer.go b/renderer/renderer.go index 606806e..d9e6d69 100755 --- a/renderer/renderer.go +++ b/renderer/renderer.go @@ -1,6 +1,7 @@ package renderer import ( + "bytes" "fmt" "html/template" "net/http" @@ -53,13 +54,22 @@ func (renderer *Renderer) Render(name string, w http.ResponseWriter, r *http.Req prefixed_files = append(prefixed_files, renderer.templateFile(f)) } - w.WriteHeader(http.StatusOK) t := template.New(name).Funcs(GetTemplateFuncMap(state.Group, renderer.GlobalConfig, renderer.FileStorage)) t = template.Must(t.ParseFiles(prefixed_files...)) - err := t.ExecuteTemplate(w, "main", state) + // Render to buffer first to avoid write timeouts during template execution + var buf bytes.Buffer + err := t.ExecuteTemplate(&buf, "main", state) if err != nil { log.Error().Err(err).Msg("issue executing template") + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusOK) + _, err = buf.WriteTo(w) + if err != nil { + log.Error().Err(err).Msg("issue writing template to response") } } @@ -69,13 +79,22 @@ func (renderer *Renderer) RenderNoLayout(name string, w http.ResponseWriter, r * prefixed_files = append(prefixed_files, renderer.templateFile(f)) } - w.WriteHeader(http.StatusOK) t := template.New(name).Funcs(GetTemplateFuncMap(state.Group, renderer.GlobalConfig, renderer.FileStorage)) - t = template.Must(t.ParseFiles(prefixed_files...)) - err := t.ExecuteTemplate(w, "main", state) + + // Render to buffer first to avoid write timeouts during template execution + var buf bytes.Buffer + err := t.ExecuteTemplate(&buf, "main", state) if err != nil { log.Error().Err(err).Msg("issue executing template") + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusOK) + _, err = buf.WriteTo(w) + if err != nil { + log.Error().Err(err).Msg("issue writing template to response") } } diff --git a/servers/publicweb/api.go b/servers/publicweb/api.go new file mode 100644 index 0000000..6dcf857 --- /dev/null +++ b/servers/publicweb/api.go @@ -0,0 +1,64 @@ +package publicweb + +import ( + "encoding/json" + "net/http" + + "github.com/gorilla/mux" + "github.com/rs/zerolog/log" +) + +func (s *PublicWebServer) setupAPIRoutes(r *mux.Router) { + api := r.PathPrefix("/api").Subrouter() + api.HandleFunc("/contact", s.contactHandler).Methods("POST", "OPTIONS") +} + +func (s *PublicWebServer) contactHandler(w http.ResponseWriter, r *http.Request) { + // Handle CORS preflight + if r.Method == "OPTIONS" { + w.Header().Set("Access-Control-Allow-Origin", "*") + w.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS") + w.Header().Set("Access-Control-Allow-Headers", "Content-Type") + w.WriteHeader(http.StatusOK) + return + } + + var data map[string]any + if err := json.NewDecoder(r.Body).Decode(&data); err != nil { + log.Error().Err(err).Msg("Failed to decode contact request") + http.Error(w, "Invalid request body", http.StatusBadRequest) + return + } + + if len(data) == 0 { + http.Error(w, "Request body cannot be empty", http.StatusBadRequest) + return + } + + // Structure data for email template + emailData := map[string]any{ + "baseUrl": s.cfg.GetString("base_url"), + "fields": data, + } + + // Send email using the mailer + contactEmail := s.cfg.GetString("server.publicweb.contact_email") + if contactEmail == "" { + log.Error().Msg("Contact email not configured") + http.Error(w, "Contact service not configured", http.StatusInternalServerError) + return + } + + if err := s.mailer.Send("contact.request", contactEmail, emailData); err != nil { + log.Error().Err(err).Msg("Failed to send contact email") + http.Error(w, "Failed to send message", http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(map[string]string{ + "status": "success", + "message": "Message sent successfully", + }) +} diff --git a/servers/publicweb/publicweb.go b/servers/publicweb/publicweb.go index 008cc5d..0c0aafc 100644 --- a/servers/publicweb/publicweb.go +++ b/servers/publicweb/publicweb.go @@ -16,6 +16,7 @@ import ( "git.coopgo.io/coopgo-apps/parcoursmob/core/application" cache "git.coopgo.io/coopgo-apps/parcoursmob/core/utils/storage" "git.coopgo.io/coopgo-apps/parcoursmob/services" + "git.coopgo.io/coopgo-platform/emailing" ) // DataProvider returns data to hydrate a page @@ -36,6 +37,7 @@ type PublicWebServer struct { kv cache.KVHandler filestorage cache.FileStorage applicationHandler *application.ApplicationHandler + mailer *emailing.Mailer rootDir string dynamicRoutes map[string]DynamicRoute } @@ -46,6 +48,7 @@ func Run( applicationHandler *application.ApplicationHandler, kv cache.KVHandler, filestorage cache.FileStorage, + mailer *emailing.Mailer, ) { address := cfg.GetString("server.publicweb.listen") rootDir := cfg.GetString("server.publicweb.root_dir") @@ -57,6 +60,7 @@ func Run( kv: kv, filestorage: filestorage, applicationHandler: applicationHandler, + mailer: mailer, rootDir: rootDir, dynamicRoutes: make(map[string]DynamicRoute), } @@ -67,6 +71,9 @@ func Run( r.HandleFunc("/health", server.healthHandler).Methods("GET") + // Setup API routes + server.setupAPIRoutes(r) + for pattern := range server.dynamicRoutes { r.HandleFunc(pattern, server.dynamicHandler).Methods("GET", "POST") } diff --git a/servers/web/application/administration.go b/servers/web/application/administration.go index 2304586..38ecd23 100644 --- a/servers/web/application/administration.go +++ b/servers/web/application/administration.go @@ -222,7 +222,7 @@ func (h *Handler) AdminStatsBookingsHTTPHandler() http.HandlerFunc { func (h *Handler) AdminStatsBeneficiariesHTTPHandler() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - result, err := h.applicationHandler.GetBeneficiariesStats() + result, err := h.applicationHandler.GetBeneficiariesStats(r.Context()) if err != nil { log.Error().Err(err).Msg("error retrieving beneficiaries stats") http.Error(w, "Internal Server Error", http.StatusInternalServerError) diff --git a/servers/web/application/journeys.go b/servers/web/application/journeys.go index 01e82e5..b9cdaff 100644 --- a/servers/web/application/journeys.go +++ b/servers/web/application/journeys.go @@ -59,7 +59,7 @@ func (h *Handler) JourneysSearchHTTPHandler() http.HandlerFunc { var departureGeo *geojson.Feature if departure == "" && passengerID != "" { // Get passenger address - p, err := h.services.GetAccount(passengerID) + p, err := h.services.GetAccount(r.Context(), passengerID) if err != nil { log.Error().Err(err).Msg("could not retrieve passenger") http.Error(w, "Not Found", http.StatusNotFound) @@ -123,7 +123,7 @@ func (h *Handler) JourneysSearchHTTPHandler() http.HandlerFunc { } group := g.(groupstorage.Group) - beneficiaries, err := h.services.GetBeneficiariesInGroup(group) + beneficiaries, err := h.services.GetBeneficiariesInGroup(r.Context(), group) if err != nil { log.Error().Err(err).Msg("issue retrieving beneficiaries") http.Error(w, "Internal Server Error", http.StatusInternalServerError) @@ -345,7 +345,7 @@ func (h *Handler) JourneysSearchCompactHTTPHandler() http.HandlerFunc { var departureGeo *geojson.Feature if departure == "" && passengerID != "" { // Get passenger address - p, err := h.services.GetAccount(passengerID) + p, err := h.services.GetAccount(r.Context(), passengerID) if err != nil { log.Error().Err(err).Msg("could not retrieve passenger") http.Error(w, "Not Found", http.StatusNotFound) diff --git a/servers/web/web.go b/servers/web/web.go index a08bb95..e63269d 100644 --- a/servers/web/web.go +++ b/servers/web/web.go @@ -89,8 +89,8 @@ func Run(cfg *viper.Viper, services *services.ServicesHandler, renderer *rendere srv := &http.Server{ Handler: r, Addr: address, - WriteTimeout: 30 * time.Second, - ReadTimeout: 15 * time.Second, + WriteTimeout: 120 * time.Second, + ReadTimeout: 30 * time.Second, } log.Info().Str("service_name", service_name).Str("address", address).Msg("Running HTTP server") diff --git a/services/mobilityaccounts.go b/services/mobilityaccounts.go index 7c99263..069eeaf 100755 --- a/services/mobilityaccounts.go +++ b/services/mobilityaccounts.go @@ -26,12 +26,12 @@ func NewMobilityAccountService(mobilityAccountsDial string) (*MobilityAccountSer }, nil } -func (s *ServicesHandler) GetBeneficiaries() (accounts []storage.Account, err error) { +func (s *ServicesHandler) GetBeneficiaries(ctx context.Context) (accounts []storage.Account, err error) { accounts = []storage.Account{} request := &mobilityaccounts.GetAccountsRequest{ Namespaces: []string{"parcoursmob_beneficiaries"}, } - resp, err := s.GRPC.MobilityAccounts.GetAccounts(context.TODO(), request) + resp, err := s.GRPC.MobilityAccounts.GetAccounts(ctx, request) if err == nil { for _, v := range resp.Accounts { @@ -43,12 +43,12 @@ func (s *ServicesHandler) GetBeneficiaries() (accounts []storage.Account, err er return } -func (s *ServicesHandler) GetBeneficiariesMap() (accounts map[string]storage.Account, err error) { +func (s *ServicesHandler) GetBeneficiariesMap(ctx context.Context) (accounts map[string]storage.Account, err error) { accounts = map[string]storage.Account{} request := &mobilityaccounts.GetAccountsRequest{ Namespaces: []string{"parcoursmob_beneficiaries"}, } - resp, err := s.GRPC.MobilityAccounts.GetAccounts(context.TODO(), request) + resp, err := s.GRPC.MobilityAccounts.GetAccounts(ctx, request) if err == nil { for _, v := range resp.Accounts { @@ -59,12 +59,12 @@ func (s *ServicesHandler) GetBeneficiariesMap() (accounts map[string]storage.Acc return } -func (s *ServicesHandler) GetAccounts() (accounts []storage.Account, err error) { +func (s *ServicesHandler) GetAccounts(ctx context.Context) (accounts []storage.Account, err error) { accounts = []storage.Account{} request := &mobilityaccounts.GetAccountsRequest{ Namespaces: []string{"parcoursmob"}, } - resp, err := s.GRPC.MobilityAccounts.GetAccounts(context.TODO(), request) + resp, err := s.GRPC.MobilityAccounts.GetAccounts(ctx, request) if err == nil { for _, v := range resp.Accounts { @@ -76,12 +76,12 @@ func (s *ServicesHandler) GetAccounts() (accounts []storage.Account, err error) return } -func (s *ServicesHandler) GetAccountsInNamespace(namespace string) (accounts []storage.Account, err error) { +func (s *ServicesHandler) GetAccountsInNamespace(ctx context.Context, namespace string) (accounts []storage.Account, err error) { accounts = []storage.Account{} request := &mobilityaccounts.GetAccountsRequest{ Namespaces: []string{namespace}, } - resp, err := s.GRPC.MobilityAccounts.GetAccounts(context.TODO(), request) + resp, err := s.GRPC.MobilityAccounts.GetAccounts(ctx, request) if err == nil { for _, v := range resp.Accounts { @@ -92,12 +92,12 @@ func (s *ServicesHandler) GetAccountsInNamespace(namespace string) (accounts []s return } -func (s *ServicesHandler) GetAccountsInNamespaceMap(namespace string) (accounts map[string]storage.Account, err error) { +func (s *ServicesHandler) GetAccountsInNamespaceMap(ctx context.Context, namespace string) (accounts map[string]storage.Account, err error) { accounts = map[string]storage.Account{} request := &mobilityaccounts.GetAccountsRequest{ Namespaces: []string{namespace}, } - resp, err := s.GRPC.MobilityAccounts.GetAccounts(context.TODO(), request) + resp, err := s.GRPC.MobilityAccounts.GetAccounts(ctx, request) if err == nil { for _, v := range resp.Accounts { @@ -108,12 +108,12 @@ func (s *ServicesHandler) GetAccountsInNamespaceMap(namespace string) (accounts return } -func (s *ServicesHandler) GetAccountsInNamespacesMap(namespaces []string) (accounts map[string]storage.Account, err error) { +func (s *ServicesHandler) GetAccountsInNamespacesMap(ctx context.Context, namespaces []string) (accounts map[string]storage.Account, err error) { accounts = map[string]storage.Account{} request := &mobilityaccounts.GetAccountsRequest{ Namespaces: namespaces, } - resp, err := s.GRPC.MobilityAccounts.GetAccounts(context.TODO(), request) + resp, err := s.GRPC.MobilityAccounts.GetAccounts(ctx, request) if err == nil { for _, v := range resp.Accounts { @@ -124,11 +124,11 @@ func (s *ServicesHandler) GetAccountsInNamespacesMap(namespaces []string) (accou return } -func (s *ServicesHandler) GetAccount(id string) (account storage.Account, err error) { +func (s *ServicesHandler) GetAccount(ctx context.Context, id string) (account storage.Account, err error) { request := &mobilityaccounts.GetAccountRequest{ Id: id, } - resp, err := s.GRPC.MobilityAccounts.GetAccount(context.TODO(), request) + resp, err := s.GRPC.MobilityAccounts.GetAccount(ctx, request) if err != nil { return storage.Account{}, err } @@ -136,7 +136,7 @@ func (s *ServicesHandler) GetAccount(id string) (account storage.Account, err er return resp.Account.ToStorageType(), nil } -func (s *ServicesHandler) GetBeneficiariesInGroup(group groupstorage.Group) (accounts []storage.Account, err error) { +func (s *ServicesHandler) GetBeneficiariesInGroup(ctx context.Context, group groupstorage.Group) (accounts []storage.Account, err error) { accounts = []storage.Account{} if len(group.Members) == 0 { @@ -147,7 +147,7 @@ func (s *ServicesHandler) GetBeneficiariesInGroup(group groupstorage.Group) (acc Accountids: group.Members, } - resp, err := s.GRPC.MobilityAccounts.GetAccountsBatch(context.TODO(), request) + resp, err := s.GRPC.MobilityAccounts.GetAccountsBatch(ctx, request) if err != nil { return accounts, err }