diff --git a/apperror/error.go b/apperror/error.go index abcf846..a0cbfb8 100644 --- a/apperror/error.go +++ b/apperror/error.go @@ -18,4 +18,8 @@ var ( InternalError = &AppError{"internal-server-error", http.StatusInternalServerError} BadRequest = &AppError{"bad-request", http.StatusBadRequest} InvalidEventId = &AppError{"invalid-event-id", http.StatusNotFound} + InvalidToken = &AppError{"invalid-token", http.StatusUnauthorized} + InvalidEmail = &AppError{"invalid-email", http.StatusNotFound} + DuplicateEmail = &AppError{"duplicate-email", http.StatusConflict} + Unauthorized = &AppError{"unauthorized", http.StatusUnauthorized} ) diff --git a/cfgldr/cfgldr.go b/cfgldr/cfgldr.go index b24ad64..2b8d087 100644 --- a/cfgldr/cfgldr.go +++ b/cfgldr/cfgldr.go @@ -8,6 +8,7 @@ type Config struct { DatabaseConfig DatabaseConfig AppConfig AppConfig RedisConfig RedisConfig + OAuth2Config OAuth2Config } type DatabaseConfig struct { @@ -25,8 +26,15 @@ type RedisConfig struct { Password string `mapstructure:"PASSWORD"` } +type OAuth2Config struct { + RedirectURL string `mapstructure:"REDIRECT_URL"` + ClientID string `mapstructure:"CLIENT_ID"` + ClientSecret string `mapstructure:"CLIENT_SECRET"` + Scopes []string `mapstructure:"SCOPES"` + Endpoint string `mapstructure:"ENDPOINT"` +} + func LoadConfig() (*Config, error) { - dbCfgLdr := viper.New() dbCfgLdr.SetEnvPrefix("DB") dbCfgLdr.AutomaticEnv() @@ -54,10 +62,20 @@ func LoadConfig() (*Config, error) { return nil, err } + oauth2CfgLdr := viper.New() + oauth2CfgLdr.SetEnvPrefix("OAUTH2") + oauth2CfgLdr.AutomaticEnv() + oauth2CfgLdr.AllowEmptyEnv(false) + oauth2Config := OAuth2Config{} + if err := oauth2CfgLdr.Unmarshal(&oauth2Config); err != nil { + return nil, err + } + return &Config{ DatabaseConfig: dbConfig, AppConfig: appConfig, RedisConfig: redisConfig, + OAuth2Config: oauth2Config, }, nil } diff --git a/cmd/main.go b/cmd/main.go index 1095aa7..46f0b8e 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -9,14 +9,14 @@ import ( ginSwagger "github.com/swaggo/gin-swagger" ) -// @title OPH-66 Backend API -// @version 1.0 -// @description Documentation outlines the specifications and endpoints for the OPH-66 Backend API. -// @Schemes http https -// @securityDefinitions.apikey Bearer -// @in header -// @name Authorization -// @description Type "Bearer" followed by a space and JWT token. +// @title OPH-66 Backend API +// @version 1.0 +// @description Documentation outlines the specifications and endpoints for the OPH-66 Backend API. +// @Schemes http https +// @securityDefinitions.apikey Bearer +// @in header +// @name Authorization +// @description Type "Bearer" followed by a space and JWT token. func main() { container, err := di.Init() if err != nil { @@ -34,10 +34,14 @@ func main() { r.GET("/live", container.FeatureflagHandler.GetLivestreamInfo) r.GET("/events", container.EventHandler.GetAllEvents) r.GET("/events/:eventId", container.EventHandler.GetEventById) + r.POST("/auth/register", container.AuthHandler.Register) + r.GET("/auth/me", container.AuthHandler.GetProfile) + r.GET("/auth/login", container.AuthHandler.GoogleLogin) + r.GET("/auth/callback", container.AuthHandler.GoogleCallback) if container.Config.AppConfig.Env == "development" { r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler)) - } + } if err := r.Run(fmt.Sprintf(":%v", container.Config.AppConfig.Port)); err != nil { container.Logger.Fatal("unable to start server") } diff --git a/di/wire.go b/di/wire.go index 02598f1..8a080f0 100644 --- a/di/wire.go +++ b/di/wire.go @@ -8,6 +8,7 @@ import ( "github.com/isd-sgcu/oph66-backend/cache" "github.com/isd-sgcu/oph66-backend/cfgldr" "github.com/isd-sgcu/oph66-backend/database" + auth "github.com/isd-sgcu/oph66-backend/internal/auth" event "github.com/isd-sgcu/oph66-backend/internal/event" featureflag "github.com/isd-sgcu/oph66-backend/internal/feature_flag" healthcheck "github.com/isd-sgcu/oph66-backend/internal/health_check" @@ -19,15 +20,17 @@ type Container struct { EventHandler event.Handler HcHandler healthcheck.Handler FeatureflagHandler featureflag.Handler + AuthHandler auth.Handler Config *cfgldr.Config Logger *zap.Logger } -func newContainer(eventHandler event.Handler, hcHandler healthcheck.Handler, featureflagHandler featureflag.Handler, config *cfgldr.Config, logger *zap.Logger) Container { +func newContainer(eventHandler event.Handler, hcHandler healthcheck.Handler, featureflagHandler featureflag.Handler, authHandler auth.Handler, config *cfgldr.Config, logger *zap.Logger) Container { return Container{ eventHandler, hcHandler, featureflagHandler, + authHandler, config, logger, } @@ -48,6 +51,9 @@ func Init() (Container, error) { featureflag.NewCache, featureflag.NewService, featureflag.NewRepository, + auth.NewHandler, + auth.NewService, + auth.NewRepository, logger.InitLogger, ) diff --git a/di/wire_gen.go b/di/wire_gen.go index ebf7cf8..bc65a40 100644 --- a/di/wire_gen.go +++ b/di/wire_gen.go @@ -10,6 +10,7 @@ import ( "github.com/isd-sgcu/oph66-backend/cache" "github.com/isd-sgcu/oph66-backend/cfgldr" "github.com/isd-sgcu/oph66-backend/database" + "github.com/isd-sgcu/oph66-backend/internal/auth" "github.com/isd-sgcu/oph66-backend/internal/event" "github.com/isd-sgcu/oph66-backend/internal/feature_flag" "github.com/isd-sgcu/oph66-backend/internal/health_check" @@ -42,7 +43,10 @@ func Init() (Container, error) { featureflagService := featureflag.NewService(featureflagRepository, zapLogger) featureflagCache := featureflag.NewCache(client, zapLogger) featureflagHandler := featureflag.NewHandler(featureflagService, featureflagCache) - container := newContainer(handler, healthcheckHandler, featureflagHandler, config, zapLogger) + authRepository := auth.NewRepository(db) + authService := auth.NewService(authRepository, zapLogger, config) + authHandler := auth.NewHandler(authService, zapLogger) + container := newContainer(handler, healthcheckHandler, featureflagHandler, authHandler, config, zapLogger) return container, nil } @@ -52,15 +56,17 @@ type Container struct { EventHandler event.Handler HcHandler healthcheck.Handler FeatureflagHandler featureflag.Handler + AuthHandler auth.Handler Config *cfgldr.Config Logger *zap.Logger } -func newContainer(eventHandler event.Handler, hcHandler healthcheck.Handler, featureflagHandler featureflag.Handler, config *cfgldr.Config, logger2 *zap.Logger) Container { +func newContainer(eventHandler event.Handler, hcHandler healthcheck.Handler, featureflagHandler featureflag.Handler, authHandler auth.Handler, config *cfgldr.Config, logger2 *zap.Logger) Container { return Container{ eventHandler, hcHandler, featureflagHandler, + authHandler, config, logger2, } } diff --git a/docs/docs.go b/docs/docs.go index dd03c60..e3b1434 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -41,6 +41,157 @@ const docTemplate = `{ } } }, + "/auth/callback": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "After successfully logging in with a @chula account, you'll receive a token. If you attempt to log in using a different domain, Google will not allow the login", + "produces": [ + "application/json" + ], + "tags": [ + "auth" + ], + "summary": "receive a token after successfully login with Google", + "operationId": "GoogleCallback", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/auth.CallbackResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/auth.CallbackInvalidResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/auth.CallbackErrorResponse" + } + } + } + } + }, + "/auth/login": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "Redirect to Google login page", + "produces": [ + "application/json" + ], + "tags": [ + "auth" + ], + "summary": "Redirect to Google login page", + "operationId": "GoogleLogin", + "responses": {} + } + }, + "/auth/me": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "Get Profile of current user", + "produces": [ + "application/json" + ], + "tags": [ + "auth" + ], + "summary": "Get Profile of current user", + "operationId": "GetProfile", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/auth.GetProfileResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/auth.GetProfileUnauthorized" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/auth.GetProfileErrorResponse" + } + } + } + } + }, + "/auth/register": { + "post": { + "description": "Register new account with @chula email", + "produces": [ + "application/json" + ], + "tags": [ + "auth" + ], + "summary": "Register", + "operationId": "Register", + "parameters": [ + { + "description": "User", + "name": "user", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/auth.MockUser" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/auth.RegisterResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/auth.RegisterUnauthorized" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/auth.RegisterInvalidResponse" + } + }, + "498": { + "description": "", + "schema": { + "$ref": "#/definitions/auth.RegisterInvalidToken" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/auth.RegisterErrorResponse" + } + } + } + } + }, "/events": { "get": { "security": [ @@ -70,13 +221,13 @@ const docTemplate = `{ "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/event.eventInvalidResponse" + "$ref": "#/definitions/event.EventInvalidResponse" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/event.eventAllErrorResponse" + "$ref": "#/definitions/event.EventAllErrorResponse" } } } @@ -117,13 +268,13 @@ const docTemplate = `{ "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/event.eventInvalidResponse" + "$ref": "#/definitions/event.EventInvalidResponse" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/event.eventErrorResponse" + "$ref": "#/definitions/event.EventErrorResponse" } } } @@ -141,7 +292,7 @@ const docTemplate = `{ "application/json" ], "tags": [ - "LiveStream" + "FeatureFlag" ], "summary": "Get livestream flag", "operationId": "GetLivestreamInfo", @@ -169,6 +320,230 @@ const docTemplate = `{ } }, "definitions": { + "auth.CallbackErrorResponse": { + "type": "object", + "properties": { + "instance": { + "type": "string", + "example": "/auth/callback" + }, + "title": { + "type": "string", + "example": "internal-server-error" + } + } + }, + "auth.CallbackInvalidResponse": { + "type": "object", + "properties": { + "instance": { + "type": "string", + "example": "/auth/callback" + }, + "title": { + "type": "string", + "example": "bad-request" + } + } + }, + "auth.CallbackResponse": { + "type": "object", + "properties": { + "token": { + "type": "string", + "example": "gbxnZjiHVzb_4mDQTQNiJdrZFOCactWXkZvZOxS2_qZsy7vAQY7uA2RFIHe2JABoEjhT0Y3KlOJuOEvE2YJMLrJDagwhpAITGex" + } + } + }, + "auth.GetProfileErrorResponse": { + "type": "object", + "properties": { + "instance": { + "type": "string", + "example": "/auth/me" + }, + "title": { + "type": "string", + "example": "internal-server-error" + } + } + }, + "auth.GetProfileResponse": { + "type": "object", + "properties": { + "user": { + "$ref": "#/definitions/auth.MockUser" + } + } + }, + "auth.GetProfileUnauthorized": { + "type": "object", + "properties": { + "instance": { + "type": "string", + "example": "/auth/me" + }, + "title": { + "type": "string", + "example": "unauthorized" + } + } + }, + "auth.MockDesiredRound": { + "type": "object", + "properties": { + "code": { + "type": "string", + "example": "1" + }, + "order": { + "type": "integer", + "example": 1 + } + } + }, + "auth.MockInterestedFaculty": { + "type": "object", + "properties": { + "code": { + "type": "string", + "example": "1" + }, + "order": { + "type": "integer", + "example": 1 + } + } + }, + "auth.MockUser": { + "type": "object", + "properties": { + "address": { + "type": "string", + "example": "Bangkok" + }, + "allergy": { + "type": "string", + "example": "None" + }, + "birth_date": { + "type": "string", + "example": "1990-01-01" + }, + "desired_rounds": { + "type": "array", + "items": { + "$ref": "#/definitions/auth.MockDesiredRound" + } + }, + "first_name": { + "type": "string", + "example": "John" + }, + "from_abroad": { + "type": "string", + "example": "no" + }, + "gender": { + "type": "string", + "example": "male" + }, + "grade": { + "type": "string", + "example": "undergraduate" + }, + "interested_faculties": { + "type": "array", + "items": { + "$ref": "#/definitions/auth.MockInterestedFaculty" + } + }, + "join_cu_reason": { + "type": "string", + "example": "Interested in the programs offered" + }, + "last_name": { + "type": "string", + "example": "Doe" + }, + "medical_condition": { + "type": "string", + "example": "None" + }, + "news_source": { + "type": "string", + "example": "Facebook" + }, + "school": { + "type": "string", + "example": "CU" + }, + "status": { + "type": "string", + "example": "student" + } + } + }, + "auth.RegisterErrorResponse": { + "type": "object", + "properties": { + "instance": { + "type": "string", + "example": "/auth/register" + }, + "title": { + "type": "string", + "example": "internal-server-error" + } + } + }, + "auth.RegisterInvalidResponse": { + "type": "object", + "properties": { + "instance": { + "type": "string", + "example": "/auth/register" + }, + "title": { + "type": "string", + "example": "bad-request" + } + } + }, + "auth.RegisterInvalidToken": { + "type": "object", + "properties": { + "instance": { + "type": "string", + "example": "/auth/register" + }, + "title": { + "type": "string", + "example": "invalid-token" + } + } + }, + "auth.RegisterResponse": { + "type": "object", + "properties": { + "user": { + "$ref": "#/definitions/auth.MockUser" + } + } + }, + "auth.RegisterUnauthorized": { + "type": "object", + "properties": { + "instance": { + "type": "string", + "example": "/auth/register" + }, + "title": { + "type": "string", + "example": "unauthorized" + } + } + }, "bilingual_field.Bilingual": { "type": "object", "properties": { @@ -241,6 +616,19 @@ const docTemplate = `{ } } }, + "event.EventAllErrorResponse": { + "type": "object", + "properties": { + "instance": { + "type": "string", + "example": "/events" + }, + "title": { + "type": "string", + "example": "internal-server-error" + } + } + }, "event.EventDTO": { "type": "object", "properties": { @@ -279,6 +667,32 @@ const docTemplate = `{ } } }, + "event.EventErrorResponse": { + "type": "object", + "properties": { + "instance": { + "type": "string", + "example": "/events/:eventId" + }, + "title": { + "type": "string", + "example": "internal-server-error" + } + } + }, + "event.EventInvalidResponse": { + "type": "object", + "properties": { + "instance": { + "type": "string", + "example": "/events/:eventId" + }, + "title": { + "type": "string", + "example": "invalid-event-id" + } + } + }, "event.Faculty": { "type": "object", "properties": { @@ -343,45 +757,6 @@ const docTemplate = `{ } } }, - "event.eventAllErrorResponse": { - "type": "object", - "properties": { - "instance": { - "type": "string", - "example": "/events" - }, - "title": { - "type": "string", - "example": "internal-server-error" - } - } - }, - "event.eventErrorResponse": { - "type": "object", - "properties": { - "instance": { - "type": "string", - "example": "/events/:eventId" - }, - "title": { - "type": "string", - "example": "internal-server-error" - } - } - }, - "event.eventInvalidResponse": { - "type": "object", - "properties": { - "instance": { - "type": "string", - "example": "/events/:eventId" - }, - "title": { - "type": "string", - "example": "invalid-event-id" - } - } - }, "faculty.Faculty": { "type": "object", "properties": { diff --git a/docs/swagger.json b/docs/swagger.json index d89f8aa..225afb0 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -37,6 +37,157 @@ } } }, + "/auth/callback": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "After successfully logging in with a @chula account, you'll receive a token. If you attempt to log in using a different domain, Google will not allow the login", + "produces": [ + "application/json" + ], + "tags": [ + "auth" + ], + "summary": "receive a token after successfully login with Google", + "operationId": "GoogleCallback", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/auth.CallbackResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/auth.CallbackInvalidResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/auth.CallbackErrorResponse" + } + } + } + } + }, + "/auth/login": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "Redirect to Google login page", + "produces": [ + "application/json" + ], + "tags": [ + "auth" + ], + "summary": "Redirect to Google login page", + "operationId": "GoogleLogin", + "responses": {} + } + }, + "/auth/me": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "Get Profile of current user", + "produces": [ + "application/json" + ], + "tags": [ + "auth" + ], + "summary": "Get Profile of current user", + "operationId": "GetProfile", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/auth.GetProfileResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/auth.GetProfileUnauthorized" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/auth.GetProfileErrorResponse" + } + } + } + } + }, + "/auth/register": { + "post": { + "description": "Register new account with @chula email", + "produces": [ + "application/json" + ], + "tags": [ + "auth" + ], + "summary": "Register", + "operationId": "Register", + "parameters": [ + { + "description": "User", + "name": "user", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/auth.MockUser" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/auth.RegisterResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/auth.RegisterUnauthorized" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/auth.RegisterInvalidResponse" + } + }, + "498": { + "description": "", + "schema": { + "$ref": "#/definitions/auth.RegisterInvalidToken" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/auth.RegisterErrorResponse" + } + } + } + } + }, "/events": { "get": { "security": [ @@ -66,13 +217,13 @@ "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/event.eventInvalidResponse" + "$ref": "#/definitions/event.EventInvalidResponse" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/event.eventAllErrorResponse" + "$ref": "#/definitions/event.EventAllErrorResponse" } } } @@ -113,13 +264,13 @@ "404": { "description": "Not Found", "schema": { - "$ref": "#/definitions/event.eventInvalidResponse" + "$ref": "#/definitions/event.EventInvalidResponse" } }, "500": { "description": "Internal Server Error", "schema": { - "$ref": "#/definitions/event.eventErrorResponse" + "$ref": "#/definitions/event.EventErrorResponse" } } } @@ -137,7 +288,7 @@ "application/json" ], "tags": [ - "LiveStream" + "FeatureFlag" ], "summary": "Get livestream flag", "operationId": "GetLivestreamInfo", @@ -165,6 +316,230 @@ } }, "definitions": { + "auth.CallbackErrorResponse": { + "type": "object", + "properties": { + "instance": { + "type": "string", + "example": "/auth/callback" + }, + "title": { + "type": "string", + "example": "internal-server-error" + } + } + }, + "auth.CallbackInvalidResponse": { + "type": "object", + "properties": { + "instance": { + "type": "string", + "example": "/auth/callback" + }, + "title": { + "type": "string", + "example": "bad-request" + } + } + }, + "auth.CallbackResponse": { + "type": "object", + "properties": { + "token": { + "type": "string", + "example": "gbxnZjiHVzb_4mDQTQNiJdrZFOCactWXkZvZOxS2_qZsy7vAQY7uA2RFIHe2JABoEjhT0Y3KlOJuOEvE2YJMLrJDagwhpAITGex" + } + } + }, + "auth.GetProfileErrorResponse": { + "type": "object", + "properties": { + "instance": { + "type": "string", + "example": "/auth/me" + }, + "title": { + "type": "string", + "example": "internal-server-error" + } + } + }, + "auth.GetProfileResponse": { + "type": "object", + "properties": { + "user": { + "$ref": "#/definitions/auth.MockUser" + } + } + }, + "auth.GetProfileUnauthorized": { + "type": "object", + "properties": { + "instance": { + "type": "string", + "example": "/auth/me" + }, + "title": { + "type": "string", + "example": "unauthorized" + } + } + }, + "auth.MockDesiredRound": { + "type": "object", + "properties": { + "code": { + "type": "string", + "example": "1" + }, + "order": { + "type": "integer", + "example": 1 + } + } + }, + "auth.MockInterestedFaculty": { + "type": "object", + "properties": { + "code": { + "type": "string", + "example": "1" + }, + "order": { + "type": "integer", + "example": 1 + } + } + }, + "auth.MockUser": { + "type": "object", + "properties": { + "address": { + "type": "string", + "example": "Bangkok" + }, + "allergy": { + "type": "string", + "example": "None" + }, + "birth_date": { + "type": "string", + "example": "1990-01-01" + }, + "desired_rounds": { + "type": "array", + "items": { + "$ref": "#/definitions/auth.MockDesiredRound" + } + }, + "first_name": { + "type": "string", + "example": "John" + }, + "from_abroad": { + "type": "string", + "example": "no" + }, + "gender": { + "type": "string", + "example": "male" + }, + "grade": { + "type": "string", + "example": "undergraduate" + }, + "interested_faculties": { + "type": "array", + "items": { + "$ref": "#/definitions/auth.MockInterestedFaculty" + } + }, + "join_cu_reason": { + "type": "string", + "example": "Interested in the programs offered" + }, + "last_name": { + "type": "string", + "example": "Doe" + }, + "medical_condition": { + "type": "string", + "example": "None" + }, + "news_source": { + "type": "string", + "example": "Facebook" + }, + "school": { + "type": "string", + "example": "CU" + }, + "status": { + "type": "string", + "example": "student" + } + } + }, + "auth.RegisterErrorResponse": { + "type": "object", + "properties": { + "instance": { + "type": "string", + "example": "/auth/register" + }, + "title": { + "type": "string", + "example": "internal-server-error" + } + } + }, + "auth.RegisterInvalidResponse": { + "type": "object", + "properties": { + "instance": { + "type": "string", + "example": "/auth/register" + }, + "title": { + "type": "string", + "example": "bad-request" + } + } + }, + "auth.RegisterInvalidToken": { + "type": "object", + "properties": { + "instance": { + "type": "string", + "example": "/auth/register" + }, + "title": { + "type": "string", + "example": "invalid-token" + } + } + }, + "auth.RegisterResponse": { + "type": "object", + "properties": { + "user": { + "$ref": "#/definitions/auth.MockUser" + } + } + }, + "auth.RegisterUnauthorized": { + "type": "object", + "properties": { + "instance": { + "type": "string", + "example": "/auth/register" + }, + "title": { + "type": "string", + "example": "unauthorized" + } + } + }, "bilingual_field.Bilingual": { "type": "object", "properties": { @@ -237,6 +612,19 @@ } } }, + "event.EventAllErrorResponse": { + "type": "object", + "properties": { + "instance": { + "type": "string", + "example": "/events" + }, + "title": { + "type": "string", + "example": "internal-server-error" + } + } + }, "event.EventDTO": { "type": "object", "properties": { @@ -275,6 +663,32 @@ } } }, + "event.EventErrorResponse": { + "type": "object", + "properties": { + "instance": { + "type": "string", + "example": "/events/:eventId" + }, + "title": { + "type": "string", + "example": "internal-server-error" + } + } + }, + "event.EventInvalidResponse": { + "type": "object", + "properties": { + "instance": { + "type": "string", + "example": "/events/:eventId" + }, + "title": { + "type": "string", + "example": "invalid-event-id" + } + } + }, "event.Faculty": { "type": "object", "properties": { @@ -339,45 +753,6 @@ } } }, - "event.eventAllErrorResponse": { - "type": "object", - "properties": { - "instance": { - "type": "string", - "example": "/events" - }, - "title": { - "type": "string", - "example": "internal-server-error" - } - } - }, - "event.eventErrorResponse": { - "type": "object", - "properties": { - "instance": { - "type": "string", - "example": "/events/:eventId" - }, - "title": { - "type": "string", - "example": "internal-server-error" - } - } - }, - "event.eventInvalidResponse": { - "type": "object", - "properties": { - "instance": { - "type": "string", - "example": "/events/:eventId" - }, - "title": { - "type": "string", - "example": "invalid-event-id" - } - } - }, "faculty.Faculty": { "type": "object", "properties": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index fce620e..4fcc3a0 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -1,4 +1,160 @@ definitions: + auth.CallbackErrorResponse: + properties: + instance: + example: /auth/callback + type: string + title: + example: internal-server-error + type: string + type: object + auth.CallbackInvalidResponse: + properties: + instance: + example: /auth/callback + type: string + title: + example: bad-request + type: string + type: object + auth.CallbackResponse: + properties: + token: + example: gbxnZjiHVzb_4mDQTQNiJdrZFOCactWXkZvZOxS2_qZsy7vAQY7uA2RFIHe2JABoEjhT0Y3KlOJuOEvE2YJMLrJDagwhpAITGex + type: string + type: object + auth.GetProfileErrorResponse: + properties: + instance: + example: /auth/me + type: string + title: + example: internal-server-error + type: string + type: object + auth.GetProfileResponse: + properties: + user: + $ref: '#/definitions/auth.MockUser' + type: object + auth.GetProfileUnauthorized: + properties: + instance: + example: /auth/me + type: string + title: + example: unauthorized + type: string + type: object + auth.MockDesiredRound: + properties: + code: + example: "1" + type: string + order: + example: 1 + type: integer + type: object + auth.MockInterestedFaculty: + properties: + code: + example: "1" + type: string + order: + example: 1 + type: integer + type: object + auth.MockUser: + properties: + address: + example: Bangkok + type: string + allergy: + example: None + type: string + birth_date: + example: "1990-01-01" + type: string + desired_rounds: + items: + $ref: '#/definitions/auth.MockDesiredRound' + type: array + first_name: + example: John + type: string + from_abroad: + example: "no" + type: string + gender: + example: male + type: string + grade: + example: undergraduate + type: string + interested_faculties: + items: + $ref: '#/definitions/auth.MockInterestedFaculty' + type: array + join_cu_reason: + example: Interested in the programs offered + type: string + last_name: + example: Doe + type: string + medical_condition: + example: None + type: string + news_source: + example: Facebook + type: string + school: + example: CU + type: string + status: + example: student + type: string + type: object + auth.RegisterErrorResponse: + properties: + instance: + example: /auth/register + type: string + title: + example: internal-server-error + type: string + type: object + auth.RegisterInvalidResponse: + properties: + instance: + example: /auth/register + type: string + title: + example: bad-request + type: string + type: object + auth.RegisterInvalidToken: + properties: + instance: + example: /auth/register + type: string + title: + example: invalid-token + type: string + type: object + auth.RegisterResponse: + properties: + user: + $ref: '#/definitions/auth.MockUser' + type: object + auth.RegisterUnauthorized: + properties: + instance: + example: /auth/register + type: string + title: + example: unauthorized + type: string + type: object bilingual_field.Bilingual: properties: en: @@ -47,6 +203,15 @@ definitions: $ref: '#/definitions/schedule.Schedule' type: array type: object + event.EventAllErrorResponse: + properties: + instance: + example: /events + type: string + title: + example: internal-server-error + type: string + type: object event.EventDTO: properties: department: @@ -73,6 +238,24 @@ definitions: $ref: '#/definitions/event.Schedule' type: array type: object + event.EventErrorResponse: + properties: + instance: + example: /events/:eventId + type: string + title: + example: internal-server-error + type: string + type: object + event.EventInvalidResponse: + properties: + instance: + example: /events/:eventId + type: string + title: + example: invalid-event-id + type: string + type: object event.Faculty: properties: code: @@ -117,33 +300,6 @@ definitions: example: "2021-08-01T00:00:00+07:00" type: string type: object - event.eventAllErrorResponse: - properties: - instance: - example: /events - type: string - title: - example: internal-server-error - type: string - type: object - event.eventErrorResponse: - properties: - instance: - example: /events/:eventId - type: string - title: - example: internal-server-error - type: string - type: object - event.eventInvalidResponse: - properties: - instance: - example: /events/:eventId - type: string - title: - example: invalid-event-id - type: string - type: object faculty.Faculty: properties: code: @@ -211,6 +367,105 @@ paths: summary: Health Check tags: - healthcheck + /auth/callback: + get: + description: After successfully logging in with a @chula account, you'll receive + a token. If you attempt to log in using a different domain, Google will not + allow the login + operationId: GoogleCallback + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/auth.CallbackResponse' + "404": + description: Not Found + schema: + $ref: '#/definitions/auth.CallbackInvalidResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/auth.CallbackErrorResponse' + security: + - Bearer: [] + summary: receive a token after successfully login with Google + tags: + - auth + /auth/login: + get: + description: Redirect to Google login page + operationId: GoogleLogin + produces: + - application/json + responses: {} + security: + - Bearer: [] + summary: Redirect to Google login page + tags: + - auth + /auth/me: + get: + description: Get Profile of current user + operationId: GetProfile + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/auth.GetProfileResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/auth.GetProfileUnauthorized' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/auth.GetProfileErrorResponse' + security: + - Bearer: [] + summary: Get Profile of current user + tags: + - auth + /auth/register: + post: + description: Register new account with @chula email + operationId: Register + parameters: + - description: User + in: body + name: user + required: true + schema: + $ref: '#/definitions/auth.MockUser' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/auth.RegisterResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/auth.RegisterUnauthorized' + "404": + description: Not Found + schema: + $ref: '#/definitions/auth.RegisterInvalidResponse' + "498": + description: "" + schema: + $ref: '#/definitions/auth.RegisterInvalidToken' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/auth.RegisterErrorResponse' + summary: Register + tags: + - auth /events: get: description: Get all events as array of events @@ -227,11 +482,11 @@ paths: "404": description: Not Found schema: - $ref: '#/definitions/event.eventInvalidResponse' + $ref: '#/definitions/event.EventInvalidResponse' "500": description: Internal Server Error schema: - $ref: '#/definitions/event.eventAllErrorResponse' + $ref: '#/definitions/event.EventAllErrorResponse' security: - Bearer: [] summary: Get all events @@ -257,11 +512,11 @@ paths: "404": description: Not Found schema: - $ref: '#/definitions/event.eventInvalidResponse' + $ref: '#/definitions/event.EventInvalidResponse' "500": description: Internal Server Error schema: - $ref: '#/definitions/event.eventErrorResponse' + $ref: '#/definitions/event.EventErrorResponse' security: - Bearer: [] summary: get event by id @@ -290,7 +545,7 @@ paths: - Bearer: [] summary: Get livestream flag tags: - - LiveStream + - FeatureFlag schemes: - http - https diff --git a/go.mod b/go.mod index 67e3151..f0731c7 100644 --- a/go.mod +++ b/go.mod @@ -7,20 +7,24 @@ require ( github.com/google/wire v0.5.0 github.com/redis/go-redis/v9 v9.3.0 github.com/spf13/viper v1.18.1 + github.com/swaggo/files v1.0.1 + github.com/swaggo/gin-swagger v1.6.0 + github.com/swaggo/swag v1.16.2 go.uber.org/zap v1.26.0 + golang.org/x/oauth2 v0.15.0 + google.golang.org/api v0.153.0 gorm.io/driver/postgres v1.5.4 gorm.io/gorm v1.25.5 ) require ( + cloud.google.com/go/compute v1.23.3 // indirect + cloud.google.com/go/compute/metadata v0.2.3 // 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.10.2 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect github.com/chenzhuoyu/iasm v0.9.1 // indirect - github.com/cpuguy83/go-md2man/v2 v2.0.3 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/gabriel-vasile/mimetype v1.4.3 // indirect @@ -33,13 +37,16 @@ require ( github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/validator/v10 v10.16.0 // indirect github.com/goccy/go-json v0.10.2 // indirect + github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/golang/protobuf v1.5.3 // indirect + github.com/google/s2a-go v0.1.7 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect github.com/jackc/pgx/v5 v5.4.3 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect - github.com/joho/godotenv v1.5.1 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/cpuid/v2 v2.2.6 // indirect @@ -51,22 +58,16 @@ 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.1.1 // indirect - github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/sagikazarmark/locafero v0.4.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect - github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect github.com/sourcegraph/conc v0.3.0 // indirect github.com/spf13/afero v1.11.0 // indirect github.com/spf13/cast v1.6.0 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/subosito/gotenv v1.6.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.2 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.2.12 // indirect - github.com/urfave/cli/v2 v2.26.0 // indirect - github.com/xrash/smetrics v0.0.0-20231213231151-1d8dd44e695e // indirect + go.opencensus.io v0.24.0 // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/arch v0.6.0 // indirect golang.org/x/crypto v0.17.0 // indirect @@ -75,9 +76,10 @@ require ( golang.org/x/sys v0.15.0 // indirect golang.org/x/text v0.14.0 // indirect golang.org/x/tools v0.16.1 // indirect + google.golang.org/appengine v1.6.7 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20231120223509-83a465c0220f // indirect + google.golang.org/grpc v1.59.0 // indirect google.golang.org/protobuf v1.32.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect - gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - sigs.k8s.io/yaml v1.4.0 // indirect ) diff --git a/go.sum b/go.sum index bbaa348..54fb0d9 100644 --- a/go.sum +++ b/go.sum @@ -1,9 +1,11 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go/compute v1.23.3 h1:6sVlXXBmbd7jNX0Ipq0trII3e4n1/MsADLK6a+aiVlk= +cloud.google.com/go/compute v1.23.3/go.mod h1:VCgBUoMnIVIR0CscqQiPJLAG25E3ZRZMzcFZeQ+h8CI= +cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY= +cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= +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/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= @@ -12,6 +14,7 @@ github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1 github.com/bytedance/sonic v1.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM= github.com/bytedance/sonic v1.10.2 h1:GQebETVBxYB7JGWJtLBi07OVzWwt+8dWA00gEVW2ZFE= github.com/bytedance/sonic v1.10.2/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= @@ -21,20 +24,26 @@ github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d/go.mod h1:8EPpV github.com/chenzhuoyu/iasm v0.9.0/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog= github.com/chenzhuoyu/iasm v0.9.1 h1:tUHQJXo3NhBqw6s33wkGn9SP3bvrWLdlVIJ3hQBL7P0= github.com/chenzhuoyu/iasm v0.9.1/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog= -github.com/cpuguy83/go-md2man/v2 v2.0.3 h1:qMCsGGgs+MAzDFyp9LpAe1Lqy/fY/qCovCm0qnXZOBM= -github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= +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.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= @@ -57,15 +66,46 @@ github.com/go-playground/validator/v10 v10.16.0 h1:x+plE831WK4vaKHO/jpgUGsvLKIqR github.com/go-playground/validator/v10 v10.16.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o= +github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw= github.com/google/subcommands v1.0.1/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4= +github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/wire v0.5.0 h1:I7ELFeVBr3yfPIcc8+MWvrjk+3VjbcSzoXm3JVa+jD8= github.com/google/wire v0.5.0/go.mod h1:ngWDr9Qvq3yZA10YrxfyGELY/AFWGVpy9c1LTRi1EoU= +github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs= +github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0= +github.com/googleapis/gax-go/v2 v2.12.0 h1:A+gCJKdRfqXkr+BIRGtZLibNXf0m1f9E4HG56etFpas= +github.com/googleapis/gax-go/v2 v2.12.0/go.mod h1:y+aIqrI5eb1YGMVJfuV3185Ts/D7qKpsEkdD5+I6QGU= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= @@ -78,8 +118,6 @@ github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= -github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= -github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= @@ -112,18 +150,15 @@ github.com/pelletier/go-toml/v2 v2.1.1/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdU github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/redis/go-redis/v9 v9.3.0 h1:RiVDjmig62jIWp7Kk4XVLs0hzV6pI3PyTnnL0cnn0u0= github.com/redis/go-redis/v9 v9.3.0/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M= -github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= -github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= -github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= -github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +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/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= -github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= -github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= @@ -157,11 +192,9 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= -github.com/urfave/cli/v2 v2.26.0 h1:3f3AMg3HpThFNT4I++TKOejZO8yU55t3JnnSr4S4QEI= -github.com/urfave/cli/v2 v2.26.0/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ= -github.com/xrash/smetrics v0.0.0-20231213231151-1d8dd44e695e h1:+SOyEddqYF09QP7vr7CgJ1eti3pY9Fn3LHO1M1r/0sI= -github.com/xrash/smetrics v0.0.0-20231213231151-1d8dd44e695e/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= +go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk= go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= @@ -172,24 +205,45 @@ golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUu golang.org/x/arch v0.6.0 h1:S0JTfE48HbRj80+4tbvZDYsJ3tGv6BUU3XxyZ7CirAc= golang.org/x/arch v0.6.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.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY= -golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +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.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= +golang.org/x/mod v0.14.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= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 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.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.15.0 h1:s8pnnxNVzjWyrvYdFUQq5llS1PX2zhPXmccZv99h7uQ= +golang.org/x/oauth2 v0.15.0/go.mod h1:q48ptWNTY5XWf+JNten23lcvHpLJ0ZSxF5ttTHKVCAM= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE= +golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -202,22 +256,55 @@ golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9sn golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= +golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190422233926-fe54fb35175b/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.16.1 h1:TLyB3WofjdOEepBHAU20JdNC1Zbg87elYofWYAY5oZA= golang.org/x/tools v0.16.1/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.153.0 h1:N1AwGhielyKFaUqH07/ZSIQR3uNPcV7NVw0vj+j4iR4= +google.golang.org/api v0.153.0/go.mod h1:3qNJX5eOmhiWYc67jRA/3GsDw97UFb5ivv7Y2PrriAY= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto/googleapis/rpc v0.0.0-20231120223509-83a465c0220f h1:ultW7fxlIvee4HYrtnaRPon9HpEgFk5zYpmfMgtKB5I= +google.golang.org/genproto/googleapis/rpc v0.0.0-20231120223509-83a465c0220f/go.mod h1:L9KNLi232K1/xB6f7AlSX692koaRnKaWSR0stBki0Yc= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.59.0 h1:Z5Iec2pjwb+LEOqzpB2MR12/eKFhDPhuqW91O+4bwUk= +google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= -google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I= google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -225,8 +312,6 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntN gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -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= @@ -234,7 +319,7 @@ gorm.io/driver/postgres v1.5.4 h1:Iyrp9Meh3GmbSuyIAGyjkN+n9K+GHX9b9MqsTL4EJCo= gorm.io/driver/postgres v1.5.4/go.mod h1:Bgo89+h0CRcdA33Y6frlaHHVuTdOf87pmyzwW9C/BH0= gorm.io/gorm v1.25.5 h1:zR9lOiiYf09VNh5Q1gphfyia1JpiClIWG9hQaxB/mls= gorm.io/gorm v1.25.5/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= -sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= -sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/internal/auth/auth.dto.go b/internal/auth/auth.dto.go new file mode 100644 index 0000000..5355939 --- /dev/null +++ b/internal/auth/auth.dto.go @@ -0,0 +1,99 @@ +package auth + +type RegisterDTO struct { + Gender string `json:"gender"` + FirstName string `json:"first_name"` + LastName string `json:"last_name"` + School string `json:"school"` + BirthDate string `json:"birth_date"` + Address string `json:"address"` + FromAbroad string `json:"from_abroad"` + Allergy string `json:"allergy"` + MedicalCondition string `json:"medical_condition"` + JoinCUReason string `json:"join_cu_reason"` + NewsSource string `json:"news_source"` + Status string `json:"status"` + Grade string `json:"grade"` + DesiredRounds []DesiredRound `json:"desired_rounds"` + InterestedFaculties []InterestedFaculty `json:"interested_faculties"` +} + +type MockUser struct { + Gender string `json:"gender" example:"male"` + FirstName string `json:"first_name" example:"John"` + LastName string `json:"last_name" example:"Doe"` + School string `json:"school" example:"CU"` + BirthDate string `json:"birth_date" example:"1990-01-01"` + Address string `json:"address" example:"Bangkok"` + FromAbroad string `json:"from_abroad" example:"no"` + Allergy string `json:"allergy" example:"None"` + MedicalCondition string `json:"medical_condition" example:"None"` + JoinCUReason string `json:"join_cu_reason" example:"Interested in the programs offered"` + NewsSource string `json:"news_source" example:"Facebook"` + Status string `json:"status" example:"student"` + Grade string `json:"grade" example:"undergraduate"` + DesiredRounds []MockDesiredRound `json:"desired_rounds"` + InterestedFaculties []MockInterestedFaculty `json:"interested_faculties"` +} + +type MockDesiredRound struct { + Order uint `json:"order" example:"1"` + Code string `json:"code" example:"1"` +} + +type MockInterestedFaculty struct { + Order uint `json:"order" example:"1"` + Code string `json:"code" example:"1"` +} + +type CallbackResponse struct { + Token string `json:"token" example:"gbxnZjiHVzb_4mDQTQNiJdrZFOCactWXkZvZOxS2_qZsy7vAQY7uA2RFIHe2JABoEjhT0Y3KlOJuOEvE2YJMLrJDagwhpAITGex"` +} + +type CallbackErrorResponse struct { + Instance string `json:"instance" example:"/auth/callback"` + Title string `json:"title" example:"internal-server-error"` +} + +type CallbackInvalidResponse struct { + Instance string `json:"instance" example:"/auth/callback"` + Title string `json:"title" example:"bad-request"` +} + +type RegisterResponse struct { + MockUser MockUser `json:"user"` +} + +type RegisterErrorResponse struct { + Instance string `json:"instance" example:"/auth/register"` + Title string `json:"title" example:"internal-server-error"` +} + +type RegisterInvalidResponse struct { + Instance string `json:"instance" example:"/auth/register"` + Title string `json:"title" example:"bad-request"` +} + +type RegisterUnauthorized struct { + Instance string `json:"instance" example:"/auth/register"` + Title string `json:"title" example:"unauthorized"` +} + +type RegisterInvalidToken struct { + Instance string `json:"instance" example:"/auth/register"` + Title string `json:"title" example:"invalid-token"` +} + +type GetProfileResponse struct { + MockUser MockUser `json:"user"` +} + +type GetProfileErrorResponse struct { + Instance string `json:"instance" example:"/auth/me"` + Title string `json:"title" example:"internal-server-error"` +} + +type GetProfileUnauthorized struct { + Instance string `json:"instance" example:"/auth/me"` + Title string `json:"title" example:"unauthorized"` +} \ No newline at end of file diff --git a/internal/auth/auth.handler.go b/internal/auth/auth.handler.go new file mode 100644 index 0000000..37c62a4 --- /dev/null +++ b/internal/auth/auth.handler.go @@ -0,0 +1,125 @@ +package auth + +import ( + "net/http" + + "github.com/gin-gonic/gin" + "github.com/isd-sgcu/oph66-backend/apperror" + "github.com/isd-sgcu/oph66-backend/utils" + "go.uber.org/zap" +) + +type Handler interface { + GoogleLogin(c *gin.Context) + GoogleCallback(c *gin.Context) + Register(c *gin.Context) + GetProfile(c *gin.Context) +} + +type handlerImpl struct { + svc Service + logger *zap.Logger +} + +func NewHandler(svc Service, logger *zap.Logger) Handler { + return &handlerImpl{ + svc, + logger, + } +} + +// GoogleLogin godoc +// @summary Redirect to Google login page +// @description Redirect to Google login page +// @id GoogleLogin +// @produce json +// @tags auth +// @Security Bearer +// @router /auth/login [get] +func (h *handlerImpl) GoogleLogin(c *gin.Context) { + url := h.svc.GoogleLogin() + c.Redirect(http.StatusTemporaryRedirect, url) +} + +// GoogleCallback godoc +// @summary receive a token after successfully login with Google +// @description After successfully logging in with a @chula account, you'll receive a token. If you attempt to log in using a different domain, Google will not allow the login +// @id GoogleCallback +// @produce json +// @tags auth +// @Security Bearer +// @router /auth/callback [get] +// @success 200 {object} auth.CallbackResponse +// @Failure 500 {object} auth.CallbackErrorResponse +// @Failure 404 {object} auth.CallbackInvalidResponse +func (h *handlerImpl) GoogleCallback(c *gin.Context) { + code := c.Query("code") + token, apperr := h.svc.GoogleCallback(c, code) + if apperr != nil { + utils.ReturnError(c, apperr) + return + } + response := CallbackResponse{ + Token: token, + } + c.JSON(http.StatusOK, response) +} + +// Register godoc +// @summary Register +// @description Register new account with @chula email +// @id Register +// @produce json +// @tags auth +// @Security Bearer +// @router /auth/register [post] +// @param user body auth.MockUser true "User" +// @success 200 {object} auth.RegisterResponse +// @Failure 500 {object} auth.RegisterErrorResponse +// @Failure 404 {object} auth.RegisterInvalidResponse +// @Failure 401 {object} auth.RegisterUnauthorized +// @Failure 498 {object} auth.RegisterInvalidToken +func (h *handlerImpl) Register(c *gin.Context) { + var data RegisterDTO + authHeader := c.GetHeader("Authorization") + if authHeader == "" { + utils.ReturnError(c, apperror.Unauthorized) + } + + if err := c.ShouldBindJSON(&data); err != nil { + utils.ReturnError(c, apperror.BadRequest) + return + } + + user, apperr := h.svc.Register(c, &data, authHeader) + if apperr != nil { + utils.ReturnError(c, apperror.InternalError) + } + + c.JSON(http.StatusOK, user) +} + +// GetProfile godoc +// @summary Get Profile of current user +// @description Get Profile of current user +// @id GetProfile +// @produce json +// @tags auth +// @Security Bearer +// @router /auth/me [get] +// @success 200 {object} auth.GetProfileResponse +// @Failure 500 {object} auth.GetProfileErrorResponse +// @Failure 401 {object} auth.GetProfileUnauthorized +func (h *handlerImpl) GetProfile(c *gin.Context) { + authHeader := c.GetHeader("Authorization") + if authHeader == "" { + utils.ReturnError(c, apperror.Unauthorized) + } + + user, apperr := h.svc.GetUserFromJWTToken(c, authHeader) + if apperr != nil { + utils.ReturnError(c, apperr) + } + + c.JSON(http.StatusOK, gin.H{"user": user, "id": user.ID}) +} \ No newline at end of file diff --git a/internal/auth/auth.model.go b/internal/auth/auth.model.go new file mode 100644 index 0000000..5cfa421 --- /dev/null +++ b/internal/auth/auth.model.go @@ -0,0 +1,49 @@ +package auth + +type User struct { + ID uint `gorm:"primaryKey;autoIncrement" json:"id"` + Gender string `json:"gender"` + FirstName string `json:"first_name"` + LastName string `json:"last_name"` + Email string `json:"email"` + School string `json:"school"` + BirthDate string `json:"birth_date"` + Address string `json:"address"` + FromAbroad string `json:"from_abroad"` + Allergy string `json:"allergy"` + MedicalCondition string `json:"medical_condition"` + JoinCUReason string `json:"join_cu_reason"` + NewsSource string `json:"news_source"` + Status string `json:"status"` + Grade string `json:"grade"` + DesiredRounds []DesiredRound `gorm:"foreignKey:UserID"` + InterestedFaculties []InterestedFaculty `gorm:"foreignKey:UserID"` +} + +type InterestedFaculty struct { + ID uint `gorm:"primaryKey;autoIncrement" json:"id"` + Order uint `json:"order"` + Faculty string `json:"faculty"` + Department string `json:"department"` + Section string `json:"section"` + UserID uint `gorm:"index"` +} + +type DesiredRound struct { + ID uint `gorm:"primaryKey;autoIncrement" json:"id"` + Order uint `json:"order"` + Round string `json:"round"` + UserID uint `gorm:"index"` +} + +func (u *User) TableName() string { + return "users" +} + +func (d *DesiredRound) TableName() string { + return "desired_round" +} + +func (i *InterestedFaculty) TableName() string { + return "interested_faculty" +} \ No newline at end of file diff --git a/internal/auth/auth.repository.go b/internal/auth/auth.repository.go new file mode 100644 index 0000000..986c642 --- /dev/null +++ b/internal/auth/auth.repository.go @@ -0,0 +1,53 @@ +package auth + +import ( + "gorm.io/gorm" +) + +type Repository interface { + CreateUser(user *User) (*User, error) + UpdateUser(id uint, user *User) (*User, error) + GetUserByEmail(user *User, email string) (*User, error) +} + +type repositoryImpl struct { + db *gorm.DB +} + +func NewRepository(db *gorm.DB) Repository { + return &repositoryImpl{ + db, + } +} + +func (r *repositoryImpl) CreateUser(user *User) (*User, error) { + if err := r.db.Create(&user).Error; err != nil { + return nil, err + } + return user, nil +} + +func (r *repositoryImpl) UpdateUser(id uint, user *User) (*User, error) { + user.ID = id + if err := r.db.Model(&user).Association("DesiredRounds").Replace(&user.DesiredRounds); err != nil { + return nil, err + } + + if err := r.db.Model(&user).Association("InterestedFaculties").Replace(&user.InterestedFaculties); err != nil { + return nil, err + } + + if err := r.db.Model(&user).Where("id = ?", id).Updates(&user).Error; err != nil { + return nil, err + } + + return user, nil +} + +func (r *repositoryImpl) GetUserByEmail(user *User, email string) (*User, error) { + if err := r.db.Where("email = ?", email).Preload("DesiredRounds").Preload("InterestedFaculties").First(&user).Error; err != nil { + return nil, err + } + + return user, nil +} \ No newline at end of file diff --git a/internal/auth/auth.service.go b/internal/auth/auth.service.go new file mode 100644 index 0000000..d60fb4a --- /dev/null +++ b/internal/auth/auth.service.go @@ -0,0 +1,149 @@ +package auth + +import ( + "context" + "strings" + + "github.com/isd-sgcu/oph66-backend/apperror" + "github.com/isd-sgcu/oph66-backend/cfgldr" + "go.uber.org/zap" + "golang.org/x/oauth2" + "golang.org/x/oauth2/google" + "google.golang.org/api/idtoken" +) + +type Service interface { + GoogleLogin() (url string) + GoogleCallback(ctx context.Context, code string) (idToken string, appErr *apperror.AppError) + Register(ctx context.Context, data *RegisterDTO, tokenString string) (user *User, appErr *apperror.AppError) + GetUserFromJWTToken(ctx context.Context, tokenString string) (user *User, appErr *apperror.AppError) +} + +func NewService(repo Repository, logger *zap.Logger, cfg *cfgldr.Config) Service { + oauthConfig := &oauth2.Config{ + ClientID: cfg.OAuth2Config.ClientID, + ClientSecret: cfg.OAuth2Config.ClientSecret, + RedirectURL: cfg.OAuth2Config.RedirectURL, + Scopes: cfg.OAuth2Config.Scopes, + Endpoint: google.Endpoint, + } + return &serviceImpl{ + repo, + cfg, + logger, + oauthConfig, + } +} + +type serviceImpl struct { + repo Repository + cfg *cfgldr.Config + logger *zap.Logger + oauthConfig *oauth2.Config +} + +func (s *serviceImpl) GoogleLogin() (url string) { + url = s.oauthConfig.AuthCodeURL("state") + return url +} + +func (s *serviceImpl) GoogleCallback(ctx context.Context, code string) (idToken string, appErr *apperror.AppError) { + token, err := s.oauthConfig.Exchange(ctx, code) + if err != nil { + s.logger.Error("Failed to exchange code for token", zap.Error(err)) + return "", apperror.InternalError + } + + rawIdToken := token.Extra("id_token") + if rawIdToken == nil { + s.logger.Error("ID token not found") + return "", apperror.InternalError + } + + return rawIdToken.(string), nil +} + +func (s *serviceImpl) Register(ctx context.Context, data *RegisterDTO, tokenString string) (user *User, appErr *apperror.AppError) { + email, appErr := getEmailFromToken(ctx, s.logger, tokenString, s.cfg.OAuth2Config.ClientID) + if appErr != nil { + return nil, appErr + } + + user, err := s.repo.GetUserByEmail(&User{}, email) + + dataUser := ConvertRegisterDTOToUser(data, email) + + if err == nil { + user, err = s.repo.UpdateUser(user.ID, dataUser) + if err != nil { + s.logger.Error("Failed to update user", zap.Error(err)) + return nil, apperror.InternalError + } + } else { + user, err = s.repo.CreateUser(dataUser) + if err != nil { + s.logger.Error("Failed to create user", zap.Error(err)) + return nil, apperror.InternalError + } + } + + return user, nil +} + +func (s *serviceImpl) GetUserFromJWTToken(ctx context.Context, tokenString string) (user *User, appErr *apperror.AppError) { + email, appErr := getEmailFromToken(ctx, s.logger, tokenString, s.cfg.OAuth2Config.ClientID) + if appErr != nil { + return nil, appErr + } + + user, err := s.repo.GetUserByEmail(user, email) + if err != nil { + s.logger.Error("Failed to get user by email", zap.Error(err)) + return nil, apperror.InternalError + } + + return user, nil +} + +func getEmailFromToken(ctx context.Context, logger *zap.Logger, tokenString string, ClientID string) (email string, appErr *apperror.AppError) { + if !strings.HasPrefix(tokenString, "Bearer ") { + return "", apperror.InvalidToken + } + + tokenString = strings.Replace(tokenString, "Bearer ", "", 1) + + token, err := idtoken.Validate(ctx, tokenString, ClientID) + if err != nil { + logger.Error("Failed to validate token", zap.Error(err)) + return "", apperror.InvalidToken + } + + email, ok := token.Claims["email"].(string) + if !ok || email == "" { + logger.Error("Email not found in token claims") + return "", apperror.InvalidToken + } + + return email, nil +} + +func ConvertRegisterDTOToUser(dto *RegisterDTO, email string) *User { + return &User{ + Gender: dto.Gender, + FirstName: dto.FirstName, + LastName: dto.LastName, + Email: email, + School: dto.School, + BirthDate: dto.BirthDate, + Address: dto.Address, + FromAbroad: dto.FromAbroad, + Allergy: dto.Allergy, + MedicalCondition: dto.MedicalCondition, + JoinCUReason: dto.JoinCUReason, + NewsSource: dto.NewsSource, + Status: dto.Status, + Grade: dto.Grade, + DesiredRounds: dto.DesiredRounds, + InterestedFaculties: dto.InterestedFaculties, + } +} \ No newline at end of file diff --git a/internal/event/event.dto.go b/internal/event/event.dto.go index 463d495..82ad372 100644 --- a/internal/event/event.dto.go +++ b/internal/event/event.dto.go @@ -4,30 +4,30 @@ import "time" type EventAll []Event -type eventInvalidResponse struct { +type EventInvalidResponse struct { Instance string `json:"instance" example:"/events/:eventId"` Title string `json:"title" example:"invalid-event-id"` } -type eventErrorResponse struct { +type EventErrorResponse struct { Instance string `json:"instance" example:"/events/:eventId"` Title string `json:"title" example:"internal-server-error"` } -type eventAllErrorResponse struct { +type EventAllErrorResponse struct { Instance string `json:"instance" example:"/events"` Title string `json:"title" example:"internal-server-error"` } type EventDTO struct { Id string `json:"id" example:"first-event"` - Name NameEventBilingual `json:"name" ` - FacultyCode int `json:"-" ` + Name NameEventBilingual `json:"name"` + FacultyCode int `json:"-"` Faculty Faculty `json:"faculty"` - Department DepartmentBilingual `json:"department" ` + Department DepartmentBilingual `json:"department"` RequireRegistration bool `json:"require_registration" example:"true"` MaxCapacity int `json:"max_capacity" example:"100"` - Schedules []Schedule `json:"schedules" ` + Schedules []Schedule `json:"schedules"` Location LocationBilingual `json:"location"` Description DescriptionBilingual `json:"description,omitempty"` } @@ -43,7 +43,7 @@ type DepartmentBilingual struct { } type Faculty struct { - Code string `json:"code" example:"21"` + Code string `json:"code" example:"21"` Name NameFacultyBilingual `json:"name"` } diff --git a/internal/event/event.handler.go b/internal/event/event.handler.go index 41e260f..0e1a1ff 100644 --- a/internal/event/event.handler.go +++ b/internal/event/event.handler.go @@ -32,16 +32,16 @@ type handlerImpl struct { } // GetAllEvents godoc -// @summary Get all events -// @description Get all events as array of events -// @id GetAllEvents -// @produce json -// @tags event -// @Security Bearer -// @router /events [get] -// @success 200 {object} event.EventAll -// @Failure 500 {object} event.eventAllErrorResponse -// @Failure 404 {object} event.eventInvalidResponse +// @summary Get all events +// @description Get all events as array of events +// @id GetAllEvents +// @produce json +// @tags event +// @Security Bearer +// @router /events [get] +// @success 200 {object} event.EventAll +// @Failure 500 {object} event.EventAllErrorResponse +// @Failure 404 {object} event.EventInvalidResponse func (h *handlerImpl) GetAllEvents(c *gin.Context) { hit, result, apperr := h.cache.Get(c.Request.Context(), "get_all_events") if apperr != nil { @@ -77,17 +77,17 @@ func (h *handlerImpl) GetAllEvents(c *gin.Context) { } // GetEvent godoc -// @summary get event by id -// @description Get event by id -// @id GetEventById -// @produce json -// @tags event -// @Security Bearer -// @param eventId path string true "event id" -// @router /events/{eventId} [get] -// @success 200 {object} event.EventDTO -// @Failure 500 {object} event.eventErrorResponse -// @Failure 404 {object} event.eventInvalidResponse +// @summary get event by id +// @description Get event by id +// @id GetEventById +// @produce json +// @tags event +// @Security Bearer +// @param eventId path string true "event id" +// @router /events/{eventId} [get] +// @success 200 {object} event.EventDTO +// @Failure 500 {object} event.EventErrorResponse +// @Failure 404 {object} event.EventInvalidResponse func (h *handlerImpl) GetEventById(c *gin.Context) { eventId := c.Param("eventId") diff --git a/internal/feature_flag/feature_flag.dto.go b/internal/feature_flag/feature_flag.dto.go index 377923b..e5af27a 100644 --- a/internal/feature_flag/feature_flag.dto.go +++ b/internal/feature_flag/feature_flag.dto.go @@ -1,8 +1,8 @@ package featureflag type response struct { - Key string `json:"key" example:"livestream"` - Enabled bool `json:"enabled" example:"true"` + Key string `json:"key" example:"livestream"` + Enabled bool `json:"enabled" example:"true"` ExtraInfo string `json:"extra_info" example:"https://www.youtube.com/watch?v=6n3pFFPSlW4"` } diff --git a/internal/feature_flag/feature_flag.handler.go b/internal/feature_flag/feature_flag.handler.go index 166e98d..d19c634 100644 --- a/internal/feature_flag/feature_flag.handler.go +++ b/internal/feature_flag/feature_flag.handler.go @@ -27,16 +27,16 @@ type handlerImpl struct { } // GetLivestreamInfo godoc -// @summary Get livestream flag -// @description Get livestream flag -// @id GetLivestreamInfo -// @produce json -// @tags LiveStream -// @Security Bearer -// @router /live [get] -// @success 200 {object} featureflag.response -// @Failure 500 {object} featureflag.errorResponse -// @Failure 404 {object} featureflag.invalidResponse +// @summary Get livestream flag +// @description Get livestream flag +// @id GetLivestreamInfo +// @produce json +// @tags FeatureFlag +// @Security Bearer +// @router /live [get] +// @success 200 {object} featureflag.response +// @Failure 500 {object} featureflag.errorResponse +// @Failure 404 {object} featureflag.invalidResponse func (h *handlerImpl) GetLivestreamInfo(c *gin.Context) { cacheKey := "livestream" diff --git a/internal/health_check/health_check.handler.go b/internal/health_check/health_check.handler.go index f98ad5f..6aa26a3 100644 --- a/internal/health_check/health_check.handler.go +++ b/internal/health_check/health_check.handler.go @@ -18,14 +18,14 @@ type handlerImpl struct { } // HealthCheck godoc -// @summary Health Check -// @description Health Check for the service -// @id HealthCheck -// @produce plain -// @tags healthcheck -// @Security Bearer -// @router /_hc [get] -// @Success 200 {string} string "OK" +// @summary Health Check +// @description Health Check for the service +// @id HealthCheck +// @produce plain +// @tags healthcheck +// @Security Bearer +// @router /_hc [get] +// @Success 200 {string} string "OK" func (h *handlerImpl) HealthCheck(c *gin.Context) { c.String(http.StatusOK, "OK") }