From b4ef16a5fb9e44e607ccb5696035c4621527fa4d Mon Sep 17 00:00:00 2001 From: Marcos Fonseca Date: Wed, 24 Jul 2024 16:52:09 -0300 Subject: [PATCH 01/11] Implementando testfy --- backend/go.mod | 3 +++ 1 file changed, 3 insertions(+) diff --git a/backend/go.mod b/backend/go.mod index 77c94c9..dbb08a9 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -27,6 +27,7 @@ require ( github.com/bytedance/sonic/loader v0.1.1 // indirect github.com/cloudwego/base64x v0.1.4 // indirect github.com/cloudwego/iasm v0.2.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/gabriel-vasile/mimetype v1.4.4 // indirect github.com/gin-contrib/sse v0.1.0 // indirect @@ -62,7 +63,9 @@ require ( github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/pelletier/go-toml/v2 v2.2.2 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rogpeppe/go-internal v1.11.0 // indirect + github.com/stretchr/testify v1.9.0 // indirect github.com/swaggo/files v1.0.1 // indirect github.com/swaggo/gin-swagger v1.6.0 // indirect github.com/swaggo/swag v1.16.3 // indirect From c7a59b30f4d306de3586ae74dc90b469dc2b8518 Mon Sep 17 00:00:00 2001 From: Marcos Fonseca Date: Wed, 24 Jul 2024 16:52:22 -0300 Subject: [PATCH 02/11] Implementando retorno em SetupOAuthConfig --- backend/cmd/api/main.go | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/backend/cmd/api/main.go b/backend/cmd/api/main.go index 02d0b08..c33507d 100644 --- a/backend/cmd/api/main.go +++ b/backend/cmd/api/main.go @@ -13,7 +13,7 @@ import ( func main() { - config.SetupOAuthConfig() + oauthConfig := config.SetupOAuthConfig() token, err := models.LoadToken() @@ -23,8 +23,14 @@ func main() { if token == nil { log.Println("Token not found, redirecting to authentication...") - fmt.Println("Please visit the following link to authorize your Google account: ", config.OAuthConfig.AuthCodeURL("state-token", oauth2.AccessTypeOffline)) + fmt.Println("Please visit the following link to authorize your Google account: ", oauthConfig.AuthCodeURL("state-token", oauth2.AccessTypeOffline)) } - http.StartServer() + appAuthConfig := config.AuthConfig{ + Config: oauthConfig, + Token: token, + } + + http.StartServer(appAuthConfig) + } From 984e9253665a00c33bb95fa5fa89c9d778fac518 Mon Sep 17 00:00:00 2001 From: Marcos Fonseca Date: Wed, 24 Jul 2024 16:52:37 -0300 Subject: [PATCH 03/11] Implementando retorno em SetupOAuthConfig --- backend/config/config.go | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/backend/config/config.go b/backend/config/config.go index f3310cb..b509cef 100644 --- a/backend/config/config.go +++ b/backend/config/config.go @@ -3,8 +3,19 @@ package config import ( "log" "os" + + "github.com/joho/godotenv" + "golang.org/x/oauth2" + "golang.org/x/oauth2/google" + "google.golang.org/api/calendar/v3" + "google.golang.org/api/gmail/v1" ) +type AuthConfig struct { + Config *oauth2.Config + Token *oauth2.Token +} + func GetEventGoogleMeet() string { meetEvent := os.Getenv("GOOGLE_MEET_EVENT") @@ -15,3 +26,28 @@ func GetEventGoogleMeet() string { return meetEvent } + +func SetupOAuthConfig() *oauth2.Config { // Função que lê as variáveis de ambiente e retorna um ponteiro para uma configuração OAuth2 + + err := godotenv.Load() + + if err != nil { + log.Printf("Warning: No .env file found, reading from environment variables. Error: %v", err) + } + + clientID := os.Getenv("GOOGLE_CLIENT_ID") + clientSecret := os.Getenv("GOOGLE_CLIENT_SECRET") + redirectURL := os.Getenv("GOOGLE_REDIRECT_URL") + + if clientID == "" || clientSecret == "" || redirectURL == "" { + log.Fatal("Please set the GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, and GOOGLE_REDIRECT_URL environment variables") + } + + return &oauth2.Config{ + ClientID: clientID, + ClientSecret: clientSecret, + RedirectURL: redirectURL, + Scopes: []string{gmail.GmailSendScope, calendar.CalendarScope}, + Endpoint: google.Endpoint, + } +} From fe2232e54fa0abefda8012cbc13cb181845d9387 Mon Sep 17 00:00:00 2001 From: Marcos Fonseca Date: Wed, 24 Jul 2024 16:53:02 -0300 Subject: [PATCH 04/11] Implementando nova estrutura para auth --- backend/internal/auth/client.go | 17 +++++++++++++++++ backend/internal/auth/token.go | 25 +++++++++++++++++++++++++ 2 files changed, 42 insertions(+) create mode 100644 backend/internal/auth/client.go create mode 100644 backend/internal/auth/token.go diff --git a/backend/internal/auth/client.go b/backend/internal/auth/client.go new file mode 100644 index 0000000..188c1fc --- /dev/null +++ b/backend/internal/auth/client.go @@ -0,0 +1,17 @@ +package auth + +import ( + "context" + "fmt" + "net/http" + + "golang.org/x/oauth2" +) + +func CreateOAuthClient(ctx context.Context, oauthConfig *oauth2.Config, token *oauth2.Token) (*http.Client, error) { + client := oauthConfig.Client(ctx, token) + if client == nil { + return nil, fmt.Errorf("failed to create OAuth client") + } + return client, nil +} diff --git a/backend/internal/auth/token.go b/backend/internal/auth/token.go new file mode 100644 index 0000000..77b0b18 --- /dev/null +++ b/backend/internal/auth/token.go @@ -0,0 +1,25 @@ +package auth + +import ( + "context" + "fmt" + + "golang.org/x/oauth2" +) + +func ExchangeCodeForToken(ctx context.Context, oauthConfig *oauth2.Config, code string) (*oauth2.Token, error) { + token, err := oauthConfig.Exchange(ctx, code) + if err != nil { + return nil, fmt.Errorf("unable to retrieve token from web: %v", err) + } + return token, nil +} + +func RefreshToken(ctx context.Context, oauthConfig *oauth2.Config, token *oauth2.Token) (*oauth2.Token, error) { + ts := oauthConfig.TokenSource(ctx, token) + newToken, err := ts.Token() + if err != nil { + return nil, fmt.Errorf("failed to refresh token: %v", err) + } + return newToken, nil +} From f2208cf86b5bdd5f67651a0c522ad83f234cffd6 Mon Sep 17 00:00:00 2001 From: Marcos Fonseca Date: Wed, 24 Jul 2024 16:53:27 -0300 Subject: [PATCH 05/11] =?UTF-8?q?Refatorando=20servi=C3=A7os?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/internal/services/calendar_service.go | 74 ++++++++++++++++ backend/internal/services/email_service.go | 10 +++ backend/internal/services/gmail_service.go | 82 +++++++++++++++++ .../services/google_calendar_service.go | 88 +++++++++++++++++++ 4 files changed, 254 insertions(+) create mode 100644 backend/internal/services/calendar_service.go create mode 100644 backend/internal/services/email_service.go create mode 100644 backend/internal/services/gmail_service.go create mode 100644 backend/internal/services/google_calendar_service.go diff --git a/backend/internal/services/calendar_service.go b/backend/internal/services/calendar_service.go new file mode 100644 index 0000000..7a5a80a --- /dev/null +++ b/backend/internal/services/calendar_service.go @@ -0,0 +1,74 @@ +package services + +import ( + "context" + + "golang.org/x/oauth2" + "google.golang.org/api/calendar/v3" +) + +type CalendarService interface { // Interface que define os métodos que devem ser implementados por um serviço de calendário + InitializeService(ctx context.Context, config *oauth2.Config, token *oauth2.Token) (CalendarAPI, error) + AddGuestToEvent(ctx context.Context, service CalendarAPI, hangoutLink, email string) (*calendar.Event, error) + FindEventByHangoutLink(ctx context.Context, service CalendarAPI, hangoutLink string) (*calendar.Event, error) +} + +type CalendarAPI interface { + EventsList(calendarID string) EventsListCall + GetEvent(calendarID, eventID string) EventCall + UpdateEvent(calendarID, eventID string, event *calendar.Event) EventCall +} + +type EventsListCall interface { + Do() (*calendar.Events, error) +} + +type EventCall interface { + Do() (*calendar.Event, error) +} + +type RealCalendarService struct { + GoogleCalendar *calendar.Service +} + +func (rcs *RealCalendarService) EventsList(calendarID string) EventsListCall { + return &realEventsListCall{ + call: rcs.GoogleCalendar.Events.List(calendarID), + } +} + +func (rcs *RealCalendarService) GetEvent(calendarID, eventID string) EventCall { + return &realEventCall{ + getCall: rcs.GoogleCalendar.Events.Get(calendarID, eventID), + } +} + +func (rcs *RealCalendarService) UpdateEvent(calendarID, eventID string, event *calendar.Event) EventCall { + return &realUpdateEventCall{ + updateCall: rcs.GoogleCalendar.Events.Update(calendarID, eventID, event), + } +} + +type realEventsListCall struct { + call *calendar.EventsListCall +} + +func (rel *realEventsListCall) Do() (*calendar.Events, error) { + return rel.call.Do() +} + +type realEventCall struct { + getCall *calendar.EventsGetCall +} + +func (rec *realEventCall) Do() (*calendar.Event, error) { + return rec.getCall.Do() +} + +type realUpdateEventCall struct { + updateCall *calendar.EventsUpdateCall +} + +func (ruc *realUpdateEventCall) Do() (*calendar.Event, error) { + return ruc.updateCall.Do() +} diff --git a/backend/internal/services/email_service.go b/backend/internal/services/email_service.go new file mode 100644 index 0000000..f95f7d6 --- /dev/null +++ b/backend/internal/services/email_service.go @@ -0,0 +1,10 @@ +package services + +import ( + "golang.org/x/oauth2" + "google.golang.org/api/calendar/v3" +) + +type EmailService interface { // Interface que define os métodos que devem ser implementados por um serviço de e-mail + SendMentorshipInvitation(recipient string, eventDetails *calendar.Event, token *oauth2.Token) error +} diff --git a/backend/internal/services/gmail_service.go b/backend/internal/services/gmail_service.go new file mode 100644 index 0000000..ddde3fa --- /dev/null +++ b/backend/internal/services/gmail_service.go @@ -0,0 +1,82 @@ +package services + +import ( + "bytes" + "context" + "faladev/pkg/utils" + "fmt" + "html/template" + "log" + + "golang.org/x/oauth2" + "google.golang.org/api/calendar/v3" + "google.golang.org/api/gmail/v1" + "google.golang.org/api/option" +) + +type GmailService struct { + config *oauth2.Config + token *oauth2.Token +} + +func NewGmailService(config *oauth2.Config, token *oauth2.Token) EmailService { // Construtor responsável por encapsular a criação de uma instância de GmailService e retornar um ponteiro para ela + return &GmailService{ + config: config, + token: token, + } +} + +func (gs *GmailService) SendMentorshipInvitation(recipient string, eventDetails *calendar.Event, token *oauth2.Token) error { // Método que envia um convite de mentoria por e-mail + + ctx := context.Background() + client := gs.config.Client(ctx, token) + + srv, err := gmail.NewService(ctx, option.WithHTTPClient(client)) + + if err != nil { + log.Fatalf("Unable to retrieve Gmail client: %v", err) + return fmt.Errorf("Unable to retrieve Gmail client: %v", err) + } + + emailTo := recipient + emailFrom := "Marcos Fonseca " + subject := "Convite para Mentoria em Carreira e Tecnologia" + + tmpl, err := template.ParseFiles("templates/email/mentorship.html") + + if err != nil { + log.Fatalf("Error loading template: %v", err) + } + + data := struct { + HangoutLink string + HtmlLink string + }{ + HangoutLink: eventDetails.HangoutLink, + HtmlLink: eventDetails.HtmlLink, + } + + var body bytes.Buffer + + if err := tmpl.Execute(&body, data); err != nil { + log.Fatalf("Error executing template: %v", err) + } + + emailHeader := fmt.Sprintf("From: %s\r\nTo: %s\r\nSubject: %s\r\nContent-Type: text/html; charset=\"UTF-8\"\r\n\r\n", + emailFrom, emailTo, subject) + + fullEmail := emailHeader + body.String() + + encodedEmail := utils.Base64URLEncode([]byte(fullEmail)) + + var message gmail.Message + message.Raw = encodedEmail + + _, err = srv.Users.Messages.Send("me", &message).Do() + + if err != nil { + return fmt.Errorf("failed to send email: %v", err) + } + + return nil +} diff --git a/backend/internal/services/google_calendar_service.go b/backend/internal/services/google_calendar_service.go new file mode 100644 index 0000000..b1e5267 --- /dev/null +++ b/backend/internal/services/google_calendar_service.go @@ -0,0 +1,88 @@ +package services + +import ( + "context" + "faladev/internal/auth" + "fmt" + + "github.com/pkg/errors" + log "github.com/sirupsen/logrus" + "golang.org/x/oauth2" + "google.golang.org/api/calendar/v3" + "google.golang.org/api/option" +) + +type GoogleCalendarService struct{} // Estrutura que implementa a interface CalendarService + +func NewGoogleCalendarService() CalendarService { // Construtor responsável por encapsular a criação de uma instância de GoogleCalendarService e retornar um ponteiro para ela + return &GoogleCalendarService{} +} + +func (gcs *GoogleCalendarService) InitializeService(ctx context.Context, config *oauth2.Config, token *oauth2.Token) (CalendarAPI, error) { // Método que inicializa o serviço de calendário + + client, err := auth.CreateOAuthClient(ctx, config, token) + + if err != nil { + return nil, fmt.Errorf("error creating OAuth client: %v", err) + } + + service, err := calendar.NewService(ctx, option.WithHTTPClient(client)) + + if err != nil { + return nil, fmt.Errorf("error creating calendar service: %v", err) + } + return &RealCalendarService{GoogleCalendar: service}, nil +} + +func (gcs *GoogleCalendarService) FindEventByHangoutLink(ctx context.Context, api CalendarAPI, hangoutLink string) (*calendar.Event, error) { // Método que recebe um link do Google Meet e retorna o evento correspondente + + events, err := api.EventsList("primary").Do() + + if err != nil { + return nil, errors.Wrap(err, "error listing events") + } + + for _, event := range events.Items { + if event.HangoutLink == hangoutLink { + return event, nil + } + } + + return nil, fmt.Errorf("event with HangoutLink %s not found", hangoutLink) +} + +func (gcs *GoogleCalendarService) AddGuestToEvent(ctx context.Context, api CalendarAPI, hangoutLink, email string) (*calendar.Event, error) { // Método que adiciona um convidado a um evento + + eventDetails, err := gcs.FindEventByHangoutLink(ctx, api, hangoutLink) + + if err != nil { + return nil, err + } + + updatedEvent, err := api.GetEvent("primary", eventDetails.Id).Do() + + if err != nil { + return nil, errors.Wrap(err, "error getting event details") + } + + for _, attendee := range updatedEvent.Attendees { + if attendee.Email == email { + log.Infof("Guest %s is already in the event %s - Meet: %s\n", email, eventDetails.Id, hangoutLink) + return updatedEvent, nil + } + } + + attendee := &calendar.EventAttendee{Email: email} + + updatedEvent.Attendees = append(updatedEvent.Attendees, attendee) + + _, err = api.UpdateEvent("primary", updatedEvent.Id, updatedEvent).Do() + + if err != nil { + return nil, errors.Wrap(err, "error adding guest to event") + } + + log.Infof("Guest %s added to the event %s - Meet: %s\n", email, eventDetails.Id, hangoutLink) + + return updatedEvent, nil +} From ecc04aa6690ef0dc9ad8b65d272858bbb870162c Mon Sep 17 00:00:00 2001 From: Marcos Fonseca Date: Wed, 24 Jul 2024 16:53:46 -0300 Subject: [PATCH 06/11] =?UTF-8?q?Implementa=C3=A7=C3=A3o=20de=20tests=20e?= =?UTF-8?q?=20mocks=20para=20calendar=20service?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tests/google_calendar_service_mocks.go | 57 ++++++++++++ backend/tests/google_calendar_service_test.go | 87 +++++++++++++++++++ 2 files changed, 144 insertions(+) create mode 100644 backend/tests/google_calendar_service_mocks.go create mode 100644 backend/tests/google_calendar_service_test.go diff --git a/backend/tests/google_calendar_service_mocks.go b/backend/tests/google_calendar_service_mocks.go new file mode 100644 index 0000000..f2d9355 --- /dev/null +++ b/backend/tests/google_calendar_service_mocks.go @@ -0,0 +1,57 @@ +package services + +import ( + "faladev/internal/services" + "fmt" + + "google.golang.org/api/calendar/v3" +) + +type FakeCalendarService struct { + EventsListMock func(calendarID string) services.EventsListCall + GetEventMock func(calendarID, eventID string) services.EventCall + UpdateEventMock func(calendarID, eventID string, event *calendar.Event) services.EventCall +} + +func (f *FakeCalendarService) EventsList(calendarID string) services.EventsListCall { + if f.EventsListMock != nil { + return f.EventsListMock(calendarID) + } + return nil +} + +func (f *FakeCalendarService) GetEvent(calendarID, eventID string) services.EventCall { + if f.GetEventMock != nil { + return f.GetEventMock(calendarID, eventID) + } + return nil +} + +func (f *FakeCalendarService) UpdateEvent(calendarID, eventID string, event *calendar.Event) services.EventCall { + if f.UpdateEventMock != nil { + return f.UpdateEventMock(calendarID, eventID, event) + } + return nil +} + +type FakeEventsListCall struct { + DoFunc func() (*calendar.Events, error) +} + +func (f *FakeEventsListCall) Do() (*calendar.Events, error) { + if f.DoFunc != nil { + return f.DoFunc() + } + return nil, fmt.Errorf("Do function not implemented") +} + +type FakeEventCall struct { + DoFunc func() (*calendar.Event, error) +} + +func (f *FakeEventCall) Do() (*calendar.Event, error) { + if f.DoFunc != nil { + return f.DoFunc() + } + return nil, fmt.Errorf("Do function not implemented") +} diff --git a/backend/tests/google_calendar_service_test.go b/backend/tests/google_calendar_service_test.go new file mode 100644 index 0000000..73743ba --- /dev/null +++ b/backend/tests/google_calendar_service_test.go @@ -0,0 +1,87 @@ +package services + +import ( + "context" + "faladev/internal/services" + "testing" + + "github.com/pkg/errors" + log "github.com/sirupsen/logrus" + "github.com/stretchr/testify/assert" + "google.golang.org/api/calendar/v3" +) + +func TestFindEventByHangoutLink(t *testing.T) { + tests := []struct { + name string + hangoutLink string + mockDoFunc func() (*calendar.Events, error) + expectedEvent *calendar.Event + expectedError string + }{ + { + name: "Event Found", + hangoutLink: "https://meet.google.com/xxx-yyyy-zzz", + mockDoFunc: func() (*calendar.Events, error) { + return &calendar.Events{ + Items: []*calendar.Event{ + {HangoutLink: "https://meet.google.com/xxx-yyyy-zzz"}, + }, + }, nil + }, + expectedEvent: &calendar.Event{HangoutLink: "https://meet.google.com/xxx-yyyy-zzz"}, + expectedError: "", + }, + { + name: "Event Not Found", + hangoutLink: "https://meet.google.com/non-existent", + mockDoFunc: func() (*calendar.Events, error) { + return &calendar.Events{ + Items: []*calendar.Event{ + {HangoutLink: "https://meet.google.com/xxx-yyyy-zzz"}, + }, + }, nil + }, + expectedEvent: nil, + expectedError: "event with HangoutLink https://meet.google.com/non-existent not found", + }, + { + name: "API Error", + hangoutLink: "https://meet.google.com/xxx-yyyy-zzz", + mockDoFunc: func() (*calendar.Events, error) { + return nil, errors.New("API error") + }, + expectedEvent: nil, + expectedError: "error listing events: API error", + }, + } + + for _, tt := range tests { + + t.Run(tt.name, func(t *testing.T) { + mockService := &FakeCalendarService{ + EventsListMock: func(calendarID string) services.EventsListCall { + return &FakeEventsListCall{ + DoFunc: tt.mockDoFunc, + } + }, + } + + gcs := services.GoogleCalendarService{} + + event, err := gcs.FindEventByHangoutLink(context.Background(), mockService, tt.hangoutLink) + + if tt.expectedError != "" { + log.Infof("Error: %v", err) + log.Info("Event: ", event) + assert.EqualError(t, err, tt.expectedError) + assert.Error(t, err) + assert.Nil(t, event) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.expectedEvent, event) + } + }) + } + +} From 2dde5a6fe9f3ad602f4052fe2aad33067cd1f67a Mon Sep 17 00:00:00 2001 From: Marcos Fonseca Date: Wed, 24 Jul 2024 16:54:49 -0300 Subject: [PATCH 07/11] Refatorando http handler --- backend/config/oauth.go | 39 ------ backend/internal/services/calendar.go | 62 ---------- backend/internal/services/email.go | 65 ---------- backend/internal/services/oauth.go | 49 -------- backend/internal/services/service.go | 20 --- backend/pkg/http/http.go | 170 +++++++++++++++++++++++--- 6 files changed, 156 insertions(+), 249 deletions(-) delete mode 100644 backend/config/oauth.go delete mode 100644 backend/internal/services/calendar.go delete mode 100644 backend/internal/services/email.go delete mode 100644 backend/internal/services/oauth.go delete mode 100644 backend/internal/services/service.go diff --git a/backend/config/oauth.go b/backend/config/oauth.go deleted file mode 100644 index e79a6b0..0000000 --- a/backend/config/oauth.go +++ /dev/null @@ -1,39 +0,0 @@ -package config - -import ( - "log" - "os" - - "github.com/joho/godotenv" - "golang.org/x/oauth2" - "golang.org/x/oauth2/google" - "google.golang.org/api/calendar/v3" - "google.golang.org/api/gmail/v1" -) - -var OAuthConfig *oauth2.Config - -func SetupOAuthConfig() { - - err := godotenv.Load() - - if err != nil { - log.Printf("Warning: No .env file found, reading from environment variables. Error: %v", err) - } - - clientID := os.Getenv("GOOGLE_CLIENT_ID") - clientSecret := os.Getenv("GOOGLE_CLIENT_SECRET") - redirectURL := os.Getenv("GOOGLE_REDIRECT_URL") - - if clientID == "" || clientSecret == "" || redirectURL == "" { - log.Fatal("Please set the GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, and GOOGLE_REDIRECT_URL environment variables") - } - - OAuthConfig = &oauth2.Config{ - ClientID: clientID, - ClientSecret: clientSecret, - RedirectURL: redirectURL, - Scopes: []string{gmail.GmailSendScope, calendar.CalendarScope}, - Endpoint: google.Endpoint, - } -} diff --git a/backend/internal/services/calendar.go b/backend/internal/services/calendar.go deleted file mode 100644 index 4c4d6f3..0000000 --- a/backend/internal/services/calendar.go +++ /dev/null @@ -1,62 +0,0 @@ -package services - -import ( - "fmt" - - "github.com/pkg/errors" - log "github.com/sirupsen/logrus" - "google.golang.org/api/calendar/v3" -) - -func findEventByHangoutLink(calendarService *calendar.Service, hangoutLink string) (*calendar.Event, error) { - - events, err := calendarService.Events.List("primary").Do() - - if err != nil { - return nil, errors.Wrap(err, "error listing events") - } - - for _, event := range events.Items { - if event.HangoutLink == hangoutLink { - return event, nil - } - } - - return nil, fmt.Errorf("event with HangoutLink %s not found", hangoutLink) -} - -func AddGuestToEvent(calendarService *calendar.Service, hangoutLink, email string) (*calendar.Event, error) { - - eventDetails, err := findEventByHangoutLink(calendarService, hangoutLink) - - if err != nil { - return nil, err - } - - updatedEvent, err := calendarService.Events.Get("primary", eventDetails.Id).Do() - - if err != nil { - return nil, errors.Wrap(err, "error getting event details") - } - - for _, attendee := range updatedEvent.Attendees { - if attendee.Email == email { - log.Infof("Guest %s is already in the event %s - Meet: %s\n", email, eventDetails.Id, hangoutLink) - return updatedEvent, nil - } - } - - attendee := &calendar.EventAttendee{Email: email} - - updatedEvent.Attendees = append(updatedEvent.Attendees, attendee) - - _, err = calendarService.Events.Update("primary", updatedEvent.Id, updatedEvent).Do() - - if err != nil { - return nil, errors.Wrap(err, "error adding guest to event") - } - - log.Infof("Guest %s added to the event %s - Meet: %s\n", email, eventDetails.Id, hangoutLink) - - return updatedEvent, nil -} diff --git a/backend/internal/services/email.go b/backend/internal/services/email.go deleted file mode 100644 index 84f692c..0000000 --- a/backend/internal/services/email.go +++ /dev/null @@ -1,65 +0,0 @@ -package services - -import ( - "bytes" - "faladev/pkg/utils" - "fmt" - "html/template" - "log" - "net/http" - - "golang.org/x/oauth2" - "google.golang.org/api/calendar/v3" - "google.golang.org/api/gmail/v1" -) - -func SendMailMentorship(recipient string, eventDetails *calendar.Event, tok *oauth2.Token, client *http.Client) error { - - srv, err := gmail.New(client) - - if err != nil { - log.Fatalf("Unable to retrieve Gmail client: %v", err) - } - - emailTo := recipient - emailFrom := "Marcos Fonseca " - subject := "Convite para Mentoria em Carreira e Tecnologia" - - tmpl, err := template.ParseFiles("templates/email/mentorship.html") - - if err != nil { - log.Fatalf("Error loading template: %v", err) - } - - data := struct { - HangoutLink string - HtmlLink string - }{ - HangoutLink: eventDetails.HangoutLink, - HtmlLink: eventDetails.HtmlLink, - } - - var body bytes.Buffer - - if err := tmpl.Execute(&body, data); err != nil { - log.Fatalf("Error executing template: %v", err) - } - - emailHeader := fmt.Sprintf("From: %s\r\nTo: %s\r\nSubject: %s\r\nContent-Type: text/html; charset=\"UTF-8\"\r\n\r\n", - emailFrom, emailTo, subject) - - fullEmail := emailHeader + body.String() - - encodedEmail := utils.Base64URLEncode([]byte(fullEmail)) - - var message gmail.Message - message.Raw = encodedEmail - - _, err = srv.Users.Messages.Send("me", &message).Do() - - if err != nil { - return fmt.Errorf("failed to send email: %v", err) - } - - return nil -} diff --git a/backend/internal/services/oauth.go b/backend/internal/services/oauth.go deleted file mode 100644 index f4508d5..0000000 --- a/backend/internal/services/oauth.go +++ /dev/null @@ -1,49 +0,0 @@ -package services - -import ( - "context" - "faladev/config" - "faladev/internal/models" - "fmt" - "net/http" - - "golang.org/x/oauth2" -) - -func ExchangeCodeForToken(oauthConfig *oauth2.Config, code string) (*oauth2.Token, error) { - token, err := oauthConfig.Exchange(context.Background(), code) - if err != nil { - return nil, fmt.Errorf("unable to retrieve token from web: %v", err) - } - return token, nil -} - -func CreateOAuthClient(oauthConfig *oauth2.Config, token *oauth2.Token) (*http.Client, error) { - client := oauthConfig.Client(context.Background(), token) - if client == nil { - return nil, fmt.Errorf("failed to create OAuth client") - } - return client, nil -} - -func RefreshToken(token *oauth2.Token) (*oauth2.Token, error) { - - ts := config.OAuthConfig.TokenSource(context.Background(), token) - - newToken, err := ts.Token() - - if err != nil { - return nil, fmt.Errorf("failed to refresh token: %v", err) - } - - if newToken.AccessToken != token.AccessToken { - - err = models.SaveToken(newToken) - - if err != nil { - return nil, fmt.Errorf("failed to save new token: %v", err) - } - } - - return newToken, nil -} diff --git a/backend/internal/services/service.go b/backend/internal/services/service.go deleted file mode 100644 index d5d651d..0000000 --- a/backend/internal/services/service.go +++ /dev/null @@ -1,20 +0,0 @@ -package services - -import ( - "context" - "fmt" - - "golang.org/x/oauth2" - "google.golang.org/api/calendar/v3" -) - -var Token *oauth2.Token - -func InitializeCalendarService(ctx context.Context, config *oauth2.Config, token *oauth2.Token) (*calendar.Service, error) { - client := config.Client(ctx, token) - service, err := calendar.New(client) - if err != nil { - return nil, fmt.Errorf("erro ao criar o serviço do Google Calendar: %v", err) - } - return service, nil -} diff --git a/backend/pkg/http/http.go b/backend/pkg/http/http.go index 7b313c6..745fe29 100644 --- a/backend/pkg/http/http.go +++ b/backend/pkg/http/http.go @@ -1,24 +1,166 @@ package http -import ( - docs "faladev/cmd/docs" - "faladev/pkg/handlers" - "fmt" - swaggerfiles "github.com/swaggo/files" - ginSwagger "github.com/swaggo/gin-swagger" - "os" +type ErrorResponse struct { + Error string +} - "github.com/gin-gonic/gin" -) +type App struct { + config *oauth2.Config + calendarService services.CalendarService + emailService services.EmailService +} -func StartServer() { +func NewApp(config *oauth2.Config, calendar services.CalendarService, email services.EmailService) *App { + return &App{ + config: config, + calendarService: calendar, + emailService: email, + } +} - router := gin.Default() +// FormHandler handles the form page. +// @Summary Render form page +// @Description Renders the form.html page to display user information form. +// @Produce html +// @Success 200 {string} html "HTML content of the form page" +// @Router /form [get] +func (app *App) FormHandler(c *gin.Context) { + + tmpl, err := template.ParseFiles("templates/web/form.html") + + if err != nil { + c.AbortWithError(http.StatusInternalServerError, fmt.Errorf("error parsing template: %v", err)) + return + } + + tmpl.Execute(c.Writer, nil) +} + +// EventHandler handles the event handling endpoint. +// @Summary Handle event creation and interaction +// @Description Handles event creation, guest addition, email sending, and redirects to Google Meet link. +// @Accept json +// @Produce json +// @Param name formData string true "Name of the student" +// @Param email formData string true "Email of the student" +// @Param phone formData string true "Phone number of the student" +// @Success 303 {string} string "Redirects to Google Meet link" +// @Failure 400 {object} ErrorResponse "No Google Meet link available or other errors" +// @Failure 500 {object} ErrorResponse "Internal server error" +// @Router /event-handler [post] +func (app *App) EventHandler(c *gin.Context) { + + name := c.PostForm("name") + email := c.PostForm("email") + phone := c.PostForm("phone") + + if err := models.InsertOrUpdateStudent(name, email, phone); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Error inserting record into database: " + err.Error()}) + return + } + + token, err := models.LoadToken() + + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to load token: " + err.Error()}) + return + } + + if !token.Valid() { + + token, err = auth.RefreshToken(c.Request.Context(), app.config, token) + + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to refresh token: " + err.Error()}) + return + } + + err = models.SaveToken(token) + + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save token: " + err.Error()}) + return + } + } + + calendarService, err := app.calendarService.InitializeService(context.Background(), app.config, token) + + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Error initializing Google Calendar service: " + err.Error()}) + return + } - router.GET("/", handlers.FormHandler) + eventID := config.GetEventGoogleMeet() + eventDetails, err := app.calendarService.AddGuestToEvent(context.Background(), calendarService, eventID, email) + + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Error getting event details: " + err.Error()}) + return + } + + if err = app.emailService.SendMentorshipInvitation(email, eventDetails, token); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to send email: " + err.Error()}) + return + } + + if eventDetails.HangoutLink == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "No Google Meet link available for this event"}) + return + } + + c.Redirect(http.StatusSeeOther, eventDetails.HangoutLink) +} + +// OAuthCallbackHandler handles the OAuth2 callback endpoint. +// @Summary Handle OAuth2 callback +// @Description Exchange code for token and save it +// @Accept json +// @Produce json +// @Param state query string true "State token" +// @Param code query string true "Authorization code" +// @Success 303 "Redirects to /" +// @Failure 400 {object} ErrorResponse "State token doesn't match" +// @Failure 500 {object} ErrorResponse "Unable to retrieve or save token" +// @Router /oauth/callback [get] +func (app *App) OAuthCallbackHandler(c *gin.Context) { + + if c.Query("state") != "state-token" { + c.JSON(http.StatusBadRequest, &ErrorResponse{ + Error: "State token doesn't match", + }) + return + } + + code := c.Query("code") + + token, err := auth.ExchangeCodeForToken(c.Request.Context(), app.config, code) + + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Unable to retrieve token from web: %v", err)}) + return + } + + err = models.SaveToken(token) + + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Unable to save token: %v", err)}) + return + } + + fmt.Println("OAuth2 configured successfully with redirect URL:", app.config.RedirectURL) + + c.Redirect(http.StatusSeeOther, "/") +} + +func StartServer(appAuthConfig config.AuthConfig) { + + app := NewApp(appAuthConfig.Config, services.NewGoogleCalendarService(), services.NewGmailService(appAuthConfig.Config, appAuthConfig.Token)) + + router := gin.Default() - router.GET("/callback", handlers.OAuthCallbackHandler) - router.POST("/event", handlers.EventHandler) + router.GET("/", app.FormHandler) + router.GET("/callback", app.OAuthCallbackHandler) + router.POST("/event", app.EventHandler) router.Static("/static/", "static") From 3416d695bab930e6d3469fa25bbd74ba14b7eb1e Mon Sep 17 00:00:00 2001 From: Marcos Fonseca Date: Wed, 24 Jul 2024 17:11:50 -0300 Subject: [PATCH 08/11] Adicionando testify --- backend/go.mod | 13 ++++--------- backend/go.sum | 28 +++++----------------------- backend/pkg/http/http.go | 19 +++++++++++++++++++ 3 files changed, 28 insertions(+), 32 deletions(-) diff --git a/backend/go.mod b/backend/go.mod index dbb08a9..d7888e3 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -10,6 +10,10 @@ require ( github.com/lib/pq v1.10.9 github.com/pkg/errors v0.9.1 github.com/sirupsen/logrus v1.9.3 + github.com/stretchr/testify v1.9.0 + github.com/swaggo/files v1.0.1 + github.com/swaggo/gin-swagger v1.6.0 + github.com/swaggo/swag v1.16.3 golang.org/x/oauth2 v0.21.0 google.golang.org/api v0.186.0 gorm.io/driver/postgres v1.5.9 @@ -21,8 +25,6 @@ require ( cloud.google.com/go/auth/oauth2adapt v0.2.2 // indirect cloud.google.com/go/compute/metadata v0.3.0 // indirect github.com/KyleBanks/depth v1.2.1 // indirect - github.com/PuerkitoBio/purell v1.2.1 // indirect - github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect github.com/bytedance/sonic v1.11.9 // indirect github.com/bytedance/sonic/loader v0.1.1 // indirect github.com/cloudwego/base64x v0.1.4 // indirect @@ -56,7 +58,6 @@ require ( github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/cpuid/v2 v2.2.8 // indirect - github.com/kr/text v0.2.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-isatty v0.0.20 // indirect @@ -64,11 +65,6 @@ require ( github.com/modern-go/reflect2 v1.0.2 // indirect github.com/pelletier/go-toml/v2 v2.2.2 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/rogpeppe/go-internal v1.11.0 // indirect - github.com/stretchr/testify v1.9.0 // indirect - github.com/swaggo/files v1.0.1 // indirect - github.com/swaggo/gin-swagger v1.6.0 // indirect - github.com/swaggo/swag v1.16.3 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.2.12 // indirect go.opencensus.io v0.24.0 // indirect @@ -86,6 +82,5 @@ require ( google.golang.org/genproto/googleapis/rpc v0.0.0-20240624140628-dc46fd24d27d // indirect google.golang.org/grpc v1.64.0 // indirect google.golang.org/protobuf v1.34.2 // indirect - gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/backend/go.sum b/backend/go.sum index beb3b9b..bf2ec86 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -8,10 +8,6 @@ cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1h github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= -github.com/PuerkitoBio/purell v1.2.1 h1:QsZ4TjvwiMpat6gBCBxEQI0rcS9ehtkKtSpiUnd9N28= -github.com/PuerkitoBio/purell v1.2.1/go.mod h1:ZwHcC/82TOaovDi//J/804umJFFmbOHPngi8iYYv/Eo= -github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= -github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= github.com/bytedance/sonic v1.11.9 h1:LFHENlIY/SLzDWverzdOvgMztTxcfcF+cqNsz9pK5zg= github.com/bytedance/sonic v1.11.9/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4= github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM= @@ -23,7 +19,6 @@ github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJ github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -35,6 +30,8 @@ github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2 github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/gabriel-vasile/mimetype v1.4.4 h1:QjV6pZ7/XZ7ryI2KuyeEDE8wnh7fHP9YnQy+R0LnH8I= github.com/gabriel-vasile/mimetype v1.4.4/go.mod h1:JwLei5XPtWdGiMFB5Pjle1oEeoSeEuJfJE+TtfvdB/s= +github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4= +github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk= github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= @@ -118,12 +115,8 @@ github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa02 github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM= github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= -github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= @@ -141,14 +134,11 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= -github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= -github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= @@ -192,8 +182,6 @@ golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= -golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30= golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -201,6 +189,8 @@ golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTk golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.19.0 h1:fEdghXQSo20giMthA7cd28ZC+jts4amQ3YMXiP5oMQ8= +golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -211,8 +201,6 @@ golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwY golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= -golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -235,8 +223,6 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= -golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= @@ -290,12 +276,8 @@ google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlba google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= -gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/backend/pkg/http/http.go b/backend/pkg/http/http.go index 745fe29..684201e 100644 --- a/backend/pkg/http/http.go +++ b/backend/pkg/http/http.go @@ -1,5 +1,24 @@ package http +import ( + "context" + docs "faladev/cmd/docs" + "faladev/config" + "faladev/internal/auth" + "faladev/internal/models" + "faladev/internal/services" + "fmt" + "html/template" + "net/http" + "os" + + swaggerfiles "github.com/swaggo/files" + ginSwagger "github.com/swaggo/gin-swagger" + "golang.org/x/oauth2" + + "github.com/gin-gonic/gin" +) + type ErrorResponse struct { Error string } From ae52be1162dd642299a2e5b20fd190f6276184f2 Mon Sep 17 00:00:00 2001 From: Marcos Fonseca Date: Wed, 24 Jul 2024 17:12:09 -0300 Subject: [PATCH 09/11] Removendo package handlers --- backend/pkg/handlers/handlers.go | 148 ------------------------------- 1 file changed, 148 deletions(-) delete mode 100644 backend/pkg/handlers/handlers.go diff --git a/backend/pkg/handlers/handlers.go b/backend/pkg/handlers/handlers.go deleted file mode 100644 index b1abc70..0000000 --- a/backend/pkg/handlers/handlers.go +++ /dev/null @@ -1,148 +0,0 @@ -package handlers - -import ( - "context" - "fmt" - "html/template" - "net/http" - - "github.com/gin-gonic/gin" - - "faladev/config" - "faladev/internal/models" - "faladev/internal/services" -) - -type ErrorResponse struct { - Error string -} - -// FormHandler handles the form page. -// @Summary Render form page -// @Description Renders the form.html page to display user information form. -// @Produce html -// @Success 200 {string} html "HTML content of the form page" -// @Router /form [get] -func FormHandler(c *gin.Context) { - tmpl, err := template.ParseFiles("templates/web/form.html") - - if err != nil { - c.AbortWithError(http.StatusInternalServerError, fmt.Errorf("error parsing template: %v", err)) - return - } - - tmpl.Execute(c.Writer, nil) -} - -// EventHandler handles the event handling endpoint. -// @Summary Handle event creation and interaction -// @Description Handles event creation, guest addition, email sending, and redirects to Google Meet link. -// @Accept json -// @Produce json -// @Param name formData string true "Name of the student" -// @Param email formData string true "Email of the student" -// @Param phone formData string true "Phone number of the student" -// @Success 303 {string} string "Redirects to Google Meet link" -// @Failure 400 {object} ErrorResponse "No Google Meet link available or other errors" -// @Failure 500 {object} ErrorResponse "Internal server error" -// @Router /event-handler [post] -func EventHandler(c *gin.Context) { - - name := c.PostForm("name") - email := c.PostForm("email") - phone := c.PostForm("phone") - - if err := models.InsertOrUpdateStudent(name, email, phone); err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Error inserting record into database: " + err.Error()}) - return - } - - token, err := models.LoadToken() - - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to load token: " + err.Error()}) - return - } - - if !token.Valid() { - token, err = services.RefreshToken(token) - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to refresh token: " + err.Error()}) - return - } - } - - calendarService, err := services.InitializeCalendarService(context.Background(), config.OAuthConfig, token) - - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Error initializing Google Calendar service: " + err.Error()}) - return - } - - eventDetails, err := services.AddGuestToEvent(calendarService, config.GetEventGoogleMeet(), email) - - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Error getting event details: " + err.Error()}) - return - } - - client, err := services.CreateOAuthClient(config.OAuthConfig, token) - - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create OAuth client: " + err.Error()}) - return - } - - if err = services.SendMailMentorship(email, eventDetails, token, client); err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to send email: " + err.Error()}) - return - } - - if eventDetails.HangoutLink == "" { - c.JSON(http.StatusBadRequest, gin.H{"error": "No Google Meet link available for this event"}) - return - } - - c.Redirect(http.StatusSeeOther, eventDetails.HangoutLink) -} - -// OAuthCallbackHandler handles the OAuth2 callback endpoint. -// @Summary Handle OAuth2 callback -// @Description Exchange code for token and save it -// @Accept json -// @Produce json -// @Param state query string true "State token" -// @Param code query string true "Authorization code" -// @Success 303 "Redirects to /" -// @Failure 400 {object} ErrorResponse "State token doesn't match" -// @Failure 500 {object} ErrorResponse "Unable to retrieve or save token" -// @Router /oauth/callback [get] -func OAuthCallbackHandler(c *gin.Context) { - - if c.Query("state") != "state-token" { - c.JSON(http.StatusBadRequest, &ErrorResponse{ - Error: "State token doesn't match", - }) - return - } - - code := c.Query("code") - - token, err := config.OAuthConfig.Exchange(context.Background(), code) - - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Unable to retrieve token from web: %v", err)}) - return - } - - err = models.SaveToken(token) - - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Unable to save token: %v", err)}) - return - } - - fmt.Println("OAuth2 configured successfully with redirect URL:", config.OAuthConfig.RedirectURL) - - c.Redirect(http.StatusSeeOther, "/") -} From 5b6617f76f7cab4721d9d304c2a701b5f75f3630 Mon Sep 17 00:00:00 2001 From: Marcos Fonseca Date: Wed, 24 Jul 2024 17:24:09 -0300 Subject: [PATCH 10/11] =?UTF-8?q?Removendo=20coment=C3=A1rios?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/config/config.go | 2 +- backend/internal/services/calendar_service.go | 2 +- backend/internal/services/email_service.go | 2 +- backend/internal/services/gmail_service.go | 4 ++-- backend/internal/services/google_calendar_service.go | 10 +++++----- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/backend/config/config.go b/backend/config/config.go index b509cef..1b2d6bb 100644 --- a/backend/config/config.go +++ b/backend/config/config.go @@ -27,7 +27,7 @@ func GetEventGoogleMeet() string { return meetEvent } -func SetupOAuthConfig() *oauth2.Config { // Função que lê as variáveis de ambiente e retorna um ponteiro para uma configuração OAuth2 +func SetupOAuthConfig() *oauth2.Config { err := godotenv.Load() diff --git a/backend/internal/services/calendar_service.go b/backend/internal/services/calendar_service.go index 7a5a80a..90ed3dd 100644 --- a/backend/internal/services/calendar_service.go +++ b/backend/internal/services/calendar_service.go @@ -7,7 +7,7 @@ import ( "google.golang.org/api/calendar/v3" ) -type CalendarService interface { // Interface que define os métodos que devem ser implementados por um serviço de calendário +type CalendarService interface { InitializeService(ctx context.Context, config *oauth2.Config, token *oauth2.Token) (CalendarAPI, error) AddGuestToEvent(ctx context.Context, service CalendarAPI, hangoutLink, email string) (*calendar.Event, error) FindEventByHangoutLink(ctx context.Context, service CalendarAPI, hangoutLink string) (*calendar.Event, error) diff --git a/backend/internal/services/email_service.go b/backend/internal/services/email_service.go index f95f7d6..6e5def1 100644 --- a/backend/internal/services/email_service.go +++ b/backend/internal/services/email_service.go @@ -5,6 +5,6 @@ import ( "google.golang.org/api/calendar/v3" ) -type EmailService interface { // Interface que define os métodos que devem ser implementados por um serviço de e-mail +type EmailService interface { SendMentorshipInvitation(recipient string, eventDetails *calendar.Event, token *oauth2.Token) error } diff --git a/backend/internal/services/gmail_service.go b/backend/internal/services/gmail_service.go index ddde3fa..523915f 100644 --- a/backend/internal/services/gmail_service.go +++ b/backend/internal/services/gmail_service.go @@ -19,14 +19,14 @@ type GmailService struct { token *oauth2.Token } -func NewGmailService(config *oauth2.Config, token *oauth2.Token) EmailService { // Construtor responsável por encapsular a criação de uma instância de GmailService e retornar um ponteiro para ela +func NewGmailService(config *oauth2.Config, token *oauth2.Token) EmailService { return &GmailService{ config: config, token: token, } } -func (gs *GmailService) SendMentorshipInvitation(recipient string, eventDetails *calendar.Event, token *oauth2.Token) error { // Método que envia um convite de mentoria por e-mail +func (gs *GmailService) SendMentorshipInvitation(recipient string, eventDetails *calendar.Event, token *oauth2.Token) error { ctx := context.Background() client := gs.config.Client(ctx, token) diff --git a/backend/internal/services/google_calendar_service.go b/backend/internal/services/google_calendar_service.go index b1e5267..dfb3389 100644 --- a/backend/internal/services/google_calendar_service.go +++ b/backend/internal/services/google_calendar_service.go @@ -12,13 +12,13 @@ import ( "google.golang.org/api/option" ) -type GoogleCalendarService struct{} // Estrutura que implementa a interface CalendarService +type GoogleCalendarService struct{} -func NewGoogleCalendarService() CalendarService { // Construtor responsável por encapsular a criação de uma instância de GoogleCalendarService e retornar um ponteiro para ela +func NewGoogleCalendarService() CalendarService { return &GoogleCalendarService{} } -func (gcs *GoogleCalendarService) InitializeService(ctx context.Context, config *oauth2.Config, token *oauth2.Token) (CalendarAPI, error) { // Método que inicializa o serviço de calendário +func (gcs *GoogleCalendarService) InitializeService(ctx context.Context, config *oauth2.Config, token *oauth2.Token) (CalendarAPI, error) { client, err := auth.CreateOAuthClient(ctx, config, token) @@ -34,7 +34,7 @@ func (gcs *GoogleCalendarService) InitializeService(ctx context.Context, config return &RealCalendarService{GoogleCalendar: service}, nil } -func (gcs *GoogleCalendarService) FindEventByHangoutLink(ctx context.Context, api CalendarAPI, hangoutLink string) (*calendar.Event, error) { // Método que recebe um link do Google Meet e retorna o evento correspondente +func (gcs *GoogleCalendarService) FindEventByHangoutLink(ctx context.Context, api CalendarAPI, hangoutLink string) (*calendar.Event, error) { events, err := api.EventsList("primary").Do() @@ -51,7 +51,7 @@ func (gcs *GoogleCalendarService) FindEventByHangoutLink(ctx context.Context, ap return nil, fmt.Errorf("event with HangoutLink %s not found", hangoutLink) } -func (gcs *GoogleCalendarService) AddGuestToEvent(ctx context.Context, api CalendarAPI, hangoutLink, email string) (*calendar.Event, error) { // Método que adiciona um convidado a um evento +func (gcs *GoogleCalendarService) AddGuestToEvent(ctx context.Context, api CalendarAPI, hangoutLink, email string) (*calendar.Event, error) { eventDetails, err := gcs.FindEventByHangoutLink(ctx, api, hangoutLink) From e39e812a8522587c56b051ed3d1ffe4083bd78fb Mon Sep 17 00:00:00 2001 From: Marcos Fonseca Date: Wed, 24 Jul 2024 17:28:58 -0300 Subject: [PATCH 11/11] Movendo template de PR para .github --- .../pull_request_template.md | 62 ------------------- 1 file changed, 62 deletions(-) delete mode 100644 .github/PULL_REQUEST_TEMPLATE/pull_request_template.md diff --git a/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md b/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md deleted file mode 100644 index c3e7a7f..0000000 --- a/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md +++ /dev/null @@ -1,62 +0,0 @@ -## Descrição - -Este pull request introduz melhorias no template HTML da página de erro. A principal melhoria é o aumento do tamanho da fonte do código de erro e da mensagem para torná-los mais destacados e fáceis de ler. - -## Mudanças Realizadas - -1. **Aumento do Tamanho da Fonte**: - - Adicionadas estilos CSS personalizados para aumentar o tamanho da fonte do código de erro e da mensagem. - - Aplicadas classes utilitárias do Tailwind CSS para garantir que o texto esteja centralizado. - -### Arquivos Modificados - -- `templates/web/error.html`: Estrutura HTML atualizada e adição de estilos personalizados. - -### Mudanças Detalhadas - -- **Tamanho da Fonte do Código de Erro**: - - Adicionada a classe CSS `.error-code` com `font-size: 4rem;` para aumentar o tamanho da fonte do código de erro. -- **Tamanho da Fonte da Mensagem de Erro**: - - Adicionada a classe CSS `.error-message` com `font-size: 1.5rem;` para aumentar o tamanho da fonte da mensagem de erro. -- **Alinhamento Central**: - - Aplicada a classe `text-center` do Tailwind CSS para centralizar o texto dentro do contêiner. - -## Antes e Depois - -### Antes - -![Screenshot Antes](link_para_screenshot_antes) - -### Depois - -![Screenshot Depois](link_para_screenshot_depois) - -## Motivação e Contexto - -O tamanho da fonte anterior para o código de erro e a mensagem era muito pequeno, dificultando para os usuários identificarem e entenderem rapidamente o erro. Aumentar o tamanho da fonte melhora a legibilidade e a experiência do usuário. - -## Como Isso Foi Testado? - -- Testado manualmente a página de erro para garantir que os novos tamanhos de fonte sejam aplicados corretamente e o texto permaneça centralizado. - -## Issue Relacionada - -- [Issue #123](link_para_issue_relacionada) - -## Tipos de Mudanças - -- [ ] Correção de bug (mudança que não quebra a compatibilidade e corrige um problema) -- [x] Nova funcionalidade (mudança que não quebra a compatibilidade e adiciona uma funcionalidade) -- [ ] Mudança que quebra a compatibilidade (correção ou funcionalidade que causa uma mudança em funcionalidades existentes) - -## Checklist - -- [x] Meu código segue o estilo de código deste projeto. -- [x] Minha mudança requer uma mudança na documentação. -- [x] Eu atualizei a documentação conforme necessário. -- [ ] Eu adicionei testes para cobrir minhas mudanças. -- [x] Todos os novos e antigos testes passaram. - -## Notas Adicionais - -- Qualquer informação ou contexto adicional que os revisores possam precisar saber.