diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index b66445b..0000000 Binary files a/.DS_Store and /dev/null differ 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. diff --git a/.gitignore b/.gitignore index d112da7..95713e0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,10 @@ -backend/.env +# dependencies +/node_modules + +#debug __debug* + +#misc +.DS_Store +backend/.env + 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) + } diff --git a/backend/config/config.go b/backend/config/config.go index f3310cb..1b2d6bb 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 { + + 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, + } +} 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/go.mod b/backend/go.mod index 77c94c9..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,12 +25,11 @@ 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 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 @@ -55,17 +58,13 @@ 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 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/rogpeppe/go-internal v1.11.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/pmezard/go-difflib v1.0.0 // 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 @@ -83,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/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 +} 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/calendar_service.go b/backend/internal/services/calendar_service.go new file mode 100644 index 0000000..90ed3dd --- /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 { + 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..6e5def1 --- /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 { + SendMentorshipInvitation(recipient string, eventDetails *calendar.Event, token *oauth2.Token) error +} diff --git a/backend/internal/services/email.go b/backend/internal/services/gmail_service.go similarity index 68% rename from backend/internal/services/email.go rename to backend/internal/services/gmail_service.go index 84f692c..523915f 100644 --- a/backend/internal/services/email.go +++ b/backend/internal/services/gmail_service.go @@ -2,23 +2,40 @@ package services import ( "bytes" + "context" "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" + "google.golang.org/api/option" ) -func SendMailMentorship(recipient string, eventDetails *calendar.Event, tok *oauth2.Token, client *http.Client) error { +type GmailService struct { + config *oauth2.Config + token *oauth2.Token +} + +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 { + + ctx := context.Background() + client := gs.config.Client(ctx, token) - srv, err := gmail.New(client) + 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 diff --git a/backend/internal/services/google_calendar_service.go b/backend/internal/services/google_calendar_service.go new file mode 100644 index 0000000..dfb3389 --- /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{} + +func NewGoogleCalendarService() CalendarService { + return &GoogleCalendarService{} +} + +func (gcs *GoogleCalendarService) InitializeService(ctx context.Context, config *oauth2.Config, token *oauth2.Token) (CalendarAPI, error) { + + 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) { + + 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) { + + 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 +} 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/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, "/") -} diff --git a/backend/pkg/http/http.go b/backend/pkg/http/http.go index 7b313c6..684201e 100644 --- a/backend/pkg/http/http.go +++ b/backend/pkg/http/http.go @@ -1,24 +1,185 @@ package http import ( + "context" docs "faladev/cmd/docs" - "faladev/pkg/handlers" + "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" - "os" + "golang.org/x/oauth2" "github.com/gin-gonic/gin" ) -func StartServer() { +type ErrorResponse struct { + Error string +} - router := gin.Default() +type App struct { + config *oauth2.Config + calendarService services.CalendarService + emailService services.EmailService +} + +func NewApp(config *oauth2.Config, calendar services.CalendarService, email services.EmailService) *App { + return &App{ + config: config, + calendarService: calendar, + emailService: email, + } +} + +// 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 + } + } - router.GET("/", handlers.FormHandler) + 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 + } + + 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") 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) + } + }) + } + +} diff --git a/node_modules/.package-lock.json b/node_modules/.package-lock.json deleted file mode 100644 index b8c1c10..0000000 --- a/node_modules/.package-lock.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "faladev", - "lockfileVersion": 2, - "requires": true, - "packages": {} -} diff --git a/teste.txt b/teste.txt new file mode 100644 index 0000000..e69de29