From faafe12cece012380829aa0e083f55d1dbf727b7 Mon Sep 17 00:00:00 2001 From: jace-ys Date: Sun, 27 Oct 2024 11:59:37 +0000 Subject: [PATCH] Auth --- app/api/v1/countup.go | 161 ++++- app/api/v1/gen/api/client.go | 22 +- app/api/v1/gen/api/endpoints.go | 46 +- app/api/v1/gen/api/service.go | 32 +- app/api/v1/gen/grpc/api/client/cli.go | 45 +- app/api/v1/gen/grpc/api/client/client.go | 21 +- .../v1/gen/grpc/api/client/encode_decode.go | 44 +- app/api/v1/gen/grpc/api/client/types.go | 19 + .../v1/gen/grpc/api/pb/goagen_v1_api.pb.go | 274 ++++++--- .../v1/gen/grpc/api/pb/goagen_v1_api.proto | 13 +- .../gen/grpc/api/pb/goagen_v1_api_grpc.pb.go | 40 ++ .../v1/gen/grpc/api/server/encode_decode.go | 85 ++- app/api/v1/gen/grpc/api/server/server.go | 33 +- app/api/v1/gen/grpc/api/server/types.go | 39 +- app/api/v1/gen/grpc/cli/countup/cli.go | 53 +- app/api/v1/gen/http/api/client/cli.go | 50 +- app/api/v1/gen/http/api/client/client.go | 34 + .../v1/gen/http/api/client/encode_decode.go | 132 ++++ app/api/v1/gen/http/api/client/paths.go | 11 +- app/api/v1/gen/http/api/client/types.go | 157 +++++ .../v1/gen/http/api/server/encode_decode.go | 128 +++- app/api/v1/gen/http/api/server/paths.go | 11 +- app/api/v1/gen/http/api/server/server.go | 87 ++- app/api/v1/gen/http/api/server/types.go | 125 +++- app/api/v1/gen/http/cli/countup/cli.go | 156 ++++- app/api/v1/gen/http/openapi.json | 2 +- app/api/v1/gen/http/openapi.yaml | 581 ++++++++++++++++-- app/api/v1/gen/http/openapi3.json | 2 +- app/api/v1/gen/http/openapi3.yaml | 466 ++++++++++---- app/api/v1/gen/http/web/client/cli.go | 53 ++ app/api/v1/gen/http/web/client/client.go | 136 +++- .../v1/gen/http/web/client/encode_decode.go | 453 +++++++++++++- app/api/v1/gen/http/web/client/paths.go | 24 +- app/api/v1/gen/http/web/client/types.go | 402 ++++++++++++ .../v1/gen/http/web/server/encode_decode.go | 337 +++++++++- app/api/v1/gen/http/web/server/paths.go | 24 +- app/api/v1/gen/http/web/server/server.go | 258 +++++++- app/api/v1/gen/http/web/server/types.go | 240 ++++++++ app/api/v1/gen/web/client.go | 81 ++- app/api/v1/gen/web/endpoints.go | 59 +- app/api/v1/gen/web/service.go | 64 +- app/cmd/countup/server.go | 61 +- app/go.mod | 4 + app/go.sum | 10 + app/internal/app/admin.go | 8 +- app/internal/app/http.go | 9 +- app/internal/{endpoints => endpoint}/goa.go | 6 +- .../middleware/goaerror/reporter.go | 0 .../middleware/tracer/endpoint.go | 3 +- app/internal/handler/api/auth.go | 23 + .../handler/api/{increment.go => counter.go} | 8 +- app/internal/handler/api/handler.go | 5 +- app/internal/handler/web/authn.go | 111 ++++ app/internal/handler/web/handler.go | 33 +- app/internal/handler/web/pages.go | 14 + app/internal/handler/web/templates/index.html | 6 +- app/internal/healthz/grpc.go | 2 +- app/internal/healthz/http.go | 2 +- app/internal/service/counter/finalize.go | 10 +- app/internal/service/counter/service.go | 20 +- .../service/counter/store/counter.sql.go | 39 +- app/internal/service/counter/store/querier.go | 4 +- app/internal/service/user/service.go | 19 + app/internal/slog/stdslog.go | 4 +- app/internal/transport/client.go | 83 +++ app/internal/transport/goa.go | 26 +- .../transport/middleware/recovery/recovery.go | 3 +- .../transport/middleware/telemetry/http.go | 3 +- app/internal/worker/instrumented.go | 6 +- app/internal/worker/pool.go | 1 - app/schema/counter.sql | 15 +- .../20241003194615_rivermigrate002.go | 4 +- .../20241003194729_rivermigrate003.go | 4 +- .../20241003195032_rivermigrate004.go | 4 +- .../20241003195147_rivermigrate005.go | 4 +- .../20241003195218_rivermigrate006.go | 4 +- app/schema/migrations/rivermigrate.go | 4 +- 77 files changed, 5017 insertions(+), 545 deletions(-) rename app/internal/{endpoints => endpoint}/goa.go (82%) rename app/internal/{endpoints => endpoint}/middleware/goaerror/reporter.go (100%) rename app/internal/{endpoints => endpoint}/middleware/tracer/endpoint.go (83%) create mode 100644 app/internal/handler/api/auth.go rename app/internal/handler/api/{increment.go => counter.go} (75%) create mode 100644 app/internal/handler/web/authn.go create mode 100644 app/internal/service/user/service.go create mode 100644 app/internal/transport/client.go diff --git a/app/api/v1/countup.go b/app/api/v1/countup.go index 286f902..a864c70 100644 --- a/app/api/v1/countup.go +++ b/app/api/v1/countup.go @@ -26,6 +26,10 @@ var _ = API("countup", func() { }) }) +var JWTAuth = JWTSecurity("jwt", func() { + Scope("api") +}) + var CounterInfo = ResultType("application/vnd.countup.counter-info`", "CounterInfo", func() { Field(1, "count", Int32) Field(2, "last_increment_by", String) @@ -35,12 +39,17 @@ var CounterInfo = ResultType("application/vnd.countup.counter-info`", "CounterIn }) var _ = Service("api", func() { + Security(JWTAuth, func() { + Scope("api") + }) + Error("unauthorized") Error("existing_increment_request", func() { Temporary() }) HTTP(func() { + Path("/api/v1") Response("unauthorized", StatusUnauthorized) Response("existing_increment_request", StatusTooManyRequests) }) @@ -50,7 +59,38 @@ var _ = Service("api", func() { Response("existing_increment_request", CodeAlreadyExists) }) + Method("AuthToken", func() { + NoSecurity() + + Payload(func() { + Field(1, "provider", String, func() { + Enum("google") + }) + Field(2, "access_token", String) + Required("provider", "access_token") + }) + + Result(func() { + Field(1, "token", String) + Required("token") + }) + + HTTP(func() { + POST("/auth/token") + Response(StatusOK) + }) + + GRPC(func() { + Response(CodeOK) + }) + }) + Method("CounterGet", func() { + Payload(func() { + TokenField(1, "token", String) + Required("token") + }) + Result(CounterInfo) HTTP(func() { @@ -65,14 +105,15 @@ var _ = Service("api", func() { Method("CounterIncrement", func() { Payload(func() { - Field(1, "user", String) - Required("user") + TokenField(1, "token", String) + Field(2, "user", String) + Required("token", "user") }) Result(CounterInfo) HTTP(func() { - POST("/counter/inc") + POST("/counter") Response(StatusAccepted) }) @@ -82,6 +123,8 @@ var _ = Service("api", func() { }) Method("Echo", func() { + NoSecurity() + Payload(func() { Field(1, "text", String) Required("text") @@ -106,7 +149,14 @@ var _ = Service("api", func() { }) var _ = Service("web", func() { - Method("index", func() { + Error("unauthorized") + + HTTP(func() { + Path("/") + Response("unauthorized", StatusUnauthorized) + }) + + Method("Index", func() { Result(Bytes) HTTP(func() { GET("/") @@ -116,7 +166,7 @@ var _ = Service("web", func() { }) }) - Method("another", func() { + Method("Another", func() { Result(Bytes) HTTP(func() { GET("/another") @@ -126,5 +176,104 @@ var _ = Service("web", func() { }) }) - Files("/static/{*path}", "static/") + Method("LoginGoogle", func() { + Result(func() { + Attribute("redirect_url", String) + Attribute("session_cookie", String) + Required("redirect_url", "session_cookie") + }) + + HTTP(func() { + GET("/login/google") + Response(StatusFound, func() { + Header("redirect_url:Location", String) + Cookie("session_cookie:countup.session") + CookieSameSite(CookieSameSiteLax) + CookieMaxAge(86400) + CookieHTTPOnly() + // CookieSecure() + CookiePath("/") + }) + }) + }) + + Method("LoginGoogleCallback", func() { + Payload(func() { + Attribute("code", String) + Attribute("state", String) + Attribute("session_cookie", String) + Required("code", "state", "session_cookie") + }) + + Result(func() { + Attribute("redirect_url", String) + Attribute("session_cookie", String) + Required("redirect_url", "session_cookie") + }) + + HTTP(func() { + GET("/login/google/callback") + Param("code", String) + Param("state", String) + Cookie("session_cookie:countup.session") + Response(StatusFound, func() { + Header("redirect_url:Location", String) + Cookie("session_cookie:countup.session") + CookieSameSite(CookieSameSiteLax) + CookieMaxAge(86400) + CookieHTTPOnly() + // CookieSecure() + CookiePath("/") + }) + }) + }) + + Method("Logout", func() { + Payload(func() { + Attribute("session_cookie", String) + Required("session_cookie") + }) + + Result(func() { + Attribute("redirect_url", String) + Attribute("session_cookie", String) + Required("redirect_url", "session_cookie") + }) + + HTTP(func() { + GET("/logout") + Cookie("session_cookie:countup.session") + Response(StatusFound, func() { + Header("redirect_url:Location", String) + Cookie("session_cookie:countup.session") + CookieSameSite(CookieSameSiteLax) + CookieMaxAge(86400) + CookieHTTPOnly() + // CookieSecure() + CookiePath("/") + }) + }) + }) + + Method("SessionToken", func() { + Payload(func() { + Attribute("session_cookie", String) + Required("session_cookie") + }) + + Result(func() { + Attribute("token", String) + Required("token") + }) + + HTTP(func() { + GET("/session/token") + Cookie("session_cookie:countup.session") + Response(StatusOK, func() { + ContentType("application/json") + }) + }) + }) + + Files("/static/*", "static/") }) diff --git a/app/api/v1/gen/api/client.go b/app/api/v1/gen/api/client.go index 3e8a87f..7805d97 100644 --- a/app/api/v1/gen/api/client.go +++ b/app/api/v1/gen/api/client.go @@ -15,28 +15,44 @@ import ( // Client is the "api" service client. type Client struct { + AuthTokenEndpoint goa.Endpoint CounterGetEndpoint goa.Endpoint CounterIncrementEndpoint goa.Endpoint EchoEndpoint goa.Endpoint } // NewClient initializes a "api" service client given the endpoints. -func NewClient(counterGet, counterIncrement, echo goa.Endpoint) *Client { +func NewClient(authToken, counterGet, counterIncrement, echo goa.Endpoint) *Client { return &Client{ + AuthTokenEndpoint: authToken, CounterGetEndpoint: counterGet, CounterIncrementEndpoint: counterIncrement, EchoEndpoint: echo, } } +// AuthToken calls the "AuthToken" endpoint of the "api" service. +// AuthToken may return the following errors: +// - "unauthorized" (type *goa.ServiceError) +// - "existing_increment_request" (type *goa.ServiceError) +// - error: internal error +func (c *Client) AuthToken(ctx context.Context, p *AuthTokenPayload) (res *AuthTokenResult, err error) { + var ires any + ires, err = c.AuthTokenEndpoint(ctx, p) + if err != nil { + return + } + return ires.(*AuthTokenResult), nil +} + // CounterGet calls the "CounterGet" endpoint of the "api" service. // CounterGet may return the following errors: // - "unauthorized" (type *goa.ServiceError) // - "existing_increment_request" (type *goa.ServiceError) // - error: internal error -func (c *Client) CounterGet(ctx context.Context) (res *CounterInfo, err error) { +func (c *Client) CounterGet(ctx context.Context, p *CounterGetPayload) (res *CounterInfo, err error) { var ires any - ires, err = c.CounterGetEndpoint(ctx, nil) + ires, err = c.CounterGetEndpoint(ctx, p) if err != nil { return } diff --git a/app/api/v1/gen/api/endpoints.go b/app/api/v1/gen/api/endpoints.go index 27587bd..620dac9 100644 --- a/app/api/v1/gen/api/endpoints.go +++ b/app/api/v1/gen/api/endpoints.go @@ -11,10 +11,12 @@ import ( "context" goa "goa.design/goa/v3/pkg" + "goa.design/goa/v3/security" ) // Endpoints wraps the "api" service endpoints. type Endpoints struct { + AuthToken goa.Endpoint CounterGet goa.Endpoint CounterIncrement goa.Endpoint Echo goa.Endpoint @@ -22,25 +24,49 @@ type Endpoints struct { // NewEndpoints wraps the methods of the "api" service with endpoints. func NewEndpoints(s Service) *Endpoints { + // Casting service to Auther interface + a := s.(Auther) return &Endpoints{ - CounterGet: NewCounterGetEndpoint(s), - CounterIncrement: NewCounterIncrementEndpoint(s), + AuthToken: NewAuthTokenEndpoint(s), + CounterGet: NewCounterGetEndpoint(s, a.JWTAuth), + CounterIncrement: NewCounterIncrementEndpoint(s, a.JWTAuth), Echo: NewEchoEndpoint(s), } } // Use applies the given middleware to all the "api" service endpoints. func (e *Endpoints) Use(m func(goa.Endpoint) goa.Endpoint) { + e.AuthToken = m(e.AuthToken) e.CounterGet = m(e.CounterGet) e.CounterIncrement = m(e.CounterIncrement) e.Echo = m(e.Echo) } +// NewAuthTokenEndpoint returns an endpoint function that calls the method +// "AuthToken" of service "api". +func NewAuthTokenEndpoint(s Service) goa.Endpoint { + return func(ctx context.Context, req any) (any, error) { + p := req.(*AuthTokenPayload) + return s.AuthToken(ctx, p) + } +} + // NewCounterGetEndpoint returns an endpoint function that calls the method // "CounterGet" of service "api". -func NewCounterGetEndpoint(s Service) goa.Endpoint { +func NewCounterGetEndpoint(s Service, authJWTFn security.AuthJWTFunc) goa.Endpoint { return func(ctx context.Context, req any) (any, error) { - res, err := s.CounterGet(ctx) + p := req.(*CounterGetPayload) + var err error + sc := security.JWTScheme{ + Name: "jwt", + Scopes: []string{"api"}, + RequiredScopes: []string{"api"}, + } + ctx, err = authJWTFn(ctx, p.Token, &sc) + if err != nil { + return nil, err + } + res, err := s.CounterGet(ctx, p) if err != nil { return nil, err } @@ -51,9 +77,19 @@ func NewCounterGetEndpoint(s Service) goa.Endpoint { // NewCounterIncrementEndpoint returns an endpoint function that calls the // method "CounterIncrement" of service "api". -func NewCounterIncrementEndpoint(s Service) goa.Endpoint { +func NewCounterIncrementEndpoint(s Service, authJWTFn security.AuthJWTFunc) goa.Endpoint { return func(ctx context.Context, req any) (any, error) { p := req.(*CounterIncrementPayload) + var err error + sc := security.JWTScheme{ + Name: "jwt", + Scopes: []string{"api"}, + RequiredScopes: []string{"api"}, + } + ctx, err = authJWTFn(ctx, p.Token, &sc) + if err != nil { + return nil, err + } res, err := s.CounterIncrement(ctx, p) if err != nil { return nil, err diff --git a/app/api/v1/gen/api/service.go b/app/api/v1/gen/api/service.go index 52c794d..6f26cd6 100644 --- a/app/api/v1/gen/api/service.go +++ b/app/api/v1/gen/api/service.go @@ -12,18 +12,27 @@ import ( apiviews "github.com/jace-ys/countup/api/v1/gen/api/views" goa "goa.design/goa/v3/pkg" + "goa.design/goa/v3/security" ) // Service is the api service interface. type Service interface { + // AuthToken implements AuthToken. + AuthToken(context.Context, *AuthTokenPayload) (res *AuthTokenResult, err error) // CounterGet implements CounterGet. - CounterGet(context.Context) (res *CounterInfo, err error) + CounterGet(context.Context, *CounterGetPayload) (res *CounterInfo, err error) // CounterIncrement implements CounterIncrement. CounterIncrement(context.Context, *CounterIncrementPayload) (res *CounterInfo, err error) // Echo implements Echo. Echo(context.Context, *EchoPayload) (res *EchoResult, err error) } +// Auther defines the authorization functions to be implemented by the service. +type Auther interface { + // JWTAuth implements the authorization logic for the JWT security scheme. + JWTAuth(ctx context.Context, token string, schema *security.JWTScheme) (context.Context, error) +} + // APIName is the name of the API as defined in the design. const APIName = "countup" @@ -38,12 +47,29 @@ const ServiceName = "api" // MethodNames lists the service method names as defined in the design. These // are the same values that are set in the endpoint request contexts under the // MethodKey key. -var MethodNames = [3]string{"CounterGet", "CounterIncrement", "Echo"} +var MethodNames = [4]string{"AuthToken", "CounterGet", "CounterIncrement", "Echo"} + +// AuthTokenPayload is the payload type of the api service AuthToken method. +type AuthTokenPayload struct { + Provider string + AccessToken string +} + +// AuthTokenResult is the result type of the api service AuthToken method. +type AuthTokenResult struct { + Token string +} + +// CounterGetPayload is the payload type of the api service CounterGet method. +type CounterGetPayload struct { + Token string +} // CounterIncrementPayload is the payload type of the api service // CounterIncrement method. type CounterIncrementPayload struct { - User string + Token string + User string } // CounterInfo is the result type of the api service CounterGet method. diff --git a/app/api/v1/gen/grpc/api/client/cli.go b/app/api/v1/gen/grpc/api/client/cli.go index 0e67ec9..13fc292 100644 --- a/app/api/v1/gen/grpc/api/client/cli.go +++ b/app/api/v1/gen/grpc/api/client/cli.go @@ -15,22 +15,61 @@ import ( apipb "github.com/jace-ys/countup/api/v1/gen/grpc/api/pb" ) +// BuildAuthTokenPayload builds the payload for the api AuthToken endpoint from +// CLI flags. +func BuildAuthTokenPayload(apiAuthTokenMessage string) (*api.AuthTokenPayload, error) { + var err error + var message apipb.AuthTokenRequest + { + if apiAuthTokenMessage != "" { + err = json.Unmarshal([]byte(apiAuthTokenMessage), &message) + if err != nil { + return nil, fmt.Errorf("invalid JSON for message, \nerror: %s, \nexample of valid JSON:\n%s", err, "'{\n \"access_token\": \"Et eum.\",\n \"provider\": \"google\"\n }'") + } + } + } + v := &api.AuthTokenPayload{ + Provider: message.Provider, + AccessToken: message.AccessToken, + } + + return v, nil +} + +// BuildCounterGetPayload builds the payload for the api CounterGet endpoint +// from CLI flags. +func BuildCounterGetPayload(apiCounterGetToken string) (*api.CounterGetPayload, error) { + var token string + { + token = apiCounterGetToken + } + v := &api.CounterGetPayload{} + v.Token = token + + return v, nil +} + // BuildCounterIncrementPayload builds the payload for the api CounterIncrement // endpoint from CLI flags. -func BuildCounterIncrementPayload(apiCounterIncrementMessage string) (*api.CounterIncrementPayload, error) { +func BuildCounterIncrementPayload(apiCounterIncrementMessage string, apiCounterIncrementToken string) (*api.CounterIncrementPayload, error) { var err error var message apipb.CounterIncrementRequest { if apiCounterIncrementMessage != "" { err = json.Unmarshal([]byte(apiCounterIncrementMessage), &message) if err != nil { - return nil, fmt.Errorf("invalid JSON for message, \nerror: %s, \nexample of valid JSON:\n%s", err, "'{\n \"user\": \"Non accusantium eos culpa autem illum architecto.\"\n }'") + return nil, fmt.Errorf("invalid JSON for message, \nerror: %s, \nexample of valid JSON:\n%s", err, "'{\n \"user\": \"Voluptates voluptas eius cumque maxime dolore.\"\n }'") } } } + var token string + { + token = apiCounterIncrementToken + } v := &api.CounterIncrementPayload{ User: message.User, } + v.Token = token return v, nil } @@ -43,7 +82,7 @@ func BuildEchoPayload(apiEchoMessage string) (*api.EchoPayload, error) { if apiEchoMessage != "" { err = json.Unmarshal([]byte(apiEchoMessage), &message) if err != nil { - return nil, fmt.Errorf("invalid JSON for message, \nerror: %s, \nexample of valid JSON:\n%s", err, "'{\n \"text\": \"Cupiditate tempore harum error iste ipsam natus.\"\n }'") + return nil, fmt.Errorf("invalid JSON for message, \nerror: %s, \nexample of valid JSON:\n%s", err, "'{\n \"text\": \"Quidem inventore.\"\n }'") } } } diff --git a/app/api/v1/gen/grpc/api/client/client.go b/app/api/v1/gen/grpc/api/client/client.go index 38b0cf8..5105b68 100644 --- a/app/api/v1/gen/grpc/api/client/client.go +++ b/app/api/v1/gen/grpc/api/client/client.go @@ -27,12 +27,31 @@ func NewClient(cc *grpc.ClientConn, opts ...grpc.CallOption) *Client { grpccli: apipb.NewAPIClient(cc), opts: opts, } +} // AuthToken calls the "AuthToken" function in apipb.APIClient interface. +func (c *Client) AuthToken() goa.Endpoint { + return func(ctx context.Context, v any) (any, error) { + inv := goagrpc.NewInvoker( + BuildAuthTokenFunc(c.grpccli, c.opts...), + EncodeAuthTokenRequest, + DecodeAuthTokenResponse) + res, err := inv.Invoke(ctx, v) + if err != nil { + resp := goagrpc.DecodeError(err) + switch message := resp.(type) { + case *goapb.ErrorResponse: + return nil, goagrpc.NewServiceError(message) + default: + return nil, goa.Fault(err.Error()) + } + } + return res, nil + } } // CounterGet calls the "CounterGet" function in apipb.APIClient interface. func (c *Client) CounterGet() goa.Endpoint { return func(ctx context.Context, v any) (any, error) { inv := goagrpc.NewInvoker( BuildCounterGetFunc(c.grpccli, c.opts...), - nil, + EncodeCounterGetRequest, DecodeCounterGetResponse) res, err := inv.Invoke(ctx, v) if err != nil { diff --git a/app/api/v1/gen/grpc/api/client/encode_decode.go b/app/api/v1/gen/grpc/api/client/encode_decode.go index 358b5b5..03cc3fd 100644 --- a/app/api/v1/gen/grpc/api/client/encode_decode.go +++ b/app/api/v1/gen/grpc/api/client/encode_decode.go @@ -18,7 +18,38 @@ import ( "google.golang.org/grpc/metadata" ) -// BuildCounterGetFunc builds the remote method to invoke for "api" service +// BuildAuthTokenFunc builds the remote method to invoke for "api" service +// "AuthToken" endpoint. +func BuildAuthTokenFunc(grpccli apipb.APIClient, cliopts ...grpc.CallOption) goagrpc.RemoteFunc { + return func(ctx context.Context, reqpb any, opts ...grpc.CallOption) (any, error) { + for _, opt := range cliopts { + opts = append(opts, opt) + } + if reqpb != nil { + return grpccli.AuthToken(ctx, reqpb.(*apipb.AuthTokenRequest), opts...) + } + return grpccli.AuthToken(ctx, &apipb.AuthTokenRequest{}, opts...) + } +} + +// EncodeAuthTokenRequest encodes requests sent to api AuthToken endpoint. +func EncodeAuthTokenRequest(ctx context.Context, v any, md *metadata.MD) (any, error) { + payload, ok := v.(*api.AuthTokenPayload) + if !ok { + return nil, goagrpc.ErrInvalidType("api", "AuthToken", "*api.AuthTokenPayload", v) + } + return NewProtoAuthTokenRequest(payload), nil +} + +// DecodeAuthTokenResponse decodes responses from the api AuthToken endpoint. +func DecodeAuthTokenResponse(ctx context.Context, v any, hdr, trlr metadata.MD) (any, error) { + message, ok := v.(*apipb.AuthTokenResponse) + if !ok { + return nil, goagrpc.ErrInvalidType("api", "AuthToken", "*apipb.AuthTokenResponse", v) + } + res := NewAuthTokenResult(message) + return res, nil +} // BuildCounterGetFunc builds the remote method to invoke for "api" service // "CounterGet" endpoint. func BuildCounterGetFunc(grpccli apipb.APIClient, cliopts ...grpc.CallOption) goagrpc.RemoteFunc { return func(ctx context.Context, reqpb any, opts ...grpc.CallOption) (any, error) { @@ -32,6 +63,16 @@ func BuildCounterGetFunc(grpccli apipb.APIClient, cliopts ...grpc.CallOption) go } } +// EncodeCounterGetRequest encodes requests sent to api CounterGet endpoint. +func EncodeCounterGetRequest(ctx context.Context, v any, md *metadata.MD) (any, error) { + payload, ok := v.(*api.CounterGetPayload) + if !ok { + return nil, goagrpc.ErrInvalidType("api", "CounterGet", "*api.CounterGetPayload", v) + } + (*md).Append("authorization", payload.Token) + return NewProtoCounterGetRequest(), nil +} + // DecodeCounterGetResponse decodes responses from the api CounterGet endpoint. func DecodeCounterGetResponse(ctx context.Context, v any, hdr, trlr metadata.MD) (any, error) { var view string @@ -71,6 +112,7 @@ func EncodeCounterIncrementRequest(ctx context.Context, v any, md *metadata.MD) if !ok { return nil, goagrpc.ErrInvalidType("api", "CounterIncrement", "*api.CounterIncrementPayload", v) } + (*md).Append("authorization", payload.Token) return NewProtoCounterIncrementRequest(payload), nil } diff --git a/app/api/v1/gen/grpc/api/client/types.go b/app/api/v1/gen/grpc/api/client/types.go index 596e175..c894535 100644 --- a/app/api/v1/gen/grpc/api/client/types.go +++ b/app/api/v1/gen/grpc/api/client/types.go @@ -13,6 +13,25 @@ import ( apipb "github.com/jace-ys/countup/api/v1/gen/grpc/api/pb" ) +// NewProtoAuthTokenRequest builds the gRPC request type from the payload of +// the "AuthToken" endpoint of the "api" service. +func NewProtoAuthTokenRequest(payload *api.AuthTokenPayload) *apipb.AuthTokenRequest { + message := &apipb.AuthTokenRequest{ + Provider: payload.Provider, + AccessToken: payload.AccessToken, + } + return message +} + +// NewAuthTokenResult builds the result type of the "AuthToken" endpoint of the +// "api" service from the gRPC response type. +func NewAuthTokenResult(message *apipb.AuthTokenResponse) *api.AuthTokenResult { + result := &api.AuthTokenResult{ + Token: message.Token, + } + return result +} + // NewProtoCounterGetRequest builds the gRPC request type from the payload of // the "CounterGet" endpoint of the "api" service. func NewProtoCounterGetRequest() *apipb.CounterGetRequest { diff --git a/app/api/v1/gen/grpc/api/pb/goagen_v1_api.pb.go b/app/api/v1/gen/grpc/api/pb/goagen_v1_api.pb.go index ecc0f0a..c63b60e 100644 --- a/app/api/v1/gen/grpc/api/pb/goagen_v1_api.pb.go +++ b/app/api/v1/gen/grpc/api/pb/goagen_v1_api.pb.go @@ -27,6 +27,104 @@ const ( _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) +type AuthTokenRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Provider string `protobuf:"bytes,1,opt,name=provider,proto3" json:"provider,omitempty"` + AccessToken string `protobuf:"bytes,2,opt,name=access_token,json=accessToken,proto3" json:"access_token,omitempty"` +} + +func (x *AuthTokenRequest) Reset() { + *x = AuthTokenRequest{} + mi := &file_goagen_v1_api_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AuthTokenRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AuthTokenRequest) ProtoMessage() {} + +func (x *AuthTokenRequest) ProtoReflect() protoreflect.Message { + mi := &file_goagen_v1_api_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AuthTokenRequest.ProtoReflect.Descriptor instead. +func (*AuthTokenRequest) Descriptor() ([]byte, []int) { + return file_goagen_v1_api_proto_rawDescGZIP(), []int{0} +} + +func (x *AuthTokenRequest) GetProvider() string { + if x != nil { + return x.Provider + } + return "" +} + +func (x *AuthTokenRequest) GetAccessToken() string { + if x != nil { + return x.AccessToken + } + return "" +} + +type AuthTokenResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Token string `protobuf:"bytes,1,opt,name=token,proto3" json:"token,omitempty"` +} + +func (x *AuthTokenResponse) Reset() { + *x = AuthTokenResponse{} + mi := &file_goagen_v1_api_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AuthTokenResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AuthTokenResponse) ProtoMessage() {} + +func (x *AuthTokenResponse) ProtoReflect() protoreflect.Message { + mi := &file_goagen_v1_api_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AuthTokenResponse.ProtoReflect.Descriptor instead. +func (*AuthTokenResponse) Descriptor() ([]byte, []int) { + return file_goagen_v1_api_proto_rawDescGZIP(), []int{1} +} + +func (x *AuthTokenResponse) GetToken() string { + if x != nil { + return x.Token + } + return "" +} + type CounterGetRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -35,7 +133,7 @@ type CounterGetRequest struct { func (x *CounterGetRequest) Reset() { *x = CounterGetRequest{} - mi := &file_goagen_v1_api_proto_msgTypes[0] + mi := &file_goagen_v1_api_proto_msgTypes[2] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -47,7 +145,7 @@ func (x *CounterGetRequest) String() string { func (*CounterGetRequest) ProtoMessage() {} func (x *CounterGetRequest) ProtoReflect() protoreflect.Message { - mi := &file_goagen_v1_api_proto_msgTypes[0] + mi := &file_goagen_v1_api_proto_msgTypes[2] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -60,7 +158,7 @@ func (x *CounterGetRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use CounterGetRequest.ProtoReflect.Descriptor instead. func (*CounterGetRequest) Descriptor() ([]byte, []int) { - return file_goagen_v1_api_proto_rawDescGZIP(), []int{0} + return file_goagen_v1_api_proto_rawDescGZIP(), []int{2} } type CounterGetResponse struct { @@ -76,7 +174,7 @@ type CounterGetResponse struct { func (x *CounterGetResponse) Reset() { *x = CounterGetResponse{} - mi := &file_goagen_v1_api_proto_msgTypes[1] + mi := &file_goagen_v1_api_proto_msgTypes[3] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -88,7 +186,7 @@ func (x *CounterGetResponse) String() string { func (*CounterGetResponse) ProtoMessage() {} func (x *CounterGetResponse) ProtoReflect() protoreflect.Message { - mi := &file_goagen_v1_api_proto_msgTypes[1] + mi := &file_goagen_v1_api_proto_msgTypes[3] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -101,7 +199,7 @@ func (x *CounterGetResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use CounterGetResponse.ProtoReflect.Descriptor instead. func (*CounterGetResponse) Descriptor() ([]byte, []int) { - return file_goagen_v1_api_proto_rawDescGZIP(), []int{1} + return file_goagen_v1_api_proto_rawDescGZIP(), []int{3} } func (x *CounterGetResponse) GetCount() int32 { @@ -137,12 +235,12 @@ type CounterIncrementRequest struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - User string `protobuf:"bytes,1,opt,name=user,proto3" json:"user,omitempty"` + User string `protobuf:"bytes,2,opt,name=user,proto3" json:"user,omitempty"` } func (x *CounterIncrementRequest) Reset() { *x = CounterIncrementRequest{} - mi := &file_goagen_v1_api_proto_msgTypes[2] + mi := &file_goagen_v1_api_proto_msgTypes[4] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -154,7 +252,7 @@ func (x *CounterIncrementRequest) String() string { func (*CounterIncrementRequest) ProtoMessage() {} func (x *CounterIncrementRequest) ProtoReflect() protoreflect.Message { - mi := &file_goagen_v1_api_proto_msgTypes[2] + mi := &file_goagen_v1_api_proto_msgTypes[4] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -167,7 +265,7 @@ func (x *CounterIncrementRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use CounterIncrementRequest.ProtoReflect.Descriptor instead. func (*CounterIncrementRequest) Descriptor() ([]byte, []int) { - return file_goagen_v1_api_proto_rawDescGZIP(), []int{2} + return file_goagen_v1_api_proto_rawDescGZIP(), []int{4} } func (x *CounterIncrementRequest) GetUser() string { @@ -190,7 +288,7 @@ type CounterIncrementResponse struct { func (x *CounterIncrementResponse) Reset() { *x = CounterIncrementResponse{} - mi := &file_goagen_v1_api_proto_msgTypes[3] + mi := &file_goagen_v1_api_proto_msgTypes[5] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -202,7 +300,7 @@ func (x *CounterIncrementResponse) String() string { func (*CounterIncrementResponse) ProtoMessage() {} func (x *CounterIncrementResponse) ProtoReflect() protoreflect.Message { - mi := &file_goagen_v1_api_proto_msgTypes[3] + mi := &file_goagen_v1_api_proto_msgTypes[5] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -215,7 +313,7 @@ func (x *CounterIncrementResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use CounterIncrementResponse.ProtoReflect.Descriptor instead. func (*CounterIncrementResponse) Descriptor() ([]byte, []int) { - return file_goagen_v1_api_proto_rawDescGZIP(), []int{3} + return file_goagen_v1_api_proto_rawDescGZIP(), []int{5} } func (x *CounterIncrementResponse) GetCount() int32 { @@ -256,7 +354,7 @@ type EchoRequest struct { func (x *EchoRequest) Reset() { *x = EchoRequest{} - mi := &file_goagen_v1_api_proto_msgTypes[4] + mi := &file_goagen_v1_api_proto_msgTypes[6] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -268,7 +366,7 @@ func (x *EchoRequest) String() string { func (*EchoRequest) ProtoMessage() {} func (x *EchoRequest) ProtoReflect() protoreflect.Message { - mi := &file_goagen_v1_api_proto_msgTypes[4] + mi := &file_goagen_v1_api_proto_msgTypes[6] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -281,7 +379,7 @@ func (x *EchoRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use EchoRequest.ProtoReflect.Descriptor instead. func (*EchoRequest) Descriptor() ([]byte, []int) { - return file_goagen_v1_api_proto_rawDescGZIP(), []int{4} + return file_goagen_v1_api_proto_rawDescGZIP(), []int{6} } func (x *EchoRequest) GetText() string { @@ -301,7 +399,7 @@ type EchoResponse struct { func (x *EchoResponse) Reset() { *x = EchoResponse{} - mi := &file_goagen_v1_api_proto_msgTypes[5] + mi := &file_goagen_v1_api_proto_msgTypes[7] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -313,7 +411,7 @@ func (x *EchoResponse) String() string { func (*EchoResponse) ProtoMessage() {} func (x *EchoResponse) ProtoReflect() protoreflect.Message { - mi := &file_goagen_v1_api_proto_msgTypes[5] + mi := &file_goagen_v1_api_proto_msgTypes[7] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -326,7 +424,7 @@ func (x *EchoResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use EchoResponse.ProtoReflect.Descriptor instead. func (*EchoResponse) Descriptor() ([]byte, []int) { - return file_goagen_v1_api_proto_rawDescGZIP(), []int{5} + return file_goagen_v1_api_proto_rawDescGZIP(), []int{7} } func (x *EchoResponse) GetText() string { @@ -340,51 +438,63 @@ var File_goagen_v1_api_proto protoreflect.FileDescriptor var file_goagen_v1_api_proto_rawDesc = []byte{ 0x0a, 0x13, 0x67, 0x6f, 0x61, 0x67, 0x65, 0x6e, 0x5f, 0x76, 0x31, 0x5f, 0x61, 0x70, 0x69, 0x2e, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x03, 0x61, 0x70, 0x69, 0x22, 0x13, 0x0a, 0x11, 0x43, 0x6f, - 0x75, 0x6e, 0x74, 0x65, 0x72, 0x47, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, - 0xac, 0x01, 0x0a, 0x12, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x65, 0x72, 0x47, 0x65, 0x74, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x11, 0x52, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x2a, 0x0a, 0x11, - 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x69, 0x6e, 0x63, 0x72, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x5f, 0x62, - 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x6c, 0x61, 0x73, 0x74, 0x49, 0x6e, 0x63, - 0x72, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x42, 0x79, 0x12, 0x2a, 0x0a, 0x11, 0x6c, 0x61, 0x73, 0x74, - 0x5f, 0x69, 0x6e, 0x63, 0x72, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x5f, 0x61, 0x74, 0x18, 0x03, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x0f, 0x6c, 0x61, 0x73, 0x74, 0x49, 0x6e, 0x63, 0x72, 0x65, 0x6d, 0x65, - 0x6e, 0x74, 0x41, 0x74, 0x12, 0x28, 0x0a, 0x10, 0x6e, 0x65, 0x78, 0x74, 0x5f, 0x66, 0x69, 0x6e, - 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x5f, 0x61, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, - 0x6e, 0x65, 0x78, 0x74, 0x46, 0x69, 0x6e, 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x41, 0x74, 0x22, 0x2d, - 0x0a, 0x17, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x65, 0x72, 0x49, 0x6e, 0x63, 0x72, 0x65, 0x6d, 0x65, - 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x75, 0x73, 0x65, - 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x75, 0x73, 0x65, 0x72, 0x22, 0xb2, 0x01, - 0x0a, 0x18, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x65, 0x72, 0x49, 0x6e, 0x63, 0x72, 0x65, 0x6d, 0x65, - 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x63, 0x6f, - 0x75, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x11, 0x52, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, - 0x12, 0x2a, 0x0a, 0x11, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x69, 0x6e, 0x63, 0x72, 0x65, 0x6d, 0x65, - 0x6e, 0x74, 0x5f, 0x62, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x6c, 0x61, 0x73, - 0x74, 0x49, 0x6e, 0x63, 0x72, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x42, 0x79, 0x12, 0x2a, 0x0a, 0x11, - 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x69, 0x6e, 0x63, 0x72, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x5f, 0x61, - 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x6c, 0x61, 0x73, 0x74, 0x49, 0x6e, 0x63, - 0x72, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x41, 0x74, 0x12, 0x28, 0x0a, 0x10, 0x6e, 0x65, 0x78, 0x74, - 0x5f, 0x66, 0x69, 0x6e, 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x5f, 0x61, 0x74, 0x18, 0x04, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x0e, 0x6e, 0x65, 0x78, 0x74, 0x46, 0x69, 0x6e, 0x61, 0x6c, 0x69, 0x7a, 0x65, - 0x41, 0x74, 0x22, 0x21, 0x0a, 0x0b, 0x45, 0x63, 0x68, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x65, 0x78, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x04, 0x74, 0x65, 0x78, 0x74, 0x22, 0x22, 0x0a, 0x0c, 0x45, 0x63, 0x68, 0x6f, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x65, 0x78, 0x74, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x65, 0x78, 0x74, 0x32, 0xc2, 0x01, 0x0a, 0x03, 0x41, 0x50, - 0x49, 0x12, 0x3d, 0x0a, 0x0a, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x65, 0x72, 0x47, 0x65, 0x74, 0x12, - 0x16, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x65, 0x72, 0x47, 0x65, 0x74, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x17, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x6f, - 0x75, 0x6e, 0x74, 0x65, 0x72, 0x47, 0x65, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x12, 0x4f, 0x0a, 0x10, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x65, 0x72, 0x49, 0x6e, 0x63, 0x72, 0x65, - 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x1c, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x6f, 0x75, 0x6e, 0x74, - 0x65, 0x72, 0x49, 0x6e, 0x63, 0x72, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x65, 0x72, - 0x49, 0x6e, 0x63, 0x72, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x12, 0x2b, 0x0a, 0x04, 0x45, 0x63, 0x68, 0x6f, 0x12, 0x10, 0x2e, 0x61, 0x70, 0x69, 0x2e, - 0x45, 0x63, 0x68, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x11, 0x2e, 0x61, 0x70, - 0x69, 0x2e, 0x45, 0x63, 0x68, 0x6f, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x08, - 0x5a, 0x06, 0x2f, 0x61, 0x70, 0x69, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x03, 0x61, 0x70, 0x69, 0x22, 0x51, 0x0a, 0x10, 0x41, 0x75, + 0x74, 0x68, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, + 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x08, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x12, 0x21, 0x0a, 0x0c, 0x61, 0x63, + 0x63, 0x65, 0x73, 0x73, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x0b, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x22, 0x29, 0x0a, + 0x11, 0x41, 0x75, 0x74, 0x68, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x22, 0x13, 0x0a, 0x11, 0x43, 0x6f, 0x75, 0x6e, + 0x74, 0x65, 0x72, 0x47, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0xac, 0x01, + 0x0a, 0x12, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x65, 0x72, 0x47, 0x65, 0x74, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x11, 0x52, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x2a, 0x0a, 0x11, 0x6c, 0x61, + 0x73, 0x74, 0x5f, 0x69, 0x6e, 0x63, 0x72, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x5f, 0x62, 0x79, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x6c, 0x61, 0x73, 0x74, 0x49, 0x6e, 0x63, 0x72, 0x65, + 0x6d, 0x65, 0x6e, 0x74, 0x42, 0x79, 0x12, 0x2a, 0x0a, 0x11, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x69, + 0x6e, 0x63, 0x72, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x5f, 0x61, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x0f, 0x6c, 0x61, 0x73, 0x74, 0x49, 0x6e, 0x63, 0x72, 0x65, 0x6d, 0x65, 0x6e, 0x74, + 0x41, 0x74, 0x12, 0x28, 0x0a, 0x10, 0x6e, 0x65, 0x78, 0x74, 0x5f, 0x66, 0x69, 0x6e, 0x61, 0x6c, + 0x69, 0x7a, 0x65, 0x5f, 0x61, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x6e, 0x65, + 0x78, 0x74, 0x46, 0x69, 0x6e, 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x41, 0x74, 0x22, 0x2d, 0x0a, 0x17, + 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x65, 0x72, 0x49, 0x6e, 0x63, 0x72, 0x65, 0x6d, 0x65, 0x6e, 0x74, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x75, 0x73, 0x65, 0x72, 0x22, 0xb2, 0x01, 0x0a, 0x18, + 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x65, 0x72, 0x49, 0x6e, 0x63, 0x72, 0x65, 0x6d, 0x65, 0x6e, 0x74, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x63, 0x6f, 0x75, 0x6e, + 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x11, 0x52, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x2a, + 0x0a, 0x11, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x69, 0x6e, 0x63, 0x72, 0x65, 0x6d, 0x65, 0x6e, 0x74, + 0x5f, 0x62, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x6c, 0x61, 0x73, 0x74, 0x49, + 0x6e, 0x63, 0x72, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x42, 0x79, 0x12, 0x2a, 0x0a, 0x11, 0x6c, 0x61, + 0x73, 0x74, 0x5f, 0x69, 0x6e, 0x63, 0x72, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x5f, 0x61, 0x74, 0x18, + 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x6c, 0x61, 0x73, 0x74, 0x49, 0x6e, 0x63, 0x72, 0x65, + 0x6d, 0x65, 0x6e, 0x74, 0x41, 0x74, 0x12, 0x28, 0x0a, 0x10, 0x6e, 0x65, 0x78, 0x74, 0x5f, 0x66, + 0x69, 0x6e, 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x5f, 0x61, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x0e, 0x6e, 0x65, 0x78, 0x74, 0x46, 0x69, 0x6e, 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x41, 0x74, + 0x22, 0x21, 0x0a, 0x0b, 0x45, 0x63, 0x68, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, + 0x12, 0x0a, 0x04, 0x74, 0x65, 0x78, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, + 0x65, 0x78, 0x74, 0x22, 0x22, 0x0a, 0x0c, 0x45, 0x63, 0x68, 0x6f, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x65, 0x78, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x04, 0x74, 0x65, 0x78, 0x74, 0x32, 0xfe, 0x01, 0x0a, 0x03, 0x41, 0x50, 0x49, 0x12, + 0x3a, 0x0a, 0x09, 0x41, 0x75, 0x74, 0x68, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x15, 0x2e, 0x61, + 0x70, 0x69, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x54, 0x6f, + 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3d, 0x0a, 0x0a, 0x43, + 0x6f, 0x75, 0x6e, 0x74, 0x65, 0x72, 0x47, 0x65, 0x74, 0x12, 0x16, 0x2e, 0x61, 0x70, 0x69, 0x2e, + 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x65, 0x72, 0x47, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x17, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x65, 0x72, 0x47, + 0x65, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4f, 0x0a, 0x10, 0x43, 0x6f, + 0x75, 0x6e, 0x74, 0x65, 0x72, 0x49, 0x6e, 0x63, 0x72, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x1c, + 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x65, 0x72, 0x49, 0x6e, 0x63, 0x72, + 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x61, + 0x70, 0x69, 0x2e, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x65, 0x72, 0x49, 0x6e, 0x63, 0x72, 0x65, 0x6d, + 0x65, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2b, 0x0a, 0x04, 0x45, + 0x63, 0x68, 0x6f, 0x12, 0x10, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x45, 0x63, 0x68, 0x6f, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x11, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x45, 0x63, 0x68, 0x6f, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x08, 0x5a, 0x06, 0x2f, 0x61, 0x70, 0x69, + 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -399,24 +509,28 @@ func file_goagen_v1_api_proto_rawDescGZIP() []byte { return file_goagen_v1_api_proto_rawDescData } -var file_goagen_v1_api_proto_msgTypes = make([]protoimpl.MessageInfo, 6) +var file_goagen_v1_api_proto_msgTypes = make([]protoimpl.MessageInfo, 8) var file_goagen_v1_api_proto_goTypes = []any{ - (*CounterGetRequest)(nil), // 0: api.CounterGetRequest - (*CounterGetResponse)(nil), // 1: api.CounterGetResponse - (*CounterIncrementRequest)(nil), // 2: api.CounterIncrementRequest - (*CounterIncrementResponse)(nil), // 3: api.CounterIncrementResponse - (*EchoRequest)(nil), // 4: api.EchoRequest - (*EchoResponse)(nil), // 5: api.EchoResponse + (*AuthTokenRequest)(nil), // 0: api.AuthTokenRequest + (*AuthTokenResponse)(nil), // 1: api.AuthTokenResponse + (*CounterGetRequest)(nil), // 2: api.CounterGetRequest + (*CounterGetResponse)(nil), // 3: api.CounterGetResponse + (*CounterIncrementRequest)(nil), // 4: api.CounterIncrementRequest + (*CounterIncrementResponse)(nil), // 5: api.CounterIncrementResponse + (*EchoRequest)(nil), // 6: api.EchoRequest + (*EchoResponse)(nil), // 7: api.EchoResponse } var file_goagen_v1_api_proto_depIdxs = []int32{ - 0, // 0: api.API.CounterGet:input_type -> api.CounterGetRequest - 2, // 1: api.API.CounterIncrement:input_type -> api.CounterIncrementRequest - 4, // 2: api.API.Echo:input_type -> api.EchoRequest - 1, // 3: api.API.CounterGet:output_type -> api.CounterGetResponse - 3, // 4: api.API.CounterIncrement:output_type -> api.CounterIncrementResponse - 5, // 5: api.API.Echo:output_type -> api.EchoResponse - 3, // [3:6] is the sub-list for method output_type - 0, // [0:3] is the sub-list for method input_type + 0, // 0: api.API.AuthToken:input_type -> api.AuthTokenRequest + 2, // 1: api.API.CounterGet:input_type -> api.CounterGetRequest + 4, // 2: api.API.CounterIncrement:input_type -> api.CounterIncrementRequest + 6, // 3: api.API.Echo:input_type -> api.EchoRequest + 1, // 4: api.API.AuthToken:output_type -> api.AuthTokenResponse + 3, // 5: api.API.CounterGet:output_type -> api.CounterGetResponse + 5, // 6: api.API.CounterIncrement:output_type -> api.CounterIncrementResponse + 7, // 7: api.API.Echo:output_type -> api.EchoResponse + 4, // [4:8] is the sub-list for method output_type + 0, // [0:4] is the sub-list for method input_type 0, // [0:0] is the sub-list for extension type_name 0, // [0:0] is the sub-list for extension extendee 0, // [0:0] is the sub-list for field type_name @@ -433,7 +547,7 @@ func file_goagen_v1_api_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_goagen_v1_api_proto_rawDesc, NumEnums: 0, - NumMessages: 6, + NumMessages: 8, NumExtensions: 0, NumServices: 1, }, diff --git a/app/api/v1/gen/grpc/api/pb/goagen_v1_api.proto b/app/api/v1/gen/grpc/api/pb/goagen_v1_api.proto index ec2860b..932a52c 100644 --- a/app/api/v1/gen/grpc/api/pb/goagen_v1_api.proto +++ b/app/api/v1/gen/grpc/api/pb/goagen_v1_api.proto @@ -13,6 +13,8 @@ option go_package = "/apipb"; // Service is the api service interface. service API { + // AuthToken implements AuthToken. + rpc AuthToken (AuthTokenRequest) returns (AuthTokenResponse); // CounterGet implements CounterGet. rpc CounterGet (CounterGetRequest) returns (CounterGetResponse); // CounterIncrement implements CounterIncrement. @@ -21,6 +23,15 @@ service API { rpc Echo (EchoRequest) returns (EchoResponse); } +message AuthTokenRequest { + string provider = 1; + string access_token = 2; +} + +message AuthTokenResponse { + string token = 1; +} + message CounterGetRequest { } @@ -32,7 +43,7 @@ message CounterGetResponse { } message CounterIncrementRequest { - string user = 1; + string user = 2; } message CounterIncrementResponse { diff --git a/app/api/v1/gen/grpc/api/pb/goagen_v1_api_grpc.pb.go b/app/api/v1/gen/grpc/api/pb/goagen_v1_api_grpc.pb.go index eed11d7..fd81604 100644 --- a/app/api/v1/gen/grpc/api/pb/goagen_v1_api_grpc.pb.go +++ b/app/api/v1/gen/grpc/api/pb/goagen_v1_api_grpc.pb.go @@ -26,6 +26,7 @@ import ( const _ = grpc.SupportPackageIsVersion9 const ( + API_AuthToken_FullMethodName = "/api.API/AuthToken" API_CounterGet_FullMethodName = "/api.API/CounterGet" API_CounterIncrement_FullMethodName = "/api.API/CounterIncrement" API_Echo_FullMethodName = "/api.API/Echo" @@ -37,6 +38,8 @@ const ( // // Service is the api service interface. type APIClient interface { + // AuthToken implements AuthToken. + AuthToken(ctx context.Context, in *AuthTokenRequest, opts ...grpc.CallOption) (*AuthTokenResponse, error) // CounterGet implements CounterGet. CounterGet(ctx context.Context, in *CounterGetRequest, opts ...grpc.CallOption) (*CounterGetResponse, error) // CounterIncrement implements CounterIncrement. @@ -53,6 +56,16 @@ func NewAPIClient(cc grpc.ClientConnInterface) APIClient { return &aPIClient{cc} } +func (c *aPIClient) AuthToken(ctx context.Context, in *AuthTokenRequest, opts ...grpc.CallOption) (*AuthTokenResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(AuthTokenResponse) + err := c.cc.Invoke(ctx, API_AuthToken_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + func (c *aPIClient) CounterGet(ctx context.Context, in *CounterGetRequest, opts ...grpc.CallOption) (*CounterGetResponse, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(CounterGetResponse) @@ -89,6 +102,8 @@ func (c *aPIClient) Echo(ctx context.Context, in *EchoRequest, opts ...grpc.Call // // Service is the api service interface. type APIServer interface { + // AuthToken implements AuthToken. + AuthToken(context.Context, *AuthTokenRequest) (*AuthTokenResponse, error) // CounterGet implements CounterGet. CounterGet(context.Context, *CounterGetRequest) (*CounterGetResponse, error) // CounterIncrement implements CounterIncrement. @@ -105,6 +120,9 @@ type APIServer interface { // pointer dereference when methods are called. type UnimplementedAPIServer struct{} +func (UnimplementedAPIServer) AuthToken(context.Context, *AuthTokenRequest) (*AuthTokenResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method AuthToken not implemented") +} func (UnimplementedAPIServer) CounterGet(context.Context, *CounterGetRequest) (*CounterGetResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method CounterGet not implemented") } @@ -135,6 +153,24 @@ func RegisterAPIServer(s grpc.ServiceRegistrar, srv APIServer) { s.RegisterService(&API_ServiceDesc, srv) } +func _API_AuthToken_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(AuthTokenRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(APIServer).AuthToken(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: API_AuthToken_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(APIServer).AuthToken(ctx, req.(*AuthTokenRequest)) + } + return interceptor(ctx, in, info, handler) +} + func _API_CounterGet_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(CounterGetRequest) if err := dec(in); err != nil { @@ -196,6 +232,10 @@ var API_ServiceDesc = grpc.ServiceDesc{ ServiceName: "api.API", HandlerType: (*APIServer)(nil), Methods: []grpc.MethodDesc{ + { + MethodName: "AuthToken", + Handler: _API_AuthToken_Handler, + }, { MethodName: "CounterGet", Handler: _API_CounterGet_Handler, diff --git a/app/api/v1/gen/grpc/api/server/encode_decode.go b/app/api/v1/gen/grpc/api/server/encode_decode.go index 2c40680..8812e2a 100644 --- a/app/api/v1/gen/grpc/api/server/encode_decode.go +++ b/app/api/v1/gen/grpc/api/server/encode_decode.go @@ -9,14 +9,49 @@ package server import ( "context" + "strings" api "github.com/jace-ys/countup/api/v1/gen/api" apiviews "github.com/jace-ys/countup/api/v1/gen/api/views" apipb "github.com/jace-ys/countup/api/v1/gen/grpc/api/pb" goagrpc "goa.design/goa/v3/grpc" + goa "goa.design/goa/v3/pkg" "google.golang.org/grpc/metadata" ) +// EncodeAuthTokenResponse encodes responses from the "api" service "AuthToken" +// endpoint. +func EncodeAuthTokenResponse(ctx context.Context, v any, hdr, trlr *metadata.MD) (any, error) { + result, ok := v.(*api.AuthTokenResult) + if !ok { + return nil, goagrpc.ErrInvalidType("api", "AuthToken", "*api.AuthTokenResult", v) + } + resp := NewProtoAuthTokenResponse(result) + return resp, nil +} + +// DecodeAuthTokenRequest decodes requests sent to "api" service "AuthToken" +// endpoint. +func DecodeAuthTokenRequest(ctx context.Context, v any, md metadata.MD) (any, error) { + var ( + message *apipb.AuthTokenRequest + ok bool + ) + { + if message, ok = v.(*apipb.AuthTokenRequest); !ok { + return nil, goagrpc.ErrInvalidType("api", "AuthToken", "*apipb.AuthTokenRequest", v) + } + if err := ValidateAuthTokenRequest(message); err != nil { + return nil, err + } + } + var payload *api.AuthTokenPayload + { + payload = NewAuthTokenPayload(message) + } + return payload, nil +} + // EncodeCounterGetResponse encodes responses from the "api" service // "CounterGet" endpoint. func EncodeCounterGetResponse(ctx context.Context, v any, hdr, trlr *metadata.MD) (any, error) { @@ -30,6 +65,35 @@ func EncodeCounterGetResponse(ctx context.Context, v any, hdr, trlr *metadata.MD return resp, nil } +// DecodeCounterGetRequest decodes requests sent to "api" service "CounterGet" +// endpoint. +func DecodeCounterGetRequest(ctx context.Context, v any, md metadata.MD) (any, error) { + var ( + token string + err error + ) + { + if vals := md.Get("authorization"); len(vals) == 0 { + err = goa.MergeErrors(err, goa.MissingFieldError("authorization", "metadata")) + } else { + token = vals[0] + } + } + if err != nil { + return nil, err + } + var payload *api.CounterGetPayload + { + payload = NewCounterGetPayload(token) + if strings.Contains(payload.Token, " ") { + // Remove authorization scheme prefix (e.g. "Bearer") + cred := strings.SplitN(payload.Token, " ", 2)[1] + payload.Token = cred + } + } + return payload, nil +} + // EncodeCounterIncrementResponse encodes responses from the "api" service // "CounterIncrement" endpoint. func EncodeCounterIncrementResponse(ctx context.Context, v any, hdr, trlr *metadata.MD) (any, error) { @@ -46,6 +110,20 @@ func EncodeCounterIncrementResponse(ctx context.Context, v any, hdr, trlr *metad // DecodeCounterIncrementRequest decodes requests sent to "api" service // "CounterIncrement" endpoint. func DecodeCounterIncrementRequest(ctx context.Context, v any, md metadata.MD) (any, error) { + var ( + token string + err error + ) + { + if vals := md.Get("authorization"); len(vals) == 0 { + err = goa.MergeErrors(err, goa.MissingFieldError("authorization", "metadata")) + } else { + token = vals[0] + } + } + if err != nil { + return nil, err + } var ( message *apipb.CounterIncrementRequest ok bool @@ -57,7 +135,12 @@ func DecodeCounterIncrementRequest(ctx context.Context, v any, md metadata.MD) ( } var payload *api.CounterIncrementPayload { - payload = NewCounterIncrementPayload(message) + payload = NewCounterIncrementPayload(message, token) + if strings.Contains(payload.Token, " ") { + // Remove authorization scheme prefix (e.g. "Bearer") + cred := strings.SplitN(payload.Token, " ", 2)[1] + payload.Token = cred + } } return payload, nil } diff --git a/app/api/v1/gen/grpc/api/server/server.go b/app/api/v1/gen/grpc/api/server/server.go index dcbceb8..ff831da 100644 --- a/app/api/v1/gen/grpc/api/server/server.go +++ b/app/api/v1/gen/grpc/api/server/server.go @@ -20,6 +20,7 @@ import ( // Server implements the apipb.APIServer interface. type Server struct { + AuthTokenH goagrpc.UnaryHandler CounterGetH goagrpc.UnaryHandler CounterIncrementH goagrpc.UnaryHandler EchoH goagrpc.UnaryHandler @@ -29,17 +30,47 @@ type Server struct { // New instantiates the server struct with the api service endpoints. func New(e *api.Endpoints, uh goagrpc.UnaryHandler) *Server { return &Server{ + AuthTokenH: NewAuthTokenHandler(e.AuthToken, uh), CounterGetH: NewCounterGetHandler(e.CounterGet, uh), CounterIncrementH: NewCounterIncrementHandler(e.CounterIncrement, uh), EchoH: NewEchoHandler(e.Echo, uh), } } +// NewAuthTokenHandler creates a gRPC handler which serves the "api" service +// "AuthToken" endpoint. +func NewAuthTokenHandler(endpoint goa.Endpoint, h goagrpc.UnaryHandler) goagrpc.UnaryHandler { + if h == nil { + h = goagrpc.NewUnaryHandler(endpoint, DecodeAuthTokenRequest, EncodeAuthTokenResponse) + } + return h +} + +// AuthToken implements the "AuthToken" method in apipb.APIServer interface. +func (s *Server) AuthToken(ctx context.Context, message *apipb.AuthTokenRequest) (*apipb.AuthTokenResponse, error) { + ctx = context.WithValue(ctx, goa.MethodKey, "AuthToken") + ctx = context.WithValue(ctx, goa.ServiceKey, "api") + resp, err := s.AuthTokenH.Handle(ctx, message) + if err != nil { + var en goa.GoaErrorNamer + if errors.As(err, &en) { + switch en.GoaErrorName() { + case "unauthorized": + return nil, goagrpc.NewStatusError(codes.PermissionDenied, err, goagrpc.NewErrorResponse(err)) + case "existing_increment_request": + return nil, goagrpc.NewStatusError(codes.AlreadyExists, err, goagrpc.NewErrorResponse(err)) + } + } + return nil, goagrpc.EncodeError(err) + } + return resp.(*apipb.AuthTokenResponse), nil +} + // NewCounterGetHandler creates a gRPC handler which serves the "api" service // "CounterGet" endpoint. func NewCounterGetHandler(endpoint goa.Endpoint, h goagrpc.UnaryHandler) goagrpc.UnaryHandler { if h == nil { - h = goagrpc.NewUnaryHandler(endpoint, nil, EncodeCounterGetResponse) + h = goagrpc.NewUnaryHandler(endpoint, DecodeCounterGetRequest, EncodeCounterGetResponse) } return h } diff --git a/app/api/v1/gen/grpc/api/server/types.go b/app/api/v1/gen/grpc/api/server/types.go index 9b23b3a..be5078b 100644 --- a/app/api/v1/gen/grpc/api/server/types.go +++ b/app/api/v1/gen/grpc/api/server/types.go @@ -11,8 +11,36 @@ import ( api "github.com/jace-ys/countup/api/v1/gen/api" apiviews "github.com/jace-ys/countup/api/v1/gen/api/views" apipb "github.com/jace-ys/countup/api/v1/gen/grpc/api/pb" + goa "goa.design/goa/v3/pkg" ) +// NewAuthTokenPayload builds the payload of the "AuthToken" endpoint of the +// "api" service from the gRPC request type. +func NewAuthTokenPayload(message *apipb.AuthTokenRequest) *api.AuthTokenPayload { + v := &api.AuthTokenPayload{ + Provider: message.Provider, + AccessToken: message.AccessToken, + } + return v +} + +// NewProtoAuthTokenResponse builds the gRPC response type from the result of +// the "AuthToken" endpoint of the "api" service. +func NewProtoAuthTokenResponse(result *api.AuthTokenResult) *apipb.AuthTokenResponse { + message := &apipb.AuthTokenResponse{ + Token: result.Token, + } + return message +} + +// NewCounterGetPayload builds the payload of the "CounterGet" endpoint of the +// "api" service from the gRPC request type. +func NewCounterGetPayload(token string) *api.CounterGetPayload { + v := &api.CounterGetPayload{} + v.Token = token + return v +} + // NewProtoCounterGetResponse builds the gRPC response type from the result of // the "CounterGet" endpoint of the "api" service. func NewProtoCounterGetResponse(result *apiviews.CounterInfoView) *apipb.CounterGetResponse { @@ -27,10 +55,11 @@ func NewProtoCounterGetResponse(result *apiviews.CounterInfoView) *apipb.Counter // NewCounterIncrementPayload builds the payload of the "CounterIncrement" // endpoint of the "api" service from the gRPC request type. -func NewCounterIncrementPayload(message *apipb.CounterIncrementRequest) *api.CounterIncrementPayload { +func NewCounterIncrementPayload(message *apipb.CounterIncrementRequest, token string) *api.CounterIncrementPayload { v := &api.CounterIncrementPayload{ User: message.User, } + v.Token = token return v } @@ -63,3 +92,11 @@ func NewProtoEchoResponse(result *api.EchoResult) *apipb.EchoResponse { } return message } + +// ValidateAuthTokenRequest runs the validations defined on AuthTokenRequest. +func ValidateAuthTokenRequest(message *apipb.AuthTokenRequest) (err error) { + if !(message.Provider == "google") { + err = goa.MergeErrors(err, goa.InvalidEnumValueError("message.provider", message.Provider, []any{"google"})) + } + return +} diff --git a/app/api/v1/gen/grpc/cli/countup/cli.go b/app/api/v1/gen/grpc/cli/countup/cli.go index 232bfc0..d31dc57 100644 --- a/app/api/v1/gen/grpc/cli/countup/cli.go +++ b/app/api/v1/gen/grpc/cli/countup/cli.go @@ -21,13 +21,16 @@ import ( // // command (subcommand1|subcommand2|...) func UsageCommands() string { - return `api (counter-get|counter-increment|echo) + return `api (auth-token|counter-get|counter-increment|echo) ` } // UsageExamples produces an example of a valid invocation of the CLI tool. func UsageExamples() string { - return os.Args[0] + ` api counter-get` + "\n" + + return os.Args[0] + ` api auth-token --message '{ + "access_token": "Et eum.", + "provider": "google" + }'` + "\n" + "" } @@ -37,15 +40,21 @@ func ParseEndpoint(cc *grpc.ClientConn, opts ...grpc.CallOption) (goa.Endpoint, var ( apiFlags = flag.NewFlagSet("api", flag.ContinueOnError) - apiCounterGetFlags = flag.NewFlagSet("counter-get", flag.ExitOnError) + apiAuthTokenFlags = flag.NewFlagSet("auth-token", flag.ExitOnError) + apiAuthTokenMessageFlag = apiAuthTokenFlags.String("message", "", "") + + apiCounterGetFlags = flag.NewFlagSet("counter-get", flag.ExitOnError) + apiCounterGetTokenFlag = apiCounterGetFlags.String("token", "REQUIRED", "") apiCounterIncrementFlags = flag.NewFlagSet("counter-increment", flag.ExitOnError) apiCounterIncrementMessageFlag = apiCounterIncrementFlags.String("message", "", "") + apiCounterIncrementTokenFlag = apiCounterIncrementFlags.String("token", "REQUIRED", "") apiEchoFlags = flag.NewFlagSet("echo", flag.ExitOnError) apiEchoMessageFlag = apiEchoFlags.String("message", "", "") ) apiFlags.Usage = apiUsage + apiAuthTokenFlags.Usage = apiAuthTokenUsage apiCounterGetFlags.Usage = apiCounterGetUsage apiCounterIncrementFlags.Usage = apiCounterIncrementUsage apiEchoFlags.Usage = apiEchoUsage @@ -84,6 +93,9 @@ func ParseEndpoint(cc *grpc.ClientConn, opts ...grpc.CallOption) (goa.Endpoint, switch svcn { case "api": switch epn { + case "auth-token": + epf = apiAuthTokenFlags + case "counter-get": epf = apiCounterGetFlags @@ -118,11 +130,15 @@ func ParseEndpoint(cc *grpc.ClientConn, opts ...grpc.CallOption) (goa.Endpoint, case "api": c := apic.NewClient(cc, opts...) switch epn { + case "auth-token": + endpoint = c.AuthToken() + data, err = apic.BuildAuthTokenPayload(*apiAuthTokenMessageFlag) case "counter-get": endpoint = c.CounterGet() + data, err = apic.BuildCounterGetPayload(*apiCounterGetTokenFlag) case "counter-increment": endpoint = c.CounterIncrement() - data, err = apic.BuildCounterIncrementPayload(*apiCounterIncrementMessageFlag) + data, err = apic.BuildCounterIncrementPayload(*apiCounterIncrementMessageFlag, *apiCounterIncrementTokenFlag) case "echo": endpoint = c.Echo() data, err = apic.BuildEchoPayload(*apiEchoMessageFlag) @@ -141,6 +157,7 @@ Usage: %[1]s [globalflags] api COMMAND [flags] COMMAND: + auth-token: AuthToken implements AuthToken. counter-get: CounterGet implements CounterGet. counter-increment: CounterIncrement implements CounterIncrement. echo: Echo implements Echo. @@ -149,26 +166,42 @@ Additional help: %[1]s api COMMAND --help `, os.Args[0]) } +func apiAuthTokenUsage() { + fmt.Fprintf(os.Stderr, `%[1]s [flags] api auth-token -message JSON + +AuthToken implements AuthToken. + -message JSON: + +Example: + %[1]s api auth-token --message '{ + "access_token": "Et eum.", + "provider": "google" + }' +`, os.Args[0]) +} + func apiCounterGetUsage() { - fmt.Fprintf(os.Stderr, `%[1]s [flags] api counter-get + fmt.Fprintf(os.Stderr, `%[1]s [flags] api counter-get -token STRING CounterGet implements CounterGet. + -token STRING: Example: - %[1]s api counter-get + %[1]s api counter-get --token "Qui voluptatum omnis." `, os.Args[0]) } func apiCounterIncrementUsage() { - fmt.Fprintf(os.Stderr, `%[1]s [flags] api counter-increment -message JSON + fmt.Fprintf(os.Stderr, `%[1]s [flags] api counter-increment -message JSON -token STRING CounterIncrement implements CounterIncrement. -message JSON: + -token STRING: Example: %[1]s api counter-increment --message '{ - "user": "Non accusantium eos culpa autem illum architecto." - }' + "user": "Voluptates voluptas eius cumque maxime dolore." + }' --token "Minima dolorem id." `, os.Args[0]) } @@ -180,7 +213,7 @@ Echo implements Echo. Example: %[1]s api echo --message '{ - "text": "Cupiditate tempore harum error iste ipsam natus." + "text": "Quidem inventore." }' `, os.Args[0]) } diff --git a/app/api/v1/gen/http/api/client/cli.go b/app/api/v1/gen/http/api/client/cli.go index 58489ba..7ba5259 100644 --- a/app/api/v1/gen/http/api/client/cli.go +++ b/app/api/v1/gen/http/api/client/cli.go @@ -12,22 +12,66 @@ import ( "fmt" api "github.com/jace-ys/countup/api/v1/gen/api" + goa "goa.design/goa/v3/pkg" ) +// BuildAuthTokenPayload builds the payload for the api AuthToken endpoint from +// CLI flags. +func BuildAuthTokenPayload(apiAuthTokenBody string) (*api.AuthTokenPayload, error) { + var err error + var body AuthTokenRequestBody + { + err = json.Unmarshal([]byte(apiAuthTokenBody), &body) + if err != nil { + return nil, fmt.Errorf("invalid JSON for body, \nerror: %s, \nexample of valid JSON:\n%s", err, "'{\n \"access_token\": \"Omnis cumque est asperiores dolorem.\",\n \"provider\": \"google\"\n }'") + } + if !(body.Provider == "google") { + err = goa.MergeErrors(err, goa.InvalidEnumValueError("body.provider", body.Provider, []any{"google"})) + } + if err != nil { + return nil, err + } + } + v := &api.AuthTokenPayload{ + Provider: body.Provider, + AccessToken: body.AccessToken, + } + + return v, nil +} + +// BuildCounterGetPayload builds the payload for the api CounterGet endpoint +// from CLI flags. +func BuildCounterGetPayload(apiCounterGetToken string) (*api.CounterGetPayload, error) { + var token string + { + token = apiCounterGetToken + } + v := &api.CounterGetPayload{} + v.Token = token + + return v, nil +} + // BuildCounterIncrementPayload builds the payload for the api CounterIncrement // endpoint from CLI flags. -func BuildCounterIncrementPayload(apiCounterIncrementBody string) (*api.CounterIncrementPayload, error) { +func BuildCounterIncrementPayload(apiCounterIncrementBody string, apiCounterIncrementToken string) (*api.CounterIncrementPayload, error) { var err error var body CounterIncrementRequestBody { err = json.Unmarshal([]byte(apiCounterIncrementBody), &body) if err != nil { - return nil, fmt.Errorf("invalid JSON for body, \nerror: %s, \nexample of valid JSON:\n%s", err, "'{\n \"user\": \"Nihil doloribus et sed sequi consequatur.\"\n }'") + return nil, fmt.Errorf("invalid JSON for body, \nerror: %s, \nexample of valid JSON:\n%s", err, "'{\n \"user\": \"Quae cupiditate.\"\n }'") } } + var token string + { + token = apiCounterIncrementToken + } v := &api.CounterIncrementPayload{ User: body.User, } + v.Token = token return v, nil } @@ -39,7 +83,7 @@ func BuildEchoPayload(apiEchoBody string) (*api.EchoPayload, error) { { err = json.Unmarshal([]byte(apiEchoBody), &body) if err != nil { - return nil, fmt.Errorf("invalid JSON for body, \nerror: %s, \nexample of valid JSON:\n%s", err, "'{\n \"text\": \"Vel omnis quo sit.\"\n }'") + return nil, fmt.Errorf("invalid JSON for body, \nerror: %s, \nexample of valid JSON:\n%s", err, "'{\n \"text\": \"Tempora repellendus.\"\n }'") } } v := &api.EchoPayload{ diff --git a/app/api/v1/gen/http/api/client/client.go b/app/api/v1/gen/http/api/client/client.go index 6de9935..3fc170a 100644 --- a/app/api/v1/gen/http/api/client/client.go +++ b/app/api/v1/gen/http/api/client/client.go @@ -17,6 +17,10 @@ import ( // Client lists the api service endpoint HTTP clients. type Client struct { + // AuthToken Doer is the HTTP client used to make requests to the AuthToken + // endpoint. + AuthTokenDoer goahttp.Doer + // CounterGet Doer is the HTTP client used to make requests to the CounterGet // endpoint. CounterGetDoer goahttp.Doer @@ -48,6 +52,7 @@ func NewClient( restoreBody bool, ) *Client { return &Client{ + AuthTokenDoer: doer, CounterGetDoer: doer, CounterIncrementDoer: doer, EchoDoer: doer, @@ -59,10 +64,35 @@ func NewClient( } } +// AuthToken returns an endpoint that makes HTTP requests to the api service +// AuthToken server. +func (c *Client) AuthToken() goa.Endpoint { + var ( + encodeRequest = EncodeAuthTokenRequest(c.encoder) + decodeResponse = DecodeAuthTokenResponse(c.decoder, c.RestoreResponseBody) + ) + return func(ctx context.Context, v any) (any, error) { + req, err := c.BuildAuthTokenRequest(ctx, v) + if err != nil { + return nil, err + } + err = encodeRequest(req, v) + if err != nil { + return nil, err + } + resp, err := c.AuthTokenDoer.Do(req) + if err != nil { + return nil, goahttp.ErrRequestError("api", "AuthToken", err) + } + return decodeResponse(resp) + } +} + // CounterGet returns an endpoint that makes HTTP requests to the api service // CounterGet server. func (c *Client) CounterGet() goa.Endpoint { var ( + encodeRequest = EncodeCounterGetRequest(c.encoder) decodeResponse = DecodeCounterGetResponse(c.decoder, c.RestoreResponseBody) ) return func(ctx context.Context, v any) (any, error) { @@ -70,6 +100,10 @@ func (c *Client) CounterGet() goa.Endpoint { if err != nil { return nil, err } + err = encodeRequest(req, v) + if err != nil { + return nil, err + } resp, err := c.CounterGetDoer.Do(req) if err != nil { return nil, goahttp.ErrRequestError("api", "CounterGet", err) diff --git a/app/api/v1/gen/http/api/client/encode_decode.go b/app/api/v1/gen/http/api/client/encode_decode.go index dd511c9..d2b563f 100644 --- a/app/api/v1/gen/http/api/client/encode_decode.go +++ b/app/api/v1/gen/http/api/client/encode_decode.go @@ -13,12 +13,116 @@ import ( "io" "net/http" "net/url" + "strings" api "github.com/jace-ys/countup/api/v1/gen/api" apiviews "github.com/jace-ys/countup/api/v1/gen/api/views" goahttp "goa.design/goa/v3/http" ) +// BuildAuthTokenRequest instantiates a HTTP request object with method and +// path set to call the "api" service "AuthToken" endpoint +func (c *Client) BuildAuthTokenRequest(ctx context.Context, v any) (*http.Request, error) { + u := &url.URL{Scheme: c.scheme, Host: c.host, Path: AuthTokenAPIPath()} + req, err := http.NewRequest("POST", u.String(), nil) + if err != nil { + return nil, goahttp.ErrInvalidURL("api", "AuthToken", u.String(), err) + } + if ctx != nil { + req = req.WithContext(ctx) + } + + return req, nil +} + +// EncodeAuthTokenRequest returns an encoder for requests sent to the api +// AuthToken server. +func EncodeAuthTokenRequest(encoder func(*http.Request) goahttp.Encoder) func(*http.Request, any) error { + return func(req *http.Request, v any) error { + p, ok := v.(*api.AuthTokenPayload) + if !ok { + return goahttp.ErrInvalidType("api", "AuthToken", "*api.AuthTokenPayload", v) + } + body := NewAuthTokenRequestBody(p) + if err := encoder(req).Encode(&body); err != nil { + return goahttp.ErrEncodingError("api", "AuthToken", err) + } + return nil + } +} + +// DecodeAuthTokenResponse returns a decoder for responses returned by the api +// AuthToken endpoint. restoreBody controls whether the response body should be +// restored after having been read. +// DecodeAuthTokenResponse may return the following errors: +// - "unauthorized" (type *goa.ServiceError): http.StatusUnauthorized +// - "existing_increment_request" (type *goa.ServiceError): http.StatusTooManyRequests +// - error: internal error +func DecodeAuthTokenResponse(decoder func(*http.Response) goahttp.Decoder, restoreBody bool) func(*http.Response) (any, error) { + return func(resp *http.Response) (any, error) { + if restoreBody { + b, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + resp.Body = io.NopCloser(bytes.NewBuffer(b)) + defer func() { + resp.Body = io.NopCloser(bytes.NewBuffer(b)) + }() + } else { + defer resp.Body.Close() + } + switch resp.StatusCode { + case http.StatusOK: + var ( + body AuthTokenResponseBody + err error + ) + err = decoder(resp).Decode(&body) + if err != nil { + return nil, goahttp.ErrDecodingError("api", "AuthToken", err) + } + err = ValidateAuthTokenResponseBody(&body) + if err != nil { + return nil, goahttp.ErrValidationError("api", "AuthToken", err) + } + res := NewAuthTokenResultOK(&body) + return res, nil + case http.StatusUnauthorized: + var ( + body AuthTokenUnauthorizedResponseBody + err error + ) + err = decoder(resp).Decode(&body) + if err != nil { + return nil, goahttp.ErrDecodingError("api", "AuthToken", err) + } + err = ValidateAuthTokenUnauthorizedResponseBody(&body) + if err != nil { + return nil, goahttp.ErrValidationError("api", "AuthToken", err) + } + return nil, NewAuthTokenUnauthorized(&body) + case http.StatusTooManyRequests: + var ( + body AuthTokenExistingIncrementRequestResponseBody + err error + ) + err = decoder(resp).Decode(&body) + if err != nil { + return nil, goahttp.ErrDecodingError("api", "AuthToken", err) + } + err = ValidateAuthTokenExistingIncrementRequestResponseBody(&body) + if err != nil { + return nil, goahttp.ErrValidationError("api", "AuthToken", err) + } + return nil, NewAuthTokenExistingIncrementRequest(&body) + default: + body, _ := io.ReadAll(resp.Body) + return nil, goahttp.ErrInvalidResponse("api", "AuthToken", resp.StatusCode, string(body)) + } + } +} + // BuildCounterGetRequest instantiates a HTTP request object with method and // path set to call the "api" service "CounterGet" endpoint func (c *Client) BuildCounterGetRequest(ctx context.Context, v any) (*http.Request, error) { @@ -34,6 +138,26 @@ func (c *Client) BuildCounterGetRequest(ctx context.Context, v any) (*http.Reque return req, nil } +// EncodeCounterGetRequest returns an encoder for requests sent to the api +// CounterGet server. +func EncodeCounterGetRequest(encoder func(*http.Request) goahttp.Encoder) func(*http.Request, any) error { + return func(req *http.Request, v any) error { + p, ok := v.(*api.CounterGetPayload) + if !ok { + return goahttp.ErrInvalidType("api", "CounterGet", "*api.CounterGetPayload", v) + } + { + head := p.Token + if !strings.Contains(head, " ") { + req.Header.Set("Authorization", "Bearer "+head) + } else { + req.Header.Set("Authorization", head) + } + } + return nil + } +} + // DecodeCounterGetResponse returns a decoder for responses returned by the api // CounterGet endpoint. restoreBody controls whether the response body should // be restored after having been read. @@ -131,6 +255,14 @@ func EncodeCounterIncrementRequest(encoder func(*http.Request) goahttp.Encoder) if !ok { return goahttp.ErrInvalidType("api", "CounterIncrement", "*api.CounterIncrementPayload", v) } + { + head := p.Token + if !strings.Contains(head, " ") { + req.Header.Set("Authorization", "Bearer "+head) + } else { + req.Header.Set("Authorization", head) + } + } body := NewCounterIncrementRequestBody(p) if err := encoder(req).Encode(&body); err != nil { return goahttp.ErrEncodingError("api", "CounterIncrement", err) diff --git a/app/api/v1/gen/http/api/client/paths.go b/app/api/v1/gen/http/api/client/paths.go index 0f1045e..6fc6e8c 100644 --- a/app/api/v1/gen/http/api/client/paths.go +++ b/app/api/v1/gen/http/api/client/paths.go @@ -7,17 +7,22 @@ package client +// AuthTokenAPIPath returns the URL path to the api service AuthToken HTTP endpoint. +func AuthTokenAPIPath() string { + return "/api/v1/auth/token" +} + // CounterGetAPIPath returns the URL path to the api service CounterGet HTTP endpoint. func CounterGetAPIPath() string { - return "/counter" + return "/api/v1/counter" } // CounterIncrementAPIPath returns the URL path to the api service CounterIncrement HTTP endpoint. func CounterIncrementAPIPath() string { - return "/counter/inc" + return "/api/v1/counter" } // EchoAPIPath returns the URL path to the api service Echo HTTP endpoint. func EchoAPIPath() string { - return "/echo" + return "/api/v1/echo" } diff --git a/app/api/v1/gen/http/api/client/types.go b/app/api/v1/gen/http/api/client/types.go index a92300a..1595e88 100644 --- a/app/api/v1/gen/http/api/client/types.go +++ b/app/api/v1/gen/http/api/client/types.go @@ -13,6 +13,13 @@ import ( goa "goa.design/goa/v3/pkg" ) +// AuthTokenRequestBody is the type of the "api" service "AuthToken" endpoint +// HTTP request body. +type AuthTokenRequestBody struct { + Provider string `form:"provider" json:"provider" xml:"provider"` + AccessToken string `form:"access_token" json:"access_token" xml:"access_token"` +} + // CounterIncrementRequestBody is the type of the "api" service // "CounterIncrement" endpoint HTTP request body. type CounterIncrementRequestBody struct { @@ -25,6 +32,12 @@ type EchoRequestBody struct { Text string `form:"text" json:"text" xml:"text"` } +// AuthTokenResponseBody is the type of the "api" service "AuthToken" endpoint +// HTTP response body. +type AuthTokenResponseBody struct { + Token *string `form:"token,omitempty" json:"token,omitempty" xml:"token,omitempty"` +} + // CounterGetResponseBody is the type of the "api" service "CounterGet" // endpoint HTTP response body. type CounterGetResponseBody struct { @@ -49,6 +62,43 @@ type EchoResponseBody struct { Text *string `form:"text,omitempty" json:"text,omitempty" xml:"text,omitempty"` } +// AuthTokenUnauthorizedResponseBody is the type of the "api" service +// "AuthToken" endpoint HTTP response body for the "unauthorized" error. +type AuthTokenUnauthorizedResponseBody struct { + // Name is the name of this class of errors. + Name *string `form:"name,omitempty" json:"name,omitempty" xml:"name,omitempty"` + // ID is a unique identifier for this particular occurrence of the problem. + ID *string `form:"id,omitempty" json:"id,omitempty" xml:"id,omitempty"` + // Message is a human-readable explanation specific to this occurrence of the + // problem. + Message *string `form:"message,omitempty" json:"message,omitempty" xml:"message,omitempty"` + // Is the error temporary? + Temporary *bool `form:"temporary,omitempty" json:"temporary,omitempty" xml:"temporary,omitempty"` + // Is the error a timeout? + Timeout *bool `form:"timeout,omitempty" json:"timeout,omitempty" xml:"timeout,omitempty"` + // Is the error a server-side fault? + Fault *bool `form:"fault,omitempty" json:"fault,omitempty" xml:"fault,omitempty"` +} + +// AuthTokenExistingIncrementRequestResponseBody is the type of the "api" +// service "AuthToken" endpoint HTTP response body for the +// "existing_increment_request" error. +type AuthTokenExistingIncrementRequestResponseBody struct { + // Name is the name of this class of errors. + Name *string `form:"name,omitempty" json:"name,omitempty" xml:"name,omitempty"` + // ID is a unique identifier for this particular occurrence of the problem. + ID *string `form:"id,omitempty" json:"id,omitempty" xml:"id,omitempty"` + // Message is a human-readable explanation specific to this occurrence of the + // problem. + Message *string `form:"message,omitempty" json:"message,omitempty" xml:"message,omitempty"` + // Is the error temporary? + Temporary *bool `form:"temporary,omitempty" json:"temporary,omitempty" xml:"temporary,omitempty"` + // Is the error a timeout? + Timeout *bool `form:"timeout,omitempty" json:"timeout,omitempty" xml:"timeout,omitempty"` + // Is the error a server-side fault? + Fault *bool `form:"fault,omitempty" json:"fault,omitempty" xml:"fault,omitempty"` +} + // CounterGetUnauthorizedResponseBody is the type of the "api" service // "CounterGet" endpoint HTTP response body for the "unauthorized" error. type CounterGetUnauthorizedResponseBody struct { @@ -160,6 +210,16 @@ type EchoExistingIncrementRequestResponseBody struct { Fault *bool `form:"fault,omitempty" json:"fault,omitempty" xml:"fault,omitempty"` } +// NewAuthTokenRequestBody builds the HTTP request body from the payload of the +// "AuthToken" endpoint of the "api" service. +func NewAuthTokenRequestBody(p *api.AuthTokenPayload) *AuthTokenRequestBody { + body := &AuthTokenRequestBody{ + Provider: p.Provider, + AccessToken: p.AccessToken, + } + return body +} + // NewCounterIncrementRequestBody builds the HTTP request body from the payload // of the "CounterIncrement" endpoint of the "api" service. func NewCounterIncrementRequestBody(p *api.CounterIncrementPayload) *CounterIncrementRequestBody { @@ -178,6 +238,46 @@ func NewEchoRequestBody(p *api.EchoPayload) *EchoRequestBody { return body } +// NewAuthTokenResultOK builds a "api" service "AuthToken" endpoint result from +// a HTTP "OK" response. +func NewAuthTokenResultOK(body *AuthTokenResponseBody) *api.AuthTokenResult { + v := &api.AuthTokenResult{ + Token: *body.Token, + } + + return v +} + +// NewAuthTokenUnauthorized builds a api service AuthToken endpoint +// unauthorized error. +func NewAuthTokenUnauthorized(body *AuthTokenUnauthorizedResponseBody) *goa.ServiceError { + v := &goa.ServiceError{ + Name: *body.Name, + ID: *body.ID, + Message: *body.Message, + Temporary: *body.Temporary, + Timeout: *body.Timeout, + Fault: *body.Fault, + } + + return v +} + +// NewAuthTokenExistingIncrementRequest builds a api service AuthToken endpoint +// existing_increment_request error. +func NewAuthTokenExistingIncrementRequest(body *AuthTokenExistingIncrementRequestResponseBody) *goa.ServiceError { + v := &goa.ServiceError{ + Name: *body.Name, + ID: *body.ID, + Message: *body.Message, + Temporary: *body.Temporary, + Timeout: *body.Timeout, + Fault: *body.Fault, + } + + return v +} + // NewCounterGetCounterInfoOK builds a "api" service "CounterGet" endpoint // result from a HTTP "OK" response. func NewCounterGetCounterInfoOK(body *CounterGetResponseBody) *apiviews.CounterInfoView { @@ -303,6 +403,15 @@ func NewEchoExistingIncrementRequest(body *EchoExistingIncrementRequestResponseB return v } +// ValidateAuthTokenResponseBody runs the validations defined on +// AuthTokenResponseBody +func ValidateAuthTokenResponseBody(body *AuthTokenResponseBody) (err error) { + if body.Token == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("token", "body")) + } + return +} + // ValidateEchoResponseBody runs the validations defined on EchoResponseBody func ValidateEchoResponseBody(body *EchoResponseBody) (err error) { if body.Text == nil { @@ -311,6 +420,54 @@ func ValidateEchoResponseBody(body *EchoResponseBody) (err error) { return } +// ValidateAuthTokenUnauthorizedResponseBody runs the validations defined on +// AuthToken_unauthorized_Response_Body +func ValidateAuthTokenUnauthorizedResponseBody(body *AuthTokenUnauthorizedResponseBody) (err error) { + if body.Name == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("name", "body")) + } + if body.ID == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("id", "body")) + } + if body.Message == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("message", "body")) + } + if body.Temporary == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("temporary", "body")) + } + if body.Timeout == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("timeout", "body")) + } + if body.Fault == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("fault", "body")) + } + return +} + +// ValidateAuthTokenExistingIncrementRequestResponseBody runs the validations +// defined on AuthToken_existing_increment_request_Response_Body +func ValidateAuthTokenExistingIncrementRequestResponseBody(body *AuthTokenExistingIncrementRequestResponseBody) (err error) { + if body.Name == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("name", "body")) + } + if body.ID == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("id", "body")) + } + if body.Message == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("message", "body")) + } + if body.Temporary == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("temporary", "body")) + } + if body.Timeout == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("timeout", "body")) + } + if body.Fault == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("fault", "body")) + } + return +} + // ValidateCounterGetUnauthorizedResponseBody runs the validations defined on // CounterGet_unauthorized_Response_Body func ValidateCounterGetUnauthorizedResponseBody(body *CounterGetUnauthorizedResponseBody) (err error) { diff --git a/app/api/v1/gen/http/api/server/encode_decode.go b/app/api/v1/gen/http/api/server/encode_decode.go index 6e7545a..5f2113e 100644 --- a/app/api/v1/gen/http/api/server/encode_decode.go +++ b/app/api/v1/gen/http/api/server/encode_decode.go @@ -12,6 +12,7 @@ import ( "errors" "io" "net/http" + "strings" api "github.com/jace-ys/countup/api/v1/gen/api" apiviews "github.com/jace-ys/countup/api/v1/gen/api/views" @@ -19,6 +20,89 @@ import ( goa "goa.design/goa/v3/pkg" ) +// EncodeAuthTokenResponse returns an encoder for responses returned by the api +// AuthToken endpoint. +func EncodeAuthTokenResponse(encoder func(context.Context, http.ResponseWriter) goahttp.Encoder) func(context.Context, http.ResponseWriter, any) error { + return func(ctx context.Context, w http.ResponseWriter, v any) error { + res, _ := v.(*api.AuthTokenResult) + enc := encoder(ctx, w) + body := NewAuthTokenResponseBody(res) + w.WriteHeader(http.StatusOK) + return enc.Encode(body) + } +} + +// DecodeAuthTokenRequest returns a decoder for requests sent to the api +// AuthToken endpoint. +func DecodeAuthTokenRequest(mux goahttp.Muxer, decoder func(*http.Request) goahttp.Decoder) func(*http.Request) (any, error) { + return func(r *http.Request) (any, error) { + var ( + body AuthTokenRequestBody + err error + ) + err = decoder(r).Decode(&body) + if err != nil { + if err == io.EOF { + return nil, goa.MissingPayloadError() + } + var gerr *goa.ServiceError + if errors.As(err, &gerr) { + return nil, gerr + } + return nil, goa.DecodePayloadError(err.Error()) + } + err = ValidateAuthTokenRequestBody(&body) + if err != nil { + return nil, err + } + payload := NewAuthTokenPayload(&body) + + return payload, nil + } +} + +// EncodeAuthTokenError returns an encoder for errors returned by the AuthToken +// api endpoint. +func EncodeAuthTokenError(encoder func(context.Context, http.ResponseWriter) goahttp.Encoder, formatter func(ctx context.Context, err error) goahttp.Statuser) func(context.Context, http.ResponseWriter, error) error { + encodeError := goahttp.ErrorEncoder(encoder, formatter) + return func(ctx context.Context, w http.ResponseWriter, v error) error { + var en goa.GoaErrorNamer + if !errors.As(v, &en) { + return encodeError(ctx, w, v) + } + switch en.GoaErrorName() { + case "unauthorized": + var res *goa.ServiceError + errors.As(v, &res) + enc := encoder(ctx, w) + var body any + if formatter != nil { + body = formatter(ctx, res) + } else { + body = NewAuthTokenUnauthorizedResponseBody(res) + } + w.Header().Set("goa-error", res.GoaErrorName()) + w.WriteHeader(http.StatusUnauthorized) + return enc.Encode(body) + case "existing_increment_request": + var res *goa.ServiceError + errors.As(v, &res) + enc := encoder(ctx, w) + var body any + if formatter != nil { + body = formatter(ctx, res) + } else { + body = NewAuthTokenExistingIncrementRequestResponseBody(res) + } + w.Header().Set("goa-error", res.GoaErrorName()) + w.WriteHeader(http.StatusTooManyRequests) + return enc.Encode(body) + default: + return encodeError(ctx, w, v) + } + } +} + // EncodeCounterGetResponse returns an encoder for responses returned by the // api CounterGet endpoint. func EncodeCounterGetResponse(encoder func(context.Context, http.ResponseWriter) goahttp.Encoder) func(context.Context, http.ResponseWriter, any) error { @@ -31,6 +115,32 @@ func EncodeCounterGetResponse(encoder func(context.Context, http.ResponseWriter) } } +// DecodeCounterGetRequest returns a decoder for requests sent to the api +// CounterGet endpoint. +func DecodeCounterGetRequest(mux goahttp.Muxer, decoder func(*http.Request) goahttp.Decoder) func(*http.Request) (any, error) { + return func(r *http.Request) (any, error) { + var ( + token string + err error + ) + token = r.Header.Get("Authorization") + if token == "" { + err = goa.MergeErrors(err, goa.MissingFieldError("token", "header")) + } + if err != nil { + return nil, err + } + payload := NewCounterGetPayload(token) + if strings.Contains(payload.Token, " ") { + // Remove authorization scheme prefix (e.g. "Bearer") + cred := strings.SplitN(payload.Token, " ", 2)[1] + payload.Token = cred + } + + return payload, nil + } +} + // EncodeCounterGetError returns an encoder for errors returned by the // CounterGet api endpoint. func EncodeCounterGetError(encoder func(context.Context, http.ResponseWriter) goahttp.Encoder, formatter func(ctx context.Context, err error) goahttp.Statuser) func(context.Context, http.ResponseWriter, error) error { @@ -108,7 +218,23 @@ func DecodeCounterIncrementRequest(mux goahttp.Muxer, decoder func(*http.Request if err != nil { return nil, err } - payload := NewCounterIncrementPayload(&body) + + var ( + token string + ) + token = r.Header.Get("Authorization") + if token == "" { + err = goa.MergeErrors(err, goa.MissingFieldError("token", "header")) + } + if err != nil { + return nil, err + } + payload := NewCounterIncrementPayload(&body, token) + if strings.Contains(payload.Token, " ") { + // Remove authorization scheme prefix (e.g. "Bearer") + cred := strings.SplitN(payload.Token, " ", 2)[1] + payload.Token = cred + } return payload, nil } diff --git a/app/api/v1/gen/http/api/server/paths.go b/app/api/v1/gen/http/api/server/paths.go index e1aae0e..5ba3287 100644 --- a/app/api/v1/gen/http/api/server/paths.go +++ b/app/api/v1/gen/http/api/server/paths.go @@ -7,17 +7,22 @@ package server +// AuthTokenAPIPath returns the URL path to the api service AuthToken HTTP endpoint. +func AuthTokenAPIPath() string { + return "/api/v1/auth/token" +} + // CounterGetAPIPath returns the URL path to the api service CounterGet HTTP endpoint. func CounterGetAPIPath() string { - return "/counter" + return "/api/v1/counter" } // CounterIncrementAPIPath returns the URL path to the api service CounterIncrement HTTP endpoint. func CounterIncrementAPIPath() string { - return "/counter/inc" + return "/api/v1/counter" } // EchoAPIPath returns the URL path to the api service Echo HTTP endpoint. func EchoAPIPath() string { - return "/echo" + return "/api/v1/echo" } diff --git a/app/api/v1/gen/http/api/server/server.go b/app/api/v1/gen/http/api/server/server.go index bb690b0..7391fe4 100644 --- a/app/api/v1/gen/http/api/server/server.go +++ b/app/api/v1/gen/http/api/server/server.go @@ -20,6 +20,7 @@ import ( // Server lists the api service endpoint HTTP handlers. type Server struct { Mounts []*MountPoint + AuthToken http.Handler CounterGet http.Handler CounterIncrement http.Handler Echo http.Handler @@ -58,11 +59,13 @@ func New( fileSystemGenHTTPOpenapi3JSON = appendPrefix(fileSystemGenHTTPOpenapi3JSON, "/gen/http") return &Server{ Mounts: []*MountPoint{ - {"CounterGet", "GET", "/counter"}, - {"CounterIncrement", "POST", "/counter/inc"}, - {"Echo", "POST", "/echo"}, - {"Serve gen/http/openapi3.json", "GET", "/openapi.json"}, + {"AuthToken", "POST", "/api/v1/auth/token"}, + {"CounterGet", "GET", "/api/v1/counter"}, + {"CounterIncrement", "POST", "/api/v1/counter"}, + {"Echo", "POST", "/api/v1/echo"}, + {"Serve gen/http/openapi3.json", "GET", "/api/v1/openapi.json"}, }, + AuthToken: NewAuthTokenHandler(e.AuthToken, mux, decoder, encoder, errhandler, formatter), CounterGet: NewCounterGetHandler(e.CounterGet, mux, decoder, encoder, errhandler, formatter), CounterIncrement: NewCounterIncrementHandler(e.CounterIncrement, mux, decoder, encoder, errhandler, formatter), Echo: NewEchoHandler(e.Echo, mux, decoder, encoder, errhandler, formatter), @@ -75,6 +78,7 @@ func (s *Server) Service() string { return "api" } // Use wraps the server handlers with the given middleware. func (s *Server) Use(m func(http.Handler) http.Handler) { + s.AuthToken = m(s.AuthToken) s.CounterGet = m(s.CounterGet) s.CounterIncrement = m(s.CounterIncrement) s.Echo = m(s.Echo) @@ -85,10 +89,11 @@ func (s *Server) MethodNames() []string { return api.MethodNames[:] } // Mount configures the mux to serve the api endpoints. func Mount(mux goahttp.Muxer, h *Server) { + MountAuthTokenHandler(mux, h.AuthToken) MountCounterGetHandler(mux, h.CounterGet) MountCounterIncrementHandler(mux, h.CounterIncrement) MountEchoHandler(mux, h.Echo) - MountGenHTTPOpenapi3JSON(mux, h.GenHTTPOpenapi3JSON) + MountGenHTTPOpenapi3JSON(mux, http.StripPrefix("/api/v1", h.GenHTTPOpenapi3JSON)) } // Mount configures the mux to serve the api endpoints. @@ -96,6 +101,57 @@ func (s *Server) Mount(mux goahttp.Muxer) { Mount(mux, s) } +// MountAuthTokenHandler configures the mux to serve the "api" service +// "AuthToken" endpoint. +func MountAuthTokenHandler(mux goahttp.Muxer, h http.Handler) { + f, ok := h.(http.HandlerFunc) + if !ok { + f = func(w http.ResponseWriter, r *http.Request) { + h.ServeHTTP(w, r) + } + } + mux.Handle("POST", "/api/v1/auth/token", f) +} + +// NewAuthTokenHandler creates a HTTP handler which loads the HTTP request and +// calls the "api" service "AuthToken" endpoint. +func NewAuthTokenHandler( + endpoint goa.Endpoint, + mux goahttp.Muxer, + decoder func(*http.Request) goahttp.Decoder, + encoder func(context.Context, http.ResponseWriter) goahttp.Encoder, + errhandler func(context.Context, http.ResponseWriter, error), + formatter func(ctx context.Context, err error) goahttp.Statuser, +) http.Handler { + var ( + decodeRequest = DecodeAuthTokenRequest(mux, decoder) + encodeResponse = EncodeAuthTokenResponse(encoder) + encodeError = EncodeAuthTokenError(encoder, formatter) + ) + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ctx := context.WithValue(r.Context(), goahttp.AcceptTypeKey, r.Header.Get("Accept")) + ctx = context.WithValue(ctx, goa.MethodKey, "AuthToken") + ctx = context.WithValue(ctx, goa.ServiceKey, "api") + payload, err := decodeRequest(r) + if err != nil { + if err := encodeError(ctx, w, err); err != nil { + errhandler(ctx, w, err) + } + return + } + res, err := endpoint(ctx, payload) + if err != nil { + if err := encodeError(ctx, w, err); err != nil { + errhandler(ctx, w, err) + } + return + } + if err := encodeResponse(ctx, w, res); err != nil { + errhandler(ctx, w, err) + } + }) +} + // MountCounterGetHandler configures the mux to serve the "api" service // "CounterGet" endpoint. func MountCounterGetHandler(mux goahttp.Muxer, h http.Handler) { @@ -105,7 +161,7 @@ func MountCounterGetHandler(mux goahttp.Muxer, h http.Handler) { h.ServeHTTP(w, r) } } - mux.Handle("GET", "/counter", f) + mux.Handle("GET", "/api/v1/counter", f) } // NewCounterGetHandler creates a HTTP handler which loads the HTTP request and @@ -119,6 +175,7 @@ func NewCounterGetHandler( formatter func(ctx context.Context, err error) goahttp.Statuser, ) http.Handler { var ( + decodeRequest = DecodeCounterGetRequest(mux, decoder) encodeResponse = EncodeCounterGetResponse(encoder) encodeError = EncodeCounterGetError(encoder, formatter) ) @@ -126,8 +183,14 @@ func NewCounterGetHandler( ctx := context.WithValue(r.Context(), goahttp.AcceptTypeKey, r.Header.Get("Accept")) ctx = context.WithValue(ctx, goa.MethodKey, "CounterGet") ctx = context.WithValue(ctx, goa.ServiceKey, "api") - var err error - res, err := endpoint(ctx, nil) + payload, err := decodeRequest(r) + if err != nil { + if err := encodeError(ctx, w, err); err != nil { + errhandler(ctx, w, err) + } + return + } + res, err := endpoint(ctx, payload) if err != nil { if err := encodeError(ctx, w, err); err != nil { errhandler(ctx, w, err) @@ -149,7 +212,7 @@ func MountCounterIncrementHandler(mux goahttp.Muxer, h http.Handler) { h.ServeHTTP(w, r) } } - mux.Handle("POST", "/counter/inc", f) + mux.Handle("POST", "/api/v1/counter", f) } // NewCounterIncrementHandler creates a HTTP handler which loads the HTTP @@ -200,7 +263,7 @@ func MountEchoHandler(mux goahttp.Muxer, h http.Handler) { h.ServeHTTP(w, r) } } - mux.Handle("POST", "/echo", f) + mux.Handle("POST", "/api/v1/echo", f) } // NewEchoHandler creates a HTTP handler which loads the HTTP request and calls @@ -266,7 +329,7 @@ func appendPrefix(fsys http.FileSystem, prefix string) http.FileSystem { } // MountGenHTTPOpenapi3JSON configures the mux to serve GET request made to -// "/openapi.json". +// "/api/v1/openapi.json". func MountGenHTTPOpenapi3JSON(mux goahttp.Muxer, h http.Handler) { - mux.Handle("GET", "/openapi.json", h.ServeHTTP) + mux.Handle("GET", "/api/v1/openapi.json", h.ServeHTTP) } diff --git a/app/api/v1/gen/http/api/server/types.go b/app/api/v1/gen/http/api/server/types.go index c517257..05f1175 100644 --- a/app/api/v1/gen/http/api/server/types.go +++ b/app/api/v1/gen/http/api/server/types.go @@ -13,6 +13,13 @@ import ( goa "goa.design/goa/v3/pkg" ) +// AuthTokenRequestBody is the type of the "api" service "AuthToken" endpoint +// HTTP request body. +type AuthTokenRequestBody struct { + Provider *string `form:"provider,omitempty" json:"provider,omitempty" xml:"provider,omitempty"` + AccessToken *string `form:"access_token,omitempty" json:"access_token,omitempty" xml:"access_token,omitempty"` +} + // CounterIncrementRequestBody is the type of the "api" service // "CounterIncrement" endpoint HTTP request body. type CounterIncrementRequestBody struct { @@ -25,6 +32,12 @@ type EchoRequestBody struct { Text *string `form:"text,omitempty" json:"text,omitempty" xml:"text,omitempty"` } +// AuthTokenResponseBody is the type of the "api" service "AuthToken" endpoint +// HTTP response body. +type AuthTokenResponseBody struct { + Token string `form:"token" json:"token" xml:"token"` +} + // CounterGetResponseBody is the type of the "api" service "CounterGet" // endpoint HTTP response body. type CounterGetResponseBody struct { @@ -49,6 +62,43 @@ type EchoResponseBody struct { Text string `form:"text" json:"text" xml:"text"` } +// AuthTokenUnauthorizedResponseBody is the type of the "api" service +// "AuthToken" endpoint HTTP response body for the "unauthorized" error. +type AuthTokenUnauthorizedResponseBody struct { + // Name is the name of this class of errors. + Name string `form:"name" json:"name" xml:"name"` + // ID is a unique identifier for this particular occurrence of the problem. + ID string `form:"id" json:"id" xml:"id"` + // Message is a human-readable explanation specific to this occurrence of the + // problem. + Message string `form:"message" json:"message" xml:"message"` + // Is the error temporary? + Temporary bool `form:"temporary" json:"temporary" xml:"temporary"` + // Is the error a timeout? + Timeout bool `form:"timeout" json:"timeout" xml:"timeout"` + // Is the error a server-side fault? + Fault bool `form:"fault" json:"fault" xml:"fault"` +} + +// AuthTokenExistingIncrementRequestResponseBody is the type of the "api" +// service "AuthToken" endpoint HTTP response body for the +// "existing_increment_request" error. +type AuthTokenExistingIncrementRequestResponseBody struct { + // Name is the name of this class of errors. + Name string `form:"name" json:"name" xml:"name"` + // ID is a unique identifier for this particular occurrence of the problem. + ID string `form:"id" json:"id" xml:"id"` + // Message is a human-readable explanation specific to this occurrence of the + // problem. + Message string `form:"message" json:"message" xml:"message"` + // Is the error temporary? + Temporary bool `form:"temporary" json:"temporary" xml:"temporary"` + // Is the error a timeout? + Timeout bool `form:"timeout" json:"timeout" xml:"timeout"` + // Is the error a server-side fault? + Fault bool `form:"fault" json:"fault" xml:"fault"` +} + // CounterGetUnauthorizedResponseBody is the type of the "api" service // "CounterGet" endpoint HTTP response body for the "unauthorized" error. type CounterGetUnauthorizedResponseBody struct { @@ -160,6 +210,15 @@ type EchoExistingIncrementRequestResponseBody struct { Fault bool `form:"fault" json:"fault" xml:"fault"` } +// NewAuthTokenResponseBody builds the HTTP response body from the result of +// the "AuthToken" endpoint of the "api" service. +func NewAuthTokenResponseBody(res *api.AuthTokenResult) *AuthTokenResponseBody { + body := &AuthTokenResponseBody{ + Token: res.Token, + } + return body +} + // NewCounterGetResponseBody builds the HTTP response body from the result of // the "CounterGet" endpoint of the "api" service. func NewCounterGetResponseBody(res *apiviews.CounterInfoView) *CounterGetResponseBody { @@ -193,6 +252,34 @@ func NewEchoResponseBody(res *api.EchoResult) *EchoResponseBody { return body } +// NewAuthTokenUnauthorizedResponseBody builds the HTTP response body from the +// result of the "AuthToken" endpoint of the "api" service. +func NewAuthTokenUnauthorizedResponseBody(res *goa.ServiceError) *AuthTokenUnauthorizedResponseBody { + body := &AuthTokenUnauthorizedResponseBody{ + Name: res.Name, + ID: res.ID, + Message: res.Message, + Temporary: res.Temporary, + Timeout: res.Timeout, + Fault: res.Fault, + } + return body +} + +// NewAuthTokenExistingIncrementRequestResponseBody builds the HTTP response +// body from the result of the "AuthToken" endpoint of the "api" service. +func NewAuthTokenExistingIncrementRequestResponseBody(res *goa.ServiceError) *AuthTokenExistingIncrementRequestResponseBody { + body := &AuthTokenExistingIncrementRequestResponseBody{ + Name: res.Name, + ID: res.ID, + Message: res.Message, + Temporary: res.Temporary, + Timeout: res.Timeout, + Fault: res.Fault, + } + return body +} + // NewCounterGetUnauthorizedResponseBody builds the HTTP response body from the // result of the "CounterGet" endpoint of the "api" service. func NewCounterGetUnauthorizedResponseBody(res *goa.ServiceError) *CounterGetUnauthorizedResponseBody { @@ -278,12 +365,31 @@ func NewEchoExistingIncrementRequestResponseBody(res *goa.ServiceError) *EchoExi return body } +// NewAuthTokenPayload builds a api service AuthToken endpoint payload. +func NewAuthTokenPayload(body *AuthTokenRequestBody) *api.AuthTokenPayload { + v := &api.AuthTokenPayload{ + Provider: *body.Provider, + AccessToken: *body.AccessToken, + } + + return v +} + +// NewCounterGetPayload builds a api service CounterGet endpoint payload. +func NewCounterGetPayload(token string) *api.CounterGetPayload { + v := &api.CounterGetPayload{} + v.Token = token + + return v +} + // NewCounterIncrementPayload builds a api service CounterIncrement endpoint // payload. -func NewCounterIncrementPayload(body *CounterIncrementRequestBody) *api.CounterIncrementPayload { +func NewCounterIncrementPayload(body *CounterIncrementRequestBody, token string) *api.CounterIncrementPayload { v := &api.CounterIncrementPayload{ User: *body.User, } + v.Token = token return v } @@ -297,6 +403,23 @@ func NewEchoPayload(body *EchoRequestBody) *api.EchoPayload { return v } +// ValidateAuthTokenRequestBody runs the validations defined on +// AuthTokenRequestBody +func ValidateAuthTokenRequestBody(body *AuthTokenRequestBody) (err error) { + if body.Provider == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("provider", "body")) + } + if body.AccessToken == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("access_token", "body")) + } + if body.Provider != nil { + if !(*body.Provider == "google") { + err = goa.MergeErrors(err, goa.InvalidEnumValueError("body.provider", *body.Provider, []any{"google"})) + } + } + return +} + // ValidateCounterIncrementRequestBody runs the validations defined on // CounterIncrementRequestBody func ValidateCounterIncrementRequestBody(body *CounterIncrementRequestBody) (err error) { diff --git a/app/api/v1/gen/http/cli/countup/cli.go b/app/api/v1/gen/http/cli/countup/cli.go index fb50aaa..3bde74b 100644 --- a/app/api/v1/gen/http/cli/countup/cli.go +++ b/app/api/v1/gen/http/cli/countup/cli.go @@ -23,14 +23,17 @@ import ( // // command (subcommand1|subcommand2|...) func UsageCommands() string { - return `api (counter-get|counter-increment|echo) -web (index|another) + return `api (auth-token|counter-get|counter-increment|echo) +web (index|another|login-google|login-google-callback|logout|session-token) ` } // UsageExamples produces an example of a valid invocation of the CLI tool. func UsageExamples() string { - return os.Args[0] + ` api counter-get` + "\n" + + return os.Args[0] + ` api auth-token --body '{ + "access_token": "Omnis cumque est asperiores dolorem.", + "provider": "google" + }'` + "\n" + os.Args[0] + ` web index` + "\n" + "" } @@ -47,10 +50,15 @@ func ParseEndpoint( var ( apiFlags = flag.NewFlagSet("api", flag.ContinueOnError) - apiCounterGetFlags = flag.NewFlagSet("counter-get", flag.ExitOnError) + apiAuthTokenFlags = flag.NewFlagSet("auth-token", flag.ExitOnError) + apiAuthTokenBodyFlag = apiAuthTokenFlags.String("body", "REQUIRED", "") - apiCounterIncrementFlags = flag.NewFlagSet("counter-increment", flag.ExitOnError) - apiCounterIncrementBodyFlag = apiCounterIncrementFlags.String("body", "REQUIRED", "") + apiCounterGetFlags = flag.NewFlagSet("counter-get", flag.ExitOnError) + apiCounterGetTokenFlag = apiCounterGetFlags.String("token", "REQUIRED", "") + + apiCounterIncrementFlags = flag.NewFlagSet("counter-increment", flag.ExitOnError) + apiCounterIncrementBodyFlag = apiCounterIncrementFlags.String("body", "REQUIRED", "") + apiCounterIncrementTokenFlag = apiCounterIncrementFlags.String("token", "REQUIRED", "") apiEchoFlags = flag.NewFlagSet("echo", flag.ExitOnError) apiEchoBodyFlag = apiEchoFlags.String("body", "REQUIRED", "") @@ -60,8 +68,22 @@ func ParseEndpoint( webIndexFlags = flag.NewFlagSet("index", flag.ExitOnError) webAnotherFlags = flag.NewFlagSet("another", flag.ExitOnError) + + webLoginGoogleFlags = flag.NewFlagSet("login-google", flag.ExitOnError) + + webLoginGoogleCallbackFlags = flag.NewFlagSet("login-google-callback", flag.ExitOnError) + webLoginGoogleCallbackCodeFlag = webLoginGoogleCallbackFlags.String("code", "REQUIRED", "") + webLoginGoogleCallbackStateFlag = webLoginGoogleCallbackFlags.String("state", "REQUIRED", "") + webLoginGoogleCallbackSessionCookieFlag = webLoginGoogleCallbackFlags.String("session-cookie", "REQUIRED", "") + + webLogoutFlags = flag.NewFlagSet("logout", flag.ExitOnError) + webLogoutSessionCookieFlag = webLogoutFlags.String("session-cookie", "REQUIRED", "") + + webSessionTokenFlags = flag.NewFlagSet("session-token", flag.ExitOnError) + webSessionTokenSessionCookieFlag = webSessionTokenFlags.String("session-cookie", "REQUIRED", "") ) apiFlags.Usage = apiUsage + apiAuthTokenFlags.Usage = apiAuthTokenUsage apiCounterGetFlags.Usage = apiCounterGetUsage apiCounterIncrementFlags.Usage = apiCounterIncrementUsage apiEchoFlags.Usage = apiEchoUsage @@ -69,6 +91,10 @@ func ParseEndpoint( webFlags.Usage = webUsage webIndexFlags.Usage = webIndexUsage webAnotherFlags.Usage = webAnotherUsage + webLoginGoogleFlags.Usage = webLoginGoogleUsage + webLoginGoogleCallbackFlags.Usage = webLoginGoogleCallbackUsage + webLogoutFlags.Usage = webLogoutUsage + webSessionTokenFlags.Usage = webSessionTokenUsage if err := flag.CommandLine.Parse(os.Args[1:]); err != nil { return nil, nil, err @@ -106,6 +132,9 @@ func ParseEndpoint( switch svcn { case "api": switch epn { + case "auth-token": + epf = apiAuthTokenFlags + case "counter-get": epf = apiCounterGetFlags @@ -125,6 +154,18 @@ func ParseEndpoint( case "another": epf = webAnotherFlags + case "login-google": + epf = webLoginGoogleFlags + + case "login-google-callback": + epf = webLoginGoogleCallbackFlags + + case "logout": + epf = webLogoutFlags + + case "session-token": + epf = webSessionTokenFlags + } } @@ -150,11 +191,15 @@ func ParseEndpoint( case "api": c := apic.NewClient(scheme, host, doer, enc, dec, restore) switch epn { + case "auth-token": + endpoint = c.AuthToken() + data, err = apic.BuildAuthTokenPayload(*apiAuthTokenBodyFlag) case "counter-get": endpoint = c.CounterGet() + data, err = apic.BuildCounterGetPayload(*apiCounterGetTokenFlag) case "counter-increment": endpoint = c.CounterIncrement() - data, err = apic.BuildCounterIncrementPayload(*apiCounterIncrementBodyFlag) + data, err = apic.BuildCounterIncrementPayload(*apiCounterIncrementBodyFlag, *apiCounterIncrementTokenFlag) case "echo": endpoint = c.Echo() data, err = apic.BuildEchoPayload(*apiEchoBodyFlag) @@ -166,6 +211,17 @@ func ParseEndpoint( endpoint = c.Index() case "another": endpoint = c.Another() + case "login-google": + endpoint = c.LoginGoogle() + case "login-google-callback": + endpoint = c.LoginGoogleCallback() + data, err = webc.BuildLoginGoogleCallbackPayload(*webLoginGoogleCallbackCodeFlag, *webLoginGoogleCallbackStateFlag, *webLoginGoogleCallbackSessionCookieFlag) + case "logout": + endpoint = c.Logout() + data, err = webc.BuildLogoutPayload(*webLogoutSessionCookieFlag) + case "session-token": + endpoint = c.SessionToken() + data, err = webc.BuildSessionTokenPayload(*webSessionTokenSessionCookieFlag) } } } @@ -183,6 +239,7 @@ Usage: %[1]s [globalflags] api COMMAND [flags] COMMAND: + auth-token: AuthToken implements AuthToken. counter-get: CounterGet implements CounterGet. counter-increment: CounterIncrement implements CounterIncrement. echo: Echo implements Echo. @@ -191,26 +248,42 @@ Additional help: %[1]s api COMMAND --help `, os.Args[0]) } +func apiAuthTokenUsage() { + fmt.Fprintf(os.Stderr, `%[1]s [flags] api auth-token -body JSON + +AuthToken implements AuthToken. + -body JSON: + +Example: + %[1]s api auth-token --body '{ + "access_token": "Omnis cumque est asperiores dolorem.", + "provider": "google" + }' +`, os.Args[0]) +} + func apiCounterGetUsage() { - fmt.Fprintf(os.Stderr, `%[1]s [flags] api counter-get + fmt.Fprintf(os.Stderr, `%[1]s [flags] api counter-get -token STRING CounterGet implements CounterGet. + -token STRING: Example: - %[1]s api counter-get + %[1]s api counter-get --token "Voluptatem aliquid in quaerat ut nihil." `, os.Args[0]) } func apiCounterIncrementUsage() { - fmt.Fprintf(os.Stderr, `%[1]s [flags] api counter-increment -body JSON + fmt.Fprintf(os.Stderr, `%[1]s [flags] api counter-increment -body JSON -token STRING CounterIncrement implements CounterIncrement. -body JSON: + -token STRING: Example: %[1]s api counter-increment --body '{ - "user": "Nihil doloribus et sed sequi consequatur." - }' + "user": "Quae cupiditate." + }' --token "Harum error iste ipsam." `, os.Args[0]) } @@ -222,7 +295,7 @@ Echo implements Echo. Example: %[1]s api echo --body '{ - "text": "Vel omnis quo sit." + "text": "Tempora repellendus." }' `, os.Args[0]) } @@ -234,8 +307,12 @@ Usage: %[1]s [globalflags] web COMMAND [flags] COMMAND: - index: Index implements index. - another: Another implements another. + index: Index implements Index. + another: Another implements Another. + login-google: LoginGoogle implements LoginGoogle. + login-google-callback: LoginGoogleCallback implements LoginGoogleCallback. + logout: Logout implements Logout. + session-token: SessionToken implements SessionToken. Additional help: %[1]s web COMMAND --help @@ -244,7 +321,7 @@ Additional help: func webIndexUsage() { fmt.Fprintf(os.Stderr, `%[1]s [flags] web index -Index implements index. +Index implements Index. Example: %[1]s web index @@ -254,9 +331,54 @@ Example: func webAnotherUsage() { fmt.Fprintf(os.Stderr, `%[1]s [flags] web another -Another implements another. +Another implements Another. Example: %[1]s web another `, os.Args[0]) } + +func webLoginGoogleUsage() { + fmt.Fprintf(os.Stderr, `%[1]s [flags] web login-google + +LoginGoogle implements LoginGoogle. + +Example: + %[1]s web login-google +`, os.Args[0]) +} + +func webLoginGoogleCallbackUsage() { + fmt.Fprintf(os.Stderr, `%[1]s [flags] web login-google-callback -code STRING -state STRING -session-cookie STRING + +LoginGoogleCallback implements LoginGoogleCallback. + -code STRING: + -state STRING: + -session-cookie STRING: + +Example: + %[1]s web login-google-callback --code "Blanditiis ut." --state "Deleniti non." --session-cookie "Rerum eum dolor." +`, os.Args[0]) +} + +func webLogoutUsage() { + fmt.Fprintf(os.Stderr, `%[1]s [flags] web logout -session-cookie STRING + +Logout implements Logout. + -session-cookie STRING: + +Example: + %[1]s web logout --session-cookie "Dolorem ullam magnam." +`, os.Args[0]) +} + +func webSessionTokenUsage() { + fmt.Fprintf(os.Stderr, `%[1]s [flags] web session-token -session-cookie STRING + +SessionToken implements SessionToken. + -session-cookie STRING: + +Example: + %[1]s web session-token --session-cookie "Rerum aut tenetur." +`, os.Args[0]) +} diff --git a/app/api/v1/gen/http/openapi.json b/app/api/v1/gen/http/openapi.json index 3cfe53a..c431ab8 100644 --- a/app/api/v1/gen/http/openapi.json +++ b/app/api/v1/gen/http/openapi.json @@ -1 +1 @@ -{"swagger":"2.0","info":{"title":"Count Up","description":"A production-ready Go service deployed on Kubernetes","version":"1.0.0"},"host":"localhost:8080","consumes":["application/json","application/xml","application/gob"],"produces":["application/json","application/xml","application/gob"],"paths":{"/":{"get":{"tags":["web"],"summary":"index web","operationId":"web#index","produces":["text/html"],"responses":{"200":{"description":"OK response.","schema":{"type":"string","format":"byte"}}},"schemes":["http"]}},"/another":{"get":{"tags":["web"],"summary":"another web","operationId":"web#another","produces":["text/html"],"responses":{"200":{"description":"OK response.","schema":{"type":"string","format":"byte"}}},"schemes":["http"]}},"/counter":{"get":{"tags":["api"],"summary":"CounterGet api","operationId":"api#CounterGet","responses":{"200":{"description":"OK response.","schema":{"$ref":"#/definitions/CounterInfo"}},"401":{"description":"Unauthorized response.","schema":{"$ref":"#/definitions/APICounterGetUnauthorizedResponseBody"}},"429":{"description":"Too Many Requests response.","schema":{"$ref":"#/definitions/APICounterGetExistingIncrementRequestResponseBody"}}},"schemes":["http"]}},"/counter/inc":{"post":{"tags":["api"],"summary":"CounterIncrement api","operationId":"api#CounterIncrement","parameters":[{"name":"CounterIncrementRequestBody","in":"body","required":true,"schema":{"$ref":"#/definitions/APICounterIncrementRequestBody","required":["user"]}}],"responses":{"202":{"description":"Accepted response.","schema":{"$ref":"#/definitions/CounterInfo"}},"401":{"description":"Unauthorized response.","schema":{"$ref":"#/definitions/APICounterIncrementUnauthorizedResponseBody"}},"429":{"description":"Too Many Requests response.","schema":{"$ref":"#/definitions/APICounterIncrementExistingIncrementRequestResponseBody"}}},"schemes":["http"]}},"/echo":{"post":{"tags":["api"],"summary":"Echo api","operationId":"api#Echo","parameters":[{"name":"EchoRequestBody","in":"body","required":true,"schema":{"$ref":"#/definitions/APIEchoRequestBody","required":["text"]}}],"responses":{"200":{"description":"OK response.","schema":{"$ref":"#/definitions/APIEchoResponseBody","required":["text"]}},"401":{"description":"Unauthorized response.","schema":{"$ref":"#/definitions/APIEchoUnauthorizedResponseBody"}},"429":{"description":"Too Many Requests response.","schema":{"$ref":"#/definitions/APIEchoExistingIncrementRequestResponseBody"}}},"schemes":["http"]}},"/openapi.json":{"get":{"tags":["api"],"summary":"Download gen/http/openapi3.json","operationId":"api#/openapi.json","responses":{"200":{"description":"File downloaded","schema":{"type":"file"}}},"schemes":["http"]}},"/static/{path}":{"get":{"tags":["web"],"summary":"Download static/","operationId":"web#/static/{*path}","parameters":[{"name":"path","in":"path","description":"Relative file path","required":true,"type":"string"}],"responses":{"200":{"description":"File downloaded","schema":{"type":"file"}},"404":{"description":"File not found","schema":{"$ref":"#/definitions/Error"}}},"schemes":["http"]}}},"definitions":{"APICounterGetExistingIncrementRequestResponseBody":{"title":"Mediatype identifier: application/vnd.goa.error; view=default","type":"object","properties":{"fault":{"type":"boolean","description":"Is the error a server-side fault?","example":false},"id":{"type":"string","description":"ID is a unique identifier for this particular occurrence of the problem.","example":"123abc"},"message":{"type":"string","description":"Message is a human-readable explanation specific to this occurrence of the problem.","example":"parameter 'p' must be an integer"},"name":{"type":"string","description":"Name is the name of this class of errors.","example":"bad_request"},"temporary":{"type":"boolean","description":"Is the error temporary?","example":true},"timeout":{"type":"boolean","description":"Is the error a timeout?","example":true}},"description":"CounterGet_existing_increment_request_Response_Body result type (default view)","example":{"fault":true,"id":"123abc","message":"parameter 'p' must be an integer","name":"bad_request","temporary":false,"timeout":false},"required":["name","id","message","temporary","timeout","fault"]},"APICounterGetUnauthorizedResponseBody":{"title":"Mediatype identifier: application/vnd.goa.error; view=default","type":"object","properties":{"fault":{"type":"boolean","description":"Is the error a server-side fault?","example":true},"id":{"type":"string","description":"ID is a unique identifier for this particular occurrence of the problem.","example":"123abc"},"message":{"type":"string","description":"Message is a human-readable explanation specific to this occurrence of the problem.","example":"parameter 'p' must be an integer"},"name":{"type":"string","description":"Name is the name of this class of errors.","example":"bad_request"},"temporary":{"type":"boolean","description":"Is the error temporary?","example":false},"timeout":{"type":"boolean","description":"Is the error a timeout?","example":true}},"description":"CounterGet_unauthorized_Response_Body result type (default view)","example":{"fault":false,"id":"123abc","message":"parameter 'p' must be an integer","name":"bad_request","temporary":false,"timeout":false},"required":["name","id","message","temporary","timeout","fault"]},"APICounterIncrementExistingIncrementRequestResponseBody":{"title":"Mediatype identifier: application/vnd.goa.error; view=default","type":"object","properties":{"fault":{"type":"boolean","description":"Is the error a server-side fault?","example":true},"id":{"type":"string","description":"ID is a unique identifier for this particular occurrence of the problem.","example":"123abc"},"message":{"type":"string","description":"Message is a human-readable explanation specific to this occurrence of the problem.","example":"parameter 'p' must be an integer"},"name":{"type":"string","description":"Name is the name of this class of errors.","example":"bad_request"},"temporary":{"type":"boolean","description":"Is the error temporary?","example":false},"timeout":{"type":"boolean","description":"Is the error a timeout?","example":true}},"description":"CounterIncrement_existing_increment_request_Response_Body result type (default view)","example":{"fault":false,"id":"123abc","message":"parameter 'p' must be an integer","name":"bad_request","temporary":false,"timeout":true},"required":["name","id","message","temporary","timeout","fault"]},"APICounterIncrementRequestBody":{"title":"APICounterIncrementRequestBody","type":"object","properties":{"user":{"type":"string","example":"Non perspiciatis eum dicta sit."}},"example":{"user":"Odio tenetur temporibus."},"required":["user"]},"APICounterIncrementUnauthorizedResponseBody":{"title":"Mediatype identifier: application/vnd.goa.error; view=default","type":"object","properties":{"fault":{"type":"boolean","description":"Is the error a server-side fault?","example":false},"id":{"type":"string","description":"ID is a unique identifier for this particular occurrence of the problem.","example":"123abc"},"message":{"type":"string","description":"Message is a human-readable explanation specific to this occurrence of the problem.","example":"parameter 'p' must be an integer"},"name":{"type":"string","description":"Name is the name of this class of errors.","example":"bad_request"},"temporary":{"type":"boolean","description":"Is the error temporary?","example":false},"timeout":{"type":"boolean","description":"Is the error a timeout?","example":true}},"description":"CounterIncrement_unauthorized_Response_Body result type (default view)","example":{"fault":false,"id":"123abc","message":"parameter 'p' must be an integer","name":"bad_request","temporary":false,"timeout":false},"required":["name","id","message","temporary","timeout","fault"]},"APIEchoExistingIncrementRequestResponseBody":{"title":"Mediatype identifier: application/vnd.goa.error; view=default","type":"object","properties":{"fault":{"type":"boolean","description":"Is the error a server-side fault?","example":false},"id":{"type":"string","description":"ID is a unique identifier for this particular occurrence of the problem.","example":"123abc"},"message":{"type":"string","description":"Message is a human-readable explanation specific to this occurrence of the problem.","example":"parameter 'p' must be an integer"},"name":{"type":"string","description":"Name is the name of this class of errors.","example":"bad_request"},"temporary":{"type":"boolean","description":"Is the error temporary?","example":true},"timeout":{"type":"boolean","description":"Is the error a timeout?","example":false}},"description":"Echo_existing_increment_request_Response_Body result type (default view)","example":{"fault":true,"id":"123abc","message":"parameter 'p' must be an integer","name":"bad_request","temporary":false,"timeout":false},"required":["name","id","message","temporary","timeout","fault"]},"APIEchoRequestBody":{"title":"APIEchoRequestBody","type":"object","properties":{"text":{"type":"string","example":"Deleniti necessitatibus numquam perspiciatis quia ipsa quam."}},"example":{"text":"Nemo dolorem ullam magnam."},"required":["text"]},"APIEchoResponseBody":{"title":"APIEchoResponseBody","type":"object","properties":{"text":{"type":"string","example":"Praesentium qui debitis."}},"example":{"text":"Odit cum blanditiis ut."},"required":["text"]},"APIEchoUnauthorizedResponseBody":{"title":"Mediatype identifier: application/vnd.goa.error; view=default","type":"object","properties":{"fault":{"type":"boolean","description":"Is the error a server-side fault?","example":true},"id":{"type":"string","description":"ID is a unique identifier for this particular occurrence of the problem.","example":"123abc"},"message":{"type":"string","description":"Message is a human-readable explanation specific to this occurrence of the problem.","example":"parameter 'p' must be an integer"},"name":{"type":"string","description":"Name is the name of this class of errors.","example":"bad_request"},"temporary":{"type":"boolean","description":"Is the error temporary?","example":false},"timeout":{"type":"boolean","description":"Is the error a timeout?","example":false}},"description":"Echo_unauthorized_Response_Body result type (default view)","example":{"fault":false,"id":"123abc","message":"parameter 'p' must be an integer","name":"bad_request","temporary":false,"timeout":true},"required":["name","id","message","temporary","timeout","fault"]},"CounterInfo":{"title":"Mediatype identifier: application/vnd.countup.counter-info`; view=default","type":"object","properties":{"count":{"type":"integer","example":1315490442,"format":"int32"},"last_increment_at":{"type":"string","example":"Eligendi quisquam."},"last_increment_by":{"type":"string","example":"Vero molestiae."},"next_finalize_at":{"type":"string","example":"Aut in dolor eum consequatur."}},"description":"CounterGetResponseBody result type (default view)","example":{"count":1162746042,"last_increment_at":"Tempore asperiores.","last_increment_by":"Tempora repellendus.","next_finalize_at":"Quae voluptatibus dolor fugit quia."},"required":["count","last_increment_by","last_increment_at","next_finalize_at"]},"Error":{"title":"Mediatype identifier: application/vnd.goa.error; view=default","type":"object","properties":{"fault":{"type":"boolean","description":"Is the error a server-side fault?","example":false},"id":{"type":"string","description":"ID is a unique identifier for this particular occurrence of the problem.","example":"123abc"},"message":{"type":"string","description":"Message is a human-readable explanation specific to this occurrence of the problem.","example":"parameter 'p' must be an integer"},"name":{"type":"string","description":"Name is the name of this class of errors.","example":"bad_request"},"temporary":{"type":"boolean","description":"Is the error temporary?","example":true},"timeout":{"type":"boolean","description":"Is the error a timeout?","example":false}},"description":"Error response result type (default view)","example":{"fault":true,"id":"123abc","message":"parameter 'p' must be an integer","name":"bad_request","temporary":true,"timeout":true},"required":["name","id","message","temporary","timeout","fault"]}}} \ No newline at end of file +{"swagger":"2.0","info":{"title":"Count Up","description":"A production-ready Go service deployed on Kubernetes","version":"1.0.0"},"host":"localhost:8080","consumes":["application/json","application/xml","application/gob"],"produces":["application/json","application/xml","application/gob"],"paths":{"/":{"get":{"tags":["web"],"summary":"Index web","operationId":"web#Index","produces":["text/html"],"responses":{"200":{"description":"OK response.","schema":{"type":"string","format":"byte"}},"401":{"description":"Unauthorized response.","schema":{"$ref":"#/definitions/WebIndexUnauthorizedResponseBody"}}},"schemes":["http"]}},"/another":{"get":{"tags":["web"],"summary":"Another web","operationId":"web#Another","produces":["text/html"],"responses":{"200":{"description":"OK response.","schema":{"type":"string","format":"byte"}},"401":{"description":"Unauthorized response.","schema":{"$ref":"#/definitions/WebAnotherUnauthorizedResponseBody"}}},"schemes":["http"]}},"/api/v1/auth/token":{"post":{"tags":["api"],"summary":"AuthToken api","operationId":"api#AuthToken","parameters":[{"name":"AuthTokenRequestBody","in":"body","required":true,"schema":{"$ref":"#/definitions/APIAuthTokenRequestBody","required":["provider","access_token"]}}],"responses":{"200":{"description":"OK response.","schema":{"$ref":"#/definitions/APIAuthTokenResponseBody","required":["token"]}},"401":{"description":"Unauthorized response.","schema":{"$ref":"#/definitions/APIAuthTokenUnauthorizedResponseBody"}},"429":{"description":"Too Many Requests response.","schema":{"$ref":"#/definitions/APIAuthTokenExistingIncrementRequestResponseBody"}}},"schemes":["http"]}},"/api/v1/counter":{"get":{"tags":["api"],"summary":"CounterGet api","description":"\n**Required security scopes for jwt**:\n * `api`","operationId":"api#CounterGet","parameters":[{"name":"Authorization","in":"header","required":true,"type":"string"}],"responses":{"200":{"description":"OK response.","schema":{"$ref":"#/definitions/CounterInfo"}},"401":{"description":"Unauthorized response.","schema":{"$ref":"#/definitions/APICounterGetUnauthorizedResponseBody"}},"429":{"description":"Too Many Requests response.","schema":{"$ref":"#/definitions/APICounterGetExistingIncrementRequestResponseBody"}}},"schemes":["http"],"security":[{"jwt_header_Authorization":[]}]},"post":{"tags":["api"],"summary":"CounterIncrement api","description":"\n**Required security scopes for jwt**:\n * `api`","operationId":"api#CounterIncrement","parameters":[{"name":"Authorization","in":"header","required":true,"type":"string"},{"name":"CounterIncrementRequestBody","in":"body","required":true,"schema":{"$ref":"#/definitions/APICounterIncrementRequestBody","required":["user"]}}],"responses":{"202":{"description":"Accepted response.","schema":{"$ref":"#/definitions/CounterInfo"}},"401":{"description":"Unauthorized response.","schema":{"$ref":"#/definitions/APICounterIncrementUnauthorizedResponseBody"}},"429":{"description":"Too Many Requests response.","schema":{"$ref":"#/definitions/APICounterIncrementExistingIncrementRequestResponseBody"}}},"schemes":["http"],"security":[{"jwt_header_Authorization":[]}]}},"/api/v1/echo":{"post":{"tags":["api"],"summary":"Echo api","operationId":"api#Echo","parameters":[{"name":"EchoRequestBody","in":"body","required":true,"schema":{"$ref":"#/definitions/APIEchoRequestBody","required":["text"]}}],"responses":{"200":{"description":"OK response.","schema":{"$ref":"#/definitions/APIEchoResponseBody","required":["text"]}},"401":{"description":"Unauthorized response.","schema":{"$ref":"#/definitions/APIEchoUnauthorizedResponseBody"}},"429":{"description":"Too Many Requests response.","schema":{"$ref":"#/definitions/APIEchoExistingIncrementRequestResponseBody"}}},"schemes":["http"]}},"/api/v1/openapi.json":{"get":{"tags":["api"],"summary":"Download gen/http/openapi3.json","operationId":"api#/api/v1/openapi.json","responses":{"200":{"description":"File downloaded","schema":{"type":"file"}}},"schemes":["http"]}},"/login/google":{"get":{"tags":["web"],"summary":"LoginGoogle web","operationId":"web#LoginGoogle","responses":{"302":{"description":"Found response.","headers":{"Location":{"type":"string"}}},"401":{"description":"Unauthorized response.","schema":{"$ref":"#/definitions/WebLoginGoogleUnauthorizedResponseBody"}}},"schemes":["http"]}},"/login/google/callback":{"get":{"tags":["web"],"summary":"LoginGoogleCallback web","operationId":"web#LoginGoogleCallback","parameters":[{"name":"code","in":"query","required":true,"type":"string"},{"name":"state","in":"query","required":true,"type":"string"}],"responses":{"302":{"description":"Found response.","headers":{"Location":{"type":"string"}}},"401":{"description":"Unauthorized response.","schema":{"$ref":"#/definitions/WebLoginGoogleCallbackUnauthorizedResponseBody"}}},"schemes":["http"]}},"/logout":{"get":{"tags":["web"],"summary":"Logout web","operationId":"web#Logout","responses":{"302":{"description":"Found response.","headers":{"Location":{"type":"string"}}},"401":{"description":"Unauthorized response.","schema":{"$ref":"#/definitions/WebLogoutUnauthorizedResponseBody"}}},"schemes":["http"]}},"/session/token":{"get":{"tags":["web"],"summary":"SessionToken web","operationId":"web#SessionToken","produces":["application/json"],"responses":{"200":{"description":"OK response.","schema":{"$ref":"#/definitions/WebSessionTokenResponseBody","required":["token"]}},"401":{"description":"Unauthorized response.","schema":{"$ref":"#/definitions/WebSessionTokenUnauthorizedResponseBody"}}},"schemes":["http"]}},"/static/*":{"get":{"tags":["web"],"summary":"Download static/","operationId":"web#/static/*","responses":{"200":{"description":"File downloaded","schema":{"type":"file"}}},"schemes":["http"]}}},"definitions":{"APIAuthTokenExistingIncrementRequestResponseBody":{"title":"Mediatype identifier: application/vnd.goa.error; view=default","type":"object","properties":{"fault":{"type":"boolean","description":"Is the error a server-side fault?","example":true},"id":{"type":"string","description":"ID is a unique identifier for this particular occurrence of the problem.","example":"123abc"},"message":{"type":"string","description":"Message is a human-readable explanation specific to this occurrence of the problem.","example":"parameter 'p' must be an integer"},"name":{"type":"string","description":"Name is the name of this class of errors.","example":"bad_request"},"temporary":{"type":"boolean","description":"Is the error temporary?","example":true},"timeout":{"type":"boolean","description":"Is the error a timeout?","example":true}},"description":"AuthToken_existing_increment_request_Response_Body result type (default view)","example":{"fault":true,"id":"123abc","message":"parameter 'p' must be an integer","name":"bad_request","temporary":false,"timeout":true},"required":["name","id","message","temporary","timeout","fault"]},"APIAuthTokenRequestBody":{"title":"APIAuthTokenRequestBody","type":"object","properties":{"access_token":{"type":"string","example":"Aut culpa quo sit dolor aperiam consequatur."},"provider":{"type":"string","example":"google","enum":["google"]}},"example":{"access_token":"Harum iusto quas.","provider":"google"},"required":["provider","access_token"]},"APIAuthTokenResponseBody":{"title":"APIAuthTokenResponseBody","type":"object","properties":{"token":{"type":"string","example":"Illo vel qui ipsa adipisci."}},"example":{"token":"Quam eaque ipsa eius quaerat."},"required":["token"]},"APIAuthTokenUnauthorizedResponseBody":{"title":"Mediatype identifier: application/vnd.goa.error; view=default","type":"object","properties":{"fault":{"type":"boolean","description":"Is the error a server-side fault?","example":true},"id":{"type":"string","description":"ID is a unique identifier for this particular occurrence of the problem.","example":"123abc"},"message":{"type":"string","description":"Message is a human-readable explanation specific to this occurrence of the problem.","example":"parameter 'p' must be an integer"},"name":{"type":"string","description":"Name is the name of this class of errors.","example":"bad_request"},"temporary":{"type":"boolean","description":"Is the error temporary?","example":true},"timeout":{"type":"boolean","description":"Is the error a timeout?","example":true}},"description":"AuthToken_unauthorized_Response_Body result type (default view)","example":{"fault":true,"id":"123abc","message":"parameter 'p' must be an integer","name":"bad_request","temporary":true,"timeout":false},"required":["name","id","message","temporary","timeout","fault"]},"APICounterGetExistingIncrementRequestResponseBody":{"title":"Mediatype identifier: application/vnd.goa.error; view=default","type":"object","properties":{"fault":{"type":"boolean","description":"Is the error a server-side fault?","example":false},"id":{"type":"string","description":"ID is a unique identifier for this particular occurrence of the problem.","example":"123abc"},"message":{"type":"string","description":"Message is a human-readable explanation specific to this occurrence of the problem.","example":"parameter 'p' must be an integer"},"name":{"type":"string","description":"Name is the name of this class of errors.","example":"bad_request"},"temporary":{"type":"boolean","description":"Is the error temporary?","example":false},"timeout":{"type":"boolean","description":"Is the error a timeout?","example":true}},"description":"CounterGet_existing_increment_request_Response_Body result type (default view)","example":{"fault":false,"id":"123abc","message":"parameter 'p' must be an integer","name":"bad_request","temporary":false,"timeout":true},"required":["name","id","message","temporary","timeout","fault"]},"APICounterGetUnauthorizedResponseBody":{"title":"Mediatype identifier: application/vnd.goa.error; view=default","type":"object","properties":{"fault":{"type":"boolean","description":"Is the error a server-side fault?","example":true},"id":{"type":"string","description":"ID is a unique identifier for this particular occurrence of the problem.","example":"123abc"},"message":{"type":"string","description":"Message is a human-readable explanation specific to this occurrence of the problem.","example":"parameter 'p' must be an integer"},"name":{"type":"string","description":"Name is the name of this class of errors.","example":"bad_request"},"temporary":{"type":"boolean","description":"Is the error temporary?","example":true},"timeout":{"type":"boolean","description":"Is the error a timeout?","example":true}},"description":"CounterGet_unauthorized_Response_Body result type (default view)","example":{"fault":true,"id":"123abc","message":"parameter 'p' must be an integer","name":"bad_request","temporary":true,"timeout":true},"required":["name","id","message","temporary","timeout","fault"]},"APICounterIncrementExistingIncrementRequestResponseBody":{"title":"Mediatype identifier: application/vnd.goa.error; view=default","type":"object","properties":{"fault":{"type":"boolean","description":"Is the error a server-side fault?","example":true},"id":{"type":"string","description":"ID is a unique identifier for this particular occurrence of the problem.","example":"123abc"},"message":{"type":"string","description":"Message is a human-readable explanation specific to this occurrence of the problem.","example":"parameter 'p' must be an integer"},"name":{"type":"string","description":"Name is the name of this class of errors.","example":"bad_request"},"temporary":{"type":"boolean","description":"Is the error temporary?","example":false},"timeout":{"type":"boolean","description":"Is the error a timeout?","example":true}},"description":"CounterIncrement_existing_increment_request_Response_Body result type (default view)","example":{"fault":false,"id":"123abc","message":"parameter 'p' must be an integer","name":"bad_request","temporary":false,"timeout":true},"required":["name","id","message","temporary","timeout","fault"]},"APICounterIncrementRequestBody":{"title":"APICounterIncrementRequestBody","type":"object","properties":{"user":{"type":"string","example":"Voluptas quae perspiciatis illo suscipit."}},"example":{"user":"Nesciunt vero repudiandae."},"required":["user"]},"APICounterIncrementUnauthorizedResponseBody":{"title":"Mediatype identifier: application/vnd.goa.error; view=default","type":"object","properties":{"fault":{"type":"boolean","description":"Is the error a server-side fault?","example":true},"id":{"type":"string","description":"ID is a unique identifier for this particular occurrence of the problem.","example":"123abc"},"message":{"type":"string","description":"Message is a human-readable explanation specific to this occurrence of the problem.","example":"parameter 'p' must be an integer"},"name":{"type":"string","description":"Name is the name of this class of errors.","example":"bad_request"},"temporary":{"type":"boolean","description":"Is the error temporary?","example":false},"timeout":{"type":"boolean","description":"Is the error a timeout?","example":true}},"description":"CounterIncrement_unauthorized_Response_Body result type (default view)","example":{"fault":true,"id":"123abc","message":"parameter 'p' must be an integer","name":"bad_request","temporary":true,"timeout":false},"required":["name","id","message","temporary","timeout","fault"]},"APIEchoExistingIncrementRequestResponseBody":{"title":"Mediatype identifier: application/vnd.goa.error; view=default","type":"object","properties":{"fault":{"type":"boolean","description":"Is the error a server-side fault?","example":false},"id":{"type":"string","description":"ID is a unique identifier for this particular occurrence of the problem.","example":"123abc"},"message":{"type":"string","description":"Message is a human-readable explanation specific to this occurrence of the problem.","example":"parameter 'p' must be an integer"},"name":{"type":"string","description":"Name is the name of this class of errors.","example":"bad_request"},"temporary":{"type":"boolean","description":"Is the error temporary?","example":false},"timeout":{"type":"boolean","description":"Is the error a timeout?","example":true}},"description":"Echo_existing_increment_request_Response_Body result type (default view)","example":{"fault":true,"id":"123abc","message":"parameter 'p' must be an integer","name":"bad_request","temporary":false,"timeout":false},"required":["name","id","message","temporary","timeout","fault"]},"APIEchoRequestBody":{"title":"APIEchoRequestBody","type":"object","properties":{"text":{"type":"string","example":"Labore ut aut possimus et."}},"example":{"text":"Magni harum atque dolor quod at consectetur."},"required":["text"]},"APIEchoResponseBody":{"title":"APIEchoResponseBody","type":"object","properties":{"text":{"type":"string","example":"Id accusamus quam aut."}},"example":{"text":"Voluptatem ipsam et dolorem quos nihil assumenda."},"required":["text"]},"APIEchoUnauthorizedResponseBody":{"title":"Mediatype identifier: application/vnd.goa.error; view=default","type":"object","properties":{"fault":{"type":"boolean","description":"Is the error a server-side fault?","example":false},"id":{"type":"string","description":"ID is a unique identifier for this particular occurrence of the problem.","example":"123abc"},"message":{"type":"string","description":"Message is a human-readable explanation specific to this occurrence of the problem.","example":"parameter 'p' must be an integer"},"name":{"type":"string","description":"Name is the name of this class of errors.","example":"bad_request"},"temporary":{"type":"boolean","description":"Is the error temporary?","example":false},"timeout":{"type":"boolean","description":"Is the error a timeout?","example":true}},"description":"Echo_unauthorized_Response_Body result type (default view)","example":{"fault":false,"id":"123abc","message":"parameter 'p' must be an integer","name":"bad_request","temporary":false,"timeout":false},"required":["name","id","message","temporary","timeout","fault"]},"CounterInfo":{"title":"Mediatype identifier: application/vnd.countup.counter-info`; view=default","type":"object","properties":{"count":{"type":"integer","example":718974130,"format":"int32"},"last_increment_at":{"type":"string","example":"Consequuntur cupiditate pariatur aut placeat."},"last_increment_by":{"type":"string","example":"Ipsam nesciunt minima cupiditate."},"next_finalize_at":{"type":"string","example":"Amet voluptas ex nostrum aperiam explicabo sed."}},"description":"CounterGetResponseBody result type (default view)","example":{"count":837784502,"last_increment_at":"Eius rerum.","last_increment_by":"Sint asperiores sed voluptas voluptatem.","next_finalize_at":"Accusamus maiores ut voluptas."},"required":["count","last_increment_by","last_increment_at","next_finalize_at"]},"WebAnotherUnauthorizedResponseBody":{"title":"Mediatype identifier: application/vnd.goa.error; view=default","type":"object","properties":{"fault":{"type":"boolean","description":"Is the error a server-side fault?","example":true},"id":{"type":"string","description":"ID is a unique identifier for this particular occurrence of the problem.","example":"123abc"},"message":{"type":"string","description":"Message is a human-readable explanation specific to this occurrence of the problem.","example":"parameter 'p' must be an integer"},"name":{"type":"string","description":"Name is the name of this class of errors.","example":"bad_request"},"temporary":{"type":"boolean","description":"Is the error temporary?","example":false},"timeout":{"type":"boolean","description":"Is the error a timeout?","example":true}},"description":"Another_unauthorized_Response_Body result type (default view)","example":{"fault":true,"id":"123abc","message":"parameter 'p' must be an integer","name":"bad_request","temporary":false,"timeout":true},"required":["name","id","message","temporary","timeout","fault"]},"WebIndexUnauthorizedResponseBody":{"title":"Mediatype identifier: application/vnd.goa.error; view=default","type":"object","properties":{"fault":{"type":"boolean","description":"Is the error a server-side fault?","example":false},"id":{"type":"string","description":"ID is a unique identifier for this particular occurrence of the problem.","example":"123abc"},"message":{"type":"string","description":"Message is a human-readable explanation specific to this occurrence of the problem.","example":"parameter 'p' must be an integer"},"name":{"type":"string","description":"Name is the name of this class of errors.","example":"bad_request"},"temporary":{"type":"boolean","description":"Is the error temporary?","example":true},"timeout":{"type":"boolean","description":"Is the error a timeout?","example":false}},"description":"Index_unauthorized_Response_Body result type (default view)","example":{"fault":false,"id":"123abc","message":"parameter 'p' must be an integer","name":"bad_request","temporary":true,"timeout":false},"required":["name","id","message","temporary","timeout","fault"]},"WebLoginGoogleCallbackUnauthorizedResponseBody":{"title":"Mediatype identifier: application/vnd.goa.error; view=default","type":"object","properties":{"fault":{"type":"boolean","description":"Is the error a server-side fault?","example":false},"id":{"type":"string","description":"ID is a unique identifier for this particular occurrence of the problem.","example":"123abc"},"message":{"type":"string","description":"Message is a human-readable explanation specific to this occurrence of the problem.","example":"parameter 'p' must be an integer"},"name":{"type":"string","description":"Name is the name of this class of errors.","example":"bad_request"},"temporary":{"type":"boolean","description":"Is the error temporary?","example":true},"timeout":{"type":"boolean","description":"Is the error a timeout?","example":true}},"description":"LoginGoogleCallback_unauthorized_Response_Body result type (default view)","example":{"fault":true,"id":"123abc","message":"parameter 'p' must be an integer","name":"bad_request","temporary":true,"timeout":false},"required":["name","id","message","temporary","timeout","fault"]},"WebLoginGoogleUnauthorizedResponseBody":{"title":"Mediatype identifier: application/vnd.goa.error; view=default","type":"object","properties":{"fault":{"type":"boolean","description":"Is the error a server-side fault?","example":false},"id":{"type":"string","description":"ID is a unique identifier for this particular occurrence of the problem.","example":"123abc"},"message":{"type":"string","description":"Message is a human-readable explanation specific to this occurrence of the problem.","example":"parameter 'p' must be an integer"},"name":{"type":"string","description":"Name is the name of this class of errors.","example":"bad_request"},"temporary":{"type":"boolean","description":"Is the error temporary?","example":false},"timeout":{"type":"boolean","description":"Is the error a timeout?","example":false}},"description":"LoginGoogle_unauthorized_Response_Body result type (default view)","example":{"fault":true,"id":"123abc","message":"parameter 'p' must be an integer","name":"bad_request","temporary":true,"timeout":true},"required":["name","id","message","temporary","timeout","fault"]},"WebLogoutUnauthorizedResponseBody":{"title":"Mediatype identifier: application/vnd.goa.error; view=default","type":"object","properties":{"fault":{"type":"boolean","description":"Is the error a server-side fault?","example":true},"id":{"type":"string","description":"ID is a unique identifier for this particular occurrence of the problem.","example":"123abc"},"message":{"type":"string","description":"Message is a human-readable explanation specific to this occurrence of the problem.","example":"parameter 'p' must be an integer"},"name":{"type":"string","description":"Name is the name of this class of errors.","example":"bad_request"},"temporary":{"type":"boolean","description":"Is the error temporary?","example":false},"timeout":{"type":"boolean","description":"Is the error a timeout?","example":true}},"description":"Logout_unauthorized_Response_Body result type (default view)","example":{"fault":false,"id":"123abc","message":"parameter 'p' must be an integer","name":"bad_request","temporary":false,"timeout":true},"required":["name","id","message","temporary","timeout","fault"]},"WebSessionTokenResponseBody":{"title":"WebSessionTokenResponseBody","type":"object","properties":{"token":{"type":"string","example":"Ex eos tempora eum."}},"example":{"token":"Optio aut est."},"required":["token"]},"WebSessionTokenUnauthorizedResponseBody":{"title":"Mediatype identifier: application/vnd.goa.error; view=default","type":"object","properties":{"fault":{"type":"boolean","description":"Is the error a server-side fault?","example":false},"id":{"type":"string","description":"ID is a unique identifier for this particular occurrence of the problem.","example":"123abc"},"message":{"type":"string","description":"Message is a human-readable explanation specific to this occurrence of the problem.","example":"parameter 'p' must be an integer"},"name":{"type":"string","description":"Name is the name of this class of errors.","example":"bad_request"},"temporary":{"type":"boolean","description":"Is the error temporary?","example":false},"timeout":{"type":"boolean","description":"Is the error a timeout?","example":true}},"description":"SessionToken_unauthorized_Response_Body result type (default view)","example":{"fault":true,"id":"123abc","message":"parameter 'p' must be an integer","name":"bad_request","temporary":false,"timeout":false},"required":["name","id","message","temporary","timeout","fault"]}},"securityDefinitions":{"jwt_header_Authorization":{"type":"apiKey","description":"\n**Security Scopes**:\n * `api`: no description","name":"Authorization","in":"header"}}} \ No newline at end of file diff --git a/app/api/v1/gen/http/openapi.yaml b/app/api/v1/gen/http/openapi.yaml index 9ccd634..1480948 100644 --- a/app/api/v1/gen/http/openapi.yaml +++ b/app/api/v1/gen/http/openapi.yaml @@ -17,8 +17,8 @@ paths: get: tags: - web - summary: index web - operationId: web#index + summary: Index web + operationId: web#Index produces: - text/html responses: @@ -27,14 +27,18 @@ paths: schema: type: string format: byte + "401": + description: Unauthorized response. + schema: + $ref: '#/definitions/WebIndexUnauthorizedResponseBody' schemes: - http /another: get: tags: - web - summary: another web - operationId: web#another + summary: Another web + operationId: web#Another produces: - text/html responses: @@ -43,14 +47,58 @@ paths: schema: type: string format: byte + "401": + description: Unauthorized response. + schema: + $ref: '#/definitions/WebAnotherUnauthorizedResponseBody' + schemes: + - http + /api/v1/auth/token: + post: + tags: + - api + summary: AuthToken api + operationId: api#AuthToken + parameters: + - name: AuthTokenRequestBody + in: body + required: true + schema: + $ref: '#/definitions/APIAuthTokenRequestBody' + required: + - provider + - access_token + responses: + "200": + description: OK response. + schema: + $ref: '#/definitions/APIAuthTokenResponseBody' + required: + - token + "401": + description: Unauthorized response. + schema: + $ref: '#/definitions/APIAuthTokenUnauthorizedResponseBody' + "429": + description: Too Many Requests response. + schema: + $ref: '#/definitions/APIAuthTokenExistingIncrementRequestResponseBody' schemes: - http - /counter: + /api/v1/counter: get: tags: - api summary: CounterGet api + description: |4- + **Required security scopes for jwt**: + * `api` operationId: api#CounterGet + parameters: + - name: Authorization + in: header + required: true + type: string responses: "200": description: OK response. @@ -66,13 +114,21 @@ paths: $ref: '#/definitions/APICounterGetExistingIncrementRequestResponseBody' schemes: - http - /counter/inc: + security: + - jwt_header_Authorization: [] post: tags: - api summary: CounterIncrement api + description: |4- + **Required security scopes for jwt**: + * `api` operationId: api#CounterIncrement parameters: + - name: Authorization + in: header + required: true + type: string - name: CounterIncrementRequestBody in: body required: true @@ -95,7 +151,9 @@ paths: $ref: '#/definitions/APICounterIncrementExistingIncrementRequestResponseBody' schemes: - http - /echo: + security: + - jwt_header_Authorization: [] + /api/v1/echo: post: tags: - api @@ -126,12 +184,12 @@ paths: $ref: '#/definitions/APIEchoExistingIncrementRequestResponseBody' schemes: - http - /openapi.json: + /api/v1/openapi.json: get: tags: - api summary: Download gen/http/openapi3.json - operationId: api#/openapi.json + operationId: api#/api/v1/openapi.json responses: "200": description: File downloaded @@ -139,38 +197,112 @@ paths: type: file schemes: - http - /static/{path}: + /login/google: get: tags: - web - summary: Download static/ - operationId: web#/static/{*path} + summary: LoginGoogle web + operationId: web#LoginGoogle + responses: + "302": + description: Found response. + headers: + Location: + type: string + "401": + description: Unauthorized response. + schema: + $ref: '#/definitions/WebLoginGoogleUnauthorizedResponseBody' + schemes: + - http + /login/google/callback: + get: + tags: + - web + summary: LoginGoogleCallback web + operationId: web#LoginGoogleCallback parameters: - - name: path - in: path - description: Relative file path + - name: code + in: query + required: true + type: string + - name: state + in: query required: true type: string + responses: + "302": + description: Found response. + headers: + Location: + type: string + "401": + description: Unauthorized response. + schema: + $ref: '#/definitions/WebLoginGoogleCallbackUnauthorizedResponseBody' + schemes: + - http + /logout: + get: + tags: + - web + summary: Logout web + operationId: web#Logout + responses: + "302": + description: Found response. + headers: + Location: + type: string + "401": + description: Unauthorized response. + schema: + $ref: '#/definitions/WebLogoutUnauthorizedResponseBody' + schemes: + - http + /session/token: + get: + tags: + - web + summary: SessionToken web + operationId: web#SessionToken + produces: + - application/json + responses: + "200": + description: OK response. + schema: + $ref: '#/definitions/WebSessionTokenResponseBody' + required: + - token + "401": + description: Unauthorized response. + schema: + $ref: '#/definitions/WebSessionTokenUnauthorizedResponseBody' + schemes: + - http + /static/*: + get: + tags: + - web + summary: Download static/ + operationId: web#/static/* responses: "200": description: File downloaded schema: type: file - "404": - description: File not found - schema: - $ref: '#/definitions/Error' schemes: - http definitions: - APICounterGetExistingIncrementRequestResponseBody: + APIAuthTokenExistingIncrementRequestResponseBody: title: 'Mediatype identifier: application/vnd.goa.error; view=default' type: object properties: fault: type: boolean description: Is the error a server-side fault? - example: false + example: true id: type: string description: ID is a unique identifier for this particular occurrence of the problem. @@ -191,14 +323,14 @@ definitions: type: boolean description: Is the error a timeout? example: true - description: CounterGet_existing_increment_request_Response_Body result type (default view) + description: AuthToken_existing_increment_request_Response_Body result type (default view) example: fault: true id: 123abc message: parameter 'p' must be an integer name: bad_request temporary: false - timeout: false + timeout: true required: - name - id @@ -206,7 +338,36 @@ definitions: - temporary - timeout - fault - APICounterGetUnauthorizedResponseBody: + APIAuthTokenRequestBody: + title: APIAuthTokenRequestBody + type: object + properties: + access_token: + type: string + example: Aut culpa quo sit dolor aperiam consequatur. + provider: + type: string + example: google + enum: + - google + example: + access_token: Harum iusto quas. + provider: google + required: + - provider + - access_token + APIAuthTokenResponseBody: + title: APIAuthTokenResponseBody + type: object + properties: + token: + type: string + example: Illo vel qui ipsa adipisci. + example: + token: Quam eaque ipsa eius quaerat. + required: + - token + APIAuthTokenUnauthorizedResponseBody: title: 'Mediatype identifier: application/vnd.goa.error; view=default' type: object properties: @@ -226,6 +387,49 @@ definitions: type: string description: Name is the name of this class of errors. example: bad_request + temporary: + type: boolean + description: Is the error temporary? + example: true + timeout: + type: boolean + description: Is the error a timeout? + example: true + description: AuthToken_unauthorized_Response_Body result type (default view) + example: + fault: true + id: 123abc + message: parameter 'p' must be an integer + name: bad_request + temporary: true + timeout: false + required: + - name + - id + - message + - temporary + - timeout + - fault + APICounterGetExistingIncrementRequestResponseBody: + title: 'Mediatype identifier: application/vnd.goa.error; view=default' + type: object + properties: + fault: + type: boolean + description: Is the error a server-side fault? + example: false + id: + type: string + description: ID is a unique identifier for this particular occurrence of the problem. + example: 123abc + message: + type: string + description: Message is a human-readable explanation specific to this occurrence of the problem. + example: parameter 'p' must be an integer + name: + type: string + description: Name is the name of this class of errors. + example: bad_request temporary: type: boolean description: Is the error temporary? @@ -234,14 +438,57 @@ definitions: type: boolean description: Is the error a timeout? example: true - description: CounterGet_unauthorized_Response_Body result type (default view) + description: CounterGet_existing_increment_request_Response_Body result type (default view) example: fault: false id: 123abc message: parameter 'p' must be an integer name: bad_request temporary: false - timeout: false + timeout: true + required: + - name + - id + - message + - temporary + - timeout + - fault + APICounterGetUnauthorizedResponseBody: + title: 'Mediatype identifier: application/vnd.goa.error; view=default' + type: object + properties: + fault: + type: boolean + description: Is the error a server-side fault? + example: true + id: + type: string + description: ID is a unique identifier for this particular occurrence of the problem. + example: 123abc + message: + type: string + description: Message is a human-readable explanation specific to this occurrence of the problem. + example: parameter 'p' must be an integer + name: + type: string + description: Name is the name of this class of errors. + example: bad_request + temporary: + type: boolean + description: Is the error temporary? + example: true + timeout: + type: boolean + description: Is the error a timeout? + example: true + description: CounterGet_unauthorized_Response_Body result type (default view) + example: + fault: true + id: 123abc + message: parameter 'p' must be an integer + name: bad_request + temporary: true + timeout: true required: - name - id @@ -298,9 +545,9 @@ definitions: properties: user: type: string - example: Non perspiciatis eum dicta sit. + example: Voluptas quae perspiciatis illo suscipit. example: - user: Odio tenetur temporibus. + user: Nesciunt vero repudiandae. required: - user APICounterIncrementUnauthorizedResponseBody: @@ -310,7 +557,7 @@ definitions: fault: type: boolean description: Is the error a server-side fault? - example: false + example: true id: type: string description: ID is a unique identifier for this particular occurrence of the problem. @@ -333,11 +580,11 @@ definitions: example: true description: CounterIncrement_unauthorized_Response_Body result type (default view) example: - fault: false + fault: true id: 123abc message: parameter 'p' must be an integer name: bad_request - temporary: false + temporary: true timeout: false required: - name @@ -369,11 +616,11 @@ definitions: temporary: type: boolean description: Is the error temporary? - example: true + example: false timeout: type: boolean description: Is the error a timeout? - example: false + example: true description: Echo_existing_increment_request_Response_Body result type (default view) example: fault: true @@ -395,9 +642,9 @@ definitions: properties: text: type: string - example: Deleniti necessitatibus numquam perspiciatis quia ipsa quam. + example: Labore ut aut possimus et. example: - text: Nemo dolorem ullam magnam. + text: Magni harum atque dolor quod at consectetur. required: - text APIEchoResponseBody: @@ -406,9 +653,9 @@ definitions: properties: text: type: string - example: Praesentium qui debitis. + example: Id accusamus quam aut. example: - text: Odit cum blanditiis ut. + text: Voluptatem ipsam et dolorem quos nihil assumenda. required: - text APIEchoUnauthorizedResponseBody: @@ -418,7 +665,7 @@ definitions: fault: type: boolean description: Is the error a server-side fault? - example: true + example: false id: type: string description: ID is a unique identifier for this particular occurrence of the problem. @@ -438,7 +685,7 @@ definitions: timeout: type: boolean description: Is the error a timeout? - example: false + example: true description: Echo_unauthorized_Response_Body result type (default view) example: fault: false @@ -446,7 +693,7 @@ definitions: message: parameter 'p' must be an integer name: bad_request temporary: false - timeout: true + timeout: false required: - name - id @@ -460,29 +707,72 @@ definitions: properties: count: type: integer - example: 1315490442 + example: 718974130 format: int32 last_increment_at: type: string - example: Eligendi quisquam. + example: Consequuntur cupiditate pariatur aut placeat. last_increment_by: type: string - example: Vero molestiae. + example: Ipsam nesciunt minima cupiditate. next_finalize_at: type: string - example: Aut in dolor eum consequatur. + example: Amet voluptas ex nostrum aperiam explicabo sed. description: CounterGetResponseBody result type (default view) example: - count: 1162746042 - last_increment_at: Tempore asperiores. - last_increment_by: Tempora repellendus. - next_finalize_at: Quae voluptatibus dolor fugit quia. + count: 837784502 + last_increment_at: Eius rerum. + last_increment_by: Sint asperiores sed voluptas voluptatem. + next_finalize_at: Accusamus maiores ut voluptas. required: - count - last_increment_by - last_increment_at - next_finalize_at - Error: + WebAnotherUnauthorizedResponseBody: + title: 'Mediatype identifier: application/vnd.goa.error; view=default' + type: object + properties: + fault: + type: boolean + description: Is the error a server-side fault? + example: true + id: + type: string + description: ID is a unique identifier for this particular occurrence of the problem. + example: 123abc + message: + type: string + description: Message is a human-readable explanation specific to this occurrence of the problem. + example: parameter 'p' must be an integer + name: + type: string + description: Name is the name of this class of errors. + example: bad_request + temporary: + type: boolean + description: Is the error temporary? + example: false + timeout: + type: boolean + description: Is the error a timeout? + example: true + description: Another_unauthorized_Response_Body result type (default view) + example: + fault: true + id: 123abc + message: parameter 'p' must be an integer + name: bad_request + temporary: false + timeout: true + required: + - name + - id + - message + - temporary + - timeout + - fault + WebIndexUnauthorizedResponseBody: title: 'Mediatype identifier: application/vnd.goa.error; view=default' type: object properties: @@ -510,13 +800,142 @@ definitions: type: boolean description: Is the error a timeout? example: false - description: Error response result type (default view) + description: Index_unauthorized_Response_Body result type (default view) + example: + fault: false + id: 123abc + message: parameter 'p' must be an integer + name: bad_request + temporary: true + timeout: false + required: + - name + - id + - message + - temporary + - timeout + - fault + WebLoginGoogleCallbackUnauthorizedResponseBody: + title: 'Mediatype identifier: application/vnd.goa.error; view=default' + type: object + properties: + fault: + type: boolean + description: Is the error a server-side fault? + example: false + id: + type: string + description: ID is a unique identifier for this particular occurrence of the problem. + example: 123abc + message: + type: string + description: Message is a human-readable explanation specific to this occurrence of the problem. + example: parameter 'p' must be an integer + name: + type: string + description: Name is the name of this class of errors. + example: bad_request + temporary: + type: boolean + description: Is the error temporary? + example: true + timeout: + type: boolean + description: Is the error a timeout? + example: true + description: LoginGoogleCallback_unauthorized_Response_Body result type (default view) example: fault: true id: 123abc message: parameter 'p' must be an integer name: bad_request temporary: true + timeout: false + required: + - name + - id + - message + - temporary + - timeout + - fault + WebLoginGoogleUnauthorizedResponseBody: + title: 'Mediatype identifier: application/vnd.goa.error; view=default' + type: object + properties: + fault: + type: boolean + description: Is the error a server-side fault? + example: false + id: + type: string + description: ID is a unique identifier for this particular occurrence of the problem. + example: 123abc + message: + type: string + description: Message is a human-readable explanation specific to this occurrence of the problem. + example: parameter 'p' must be an integer + name: + type: string + description: Name is the name of this class of errors. + example: bad_request + temporary: + type: boolean + description: Is the error temporary? + example: false + timeout: + type: boolean + description: Is the error a timeout? + example: false + description: LoginGoogle_unauthorized_Response_Body result type (default view) + example: + fault: true + id: 123abc + message: parameter 'p' must be an integer + name: bad_request + temporary: true + timeout: true + required: + - name + - id + - message + - temporary + - timeout + - fault + WebLogoutUnauthorizedResponseBody: + title: 'Mediatype identifier: application/vnd.goa.error; view=default' + type: object + properties: + fault: + type: boolean + description: Is the error a server-side fault? + example: true + id: + type: string + description: ID is a unique identifier for this particular occurrence of the problem. + example: 123abc + message: + type: string + description: Message is a human-readable explanation specific to this occurrence of the problem. + example: parameter 'p' must be an integer + name: + type: string + description: Name is the name of this class of errors. + example: bad_request + temporary: + type: boolean + description: Is the error temporary? + example: false + timeout: + type: boolean + description: Is the error a timeout? + example: true + description: Logout_unauthorized_Response_Body result type (default view) + example: + fault: false + id: 123abc + message: parameter 'p' must be an integer + name: bad_request + temporary: false timeout: true required: - name @@ -525,3 +944,65 @@ definitions: - temporary - timeout - fault + WebSessionTokenResponseBody: + title: WebSessionTokenResponseBody + type: object + properties: + token: + type: string + example: Ex eos tempora eum. + example: + token: Optio aut est. + required: + - token + WebSessionTokenUnauthorizedResponseBody: + title: 'Mediatype identifier: application/vnd.goa.error; view=default' + type: object + properties: + fault: + type: boolean + description: Is the error a server-side fault? + example: false + id: + type: string + description: ID is a unique identifier for this particular occurrence of the problem. + example: 123abc + message: + type: string + description: Message is a human-readable explanation specific to this occurrence of the problem. + example: parameter 'p' must be an integer + name: + type: string + description: Name is the name of this class of errors. + example: bad_request + temporary: + type: boolean + description: Is the error temporary? + example: false + timeout: + type: boolean + description: Is the error a timeout? + example: true + description: SessionToken_unauthorized_Response_Body result type (default view) + example: + fault: true + id: 123abc + message: parameter 'p' must be an integer + name: bad_request + temporary: false + timeout: false + required: + - name + - id + - message + - temporary + - timeout + - fault +securityDefinitions: + jwt_header_Authorization: + type: apiKey + description: |4- + **Security Scopes**: + * `api`: no description + name: Authorization + in: header diff --git a/app/api/v1/gen/http/openapi3.json b/app/api/v1/gen/http/openapi3.json index 0787a4c..888cd54 100644 --- a/app/api/v1/gen/http/openapi3.json +++ b/app/api/v1/gen/http/openapi3.json @@ -1 +1 @@ -{"openapi":"3.0.3","info":{"title":"Count Up","description":"A production-ready Go service deployed on Kubernetes","version":"1.0.0"},"servers":[{"url":"http://localhost:8080"},{"url":"http://localhost:80"}],"paths":{"/":{"get":{"tags":["web"],"summary":"index web","operationId":"web#index","responses":{"200":{"description":"OK response.","content":{"text/html":{"schema":{"type":"string","example":"TmVtbyBtYWduaSBkdWNpbXVzIGltcGVkaXQu","format":"binary"},"example":"TW9sbGl0aWEgZGVsZW5pdGkgZXhwZWRpdGEgYWNjdXNhbnRpdW0u"}}}}}},"/another":{"get":{"tags":["web"],"summary":"another web","operationId":"web#another","responses":{"200":{"description":"OK response.","content":{"text/html":{"schema":{"type":"string","example":"TnVtcXVhbSBkb2xvcmlidXMgc3VzY2lwaXQu","format":"binary"},"example":"SW52ZW50b3JlIGxhYm9yZSBlcnJvciBhbmltaS4="}}}}}},"/counter":{"get":{"tags":["api"],"summary":"CounterGet api","operationId":"api#CounterGet","responses":{"200":{"description":"OK response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CounterInfo"},"example":{"count":1425094436,"last_increment_at":"Iste et distinctio accusantium.","last_increment_by":"Sequi ipsa aliquam esse.","next_finalize_at":"A non."}}}},"401":{"description":"unauthorized: Unauthorized response.","content":{"application/vnd.goa.error":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"existing_increment_request: Too Many Requests response.","content":{"application/vnd.goa.error":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/counter/inc":{"post":{"tags":["api"],"summary":"CounterIncrement api","operationId":"api#CounterIncrement","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CounterIncrementRequestBody"},"example":{"user":"Nihil doloribus et sed sequi consequatur."}}}},"responses":{"202":{"description":"Accepted response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CounterInfo"},"example":{"count":278214526,"last_increment_at":"Inventore accusantium.","last_increment_by":"Laborum vel mollitia aut.","next_finalize_at":"Voluptas ut eius."}}}},"401":{"description":"unauthorized: Unauthorized response.","content":{"application/vnd.goa.error":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"existing_increment_request: Too Many Requests response.","content":{"application/vnd.goa.error":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/echo":{"post":{"tags":["api"],"summary":"Echo api","operationId":"api#Echo","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EchoRequestBody"},"example":{"text":"Vel omnis quo sit."}}}},"responses":{"200":{"description":"OK response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EchoRequestBody"},"example":{"text":"Distinctio illo."}}}},"401":{"description":"unauthorized: Unauthorized response.","content":{"application/vnd.goa.error":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"existing_increment_request: Too Many Requests response.","content":{"application/vnd.goa.error":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/openapi.json":{"get":{"tags":["api"],"summary":"Download gen/http/openapi3.json","operationId":"api#/openapi.json","responses":{"200":{"description":"File downloaded"}}}},"/static/{*path}":{"get":{"tags":["web"],"summary":"Download static/","operationId":"web#/static/{*path}","parameters":[{"name":"path","in":"path","description":"Relative file path","required":true}],"responses":{"200":{"description":"File not found"},"404":{"description":"File not found"}}}}},"components":{"schemas":{"CounterIncrementRequestBody":{"type":"object","properties":{"user":{"type":"string","example":"Omnis debitis."}},"example":{"user":"Eos minima dolorem id sunt voluptates voluptas."},"required":["user"]},"CounterInfo":{"type":"object","properties":{"count":{"type":"integer","example":495281550,"format":"int32"},"last_increment_at":{"type":"string","example":"Aut tenetur eos."},"last_increment_by":{"type":"string","example":"Consectetur odio."},"next_finalize_at":{"type":"string","example":"Laborum et veniam et illum quaerat et."}},"example":{"count":1773148578,"last_increment_at":"Voluptatum omnis possimus saepe deleniti.","last_increment_by":"Corporis est sunt voluptatem reprehenderit neque modi.","next_finalize_at":"Rerum facere veritatis."},"required":["count","last_increment_by","last_increment_at","next_finalize_at"]},"EchoRequestBody":{"type":"object","properties":{"text":{"type":"string","example":"Cumque maxime dolore hic laboriosam."}},"example":{"text":"Aut recusandae cum."},"required":["text"]},"Error":{"type":"object","properties":{"fault":{"type":"boolean","description":"Is the error a server-side fault?","example":true},"id":{"type":"string","description":"ID is a unique identifier for this particular occurrence of the problem.","example":"123abc"},"message":{"type":"string","description":"Message is a human-readable explanation specific to this occurrence of the problem.","example":"parameter 'p' must be an integer"},"name":{"type":"string","description":"Name is the name of this class of errors.","example":"bad_request"},"temporary":{"type":"boolean","description":"Is the error temporary?","example":true},"timeout":{"type":"boolean","description":"Is the error a timeout?","example":false}},"example":{"fault":false,"id":"123abc","message":"parameter 'p' must be an integer","name":"bad_request","temporary":true,"timeout":false},"required":["name","id","message","temporary","timeout","fault"]}}},"tags":[{"name":"api"},{"name":"web"}]} \ No newline at end of file +{"openapi":"3.0.3","info":{"title":"Count Up","description":"A production-ready Go service deployed on Kubernetes","version":"1.0.0"},"servers":[{"url":"http://localhost:8080"},{"url":"http://localhost:80"}],"paths":{"/":{"get":{"tags":["web"],"summary":"Index web","operationId":"web#Index","responses":{"200":{"description":"OK response.","content":{"text/html":{"schema":{"type":"string","example":"Vm9sdXB0YXRlbSBsYWJvcnVtIG9mZmljaWlzLg==","format":"binary"},"example":"T3B0aW8gZXQu"}}},"401":{"description":"unauthorized: Unauthorized response.","content":{"application/vnd.goa.error":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/another":{"get":{"tags":["web"],"summary":"Another web","operationId":"web#Another","responses":{"200":{"description":"OK response.","content":{"text/html":{"schema":{"type":"string","example":"UmVwZWxsZW5kdXMgcXVvIGVycm9yLg==","format":"binary"},"example":"UGVyc3BpY2lhdGlzIHV0IGVzdCBxdW9kIG1hZ25pIG5lY2Vzc2l0YXRpYnVzIGVzdC4="}}},"401":{"description":"unauthorized: Unauthorized response.","content":{"application/vnd.goa.error":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/auth/token":{"post":{"tags":["api"],"summary":"AuthToken api","operationId":"api#AuthToken","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthTokenRequestBody"},"example":{"access_token":"Omnis cumque est asperiores dolorem.","provider":"google"}}}},"responses":{"200":{"description":"OK response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthTokenResponseBody"},"example":{"token":"Tempore laboriosam maiores autem."}}}},"401":{"description":"unauthorized: Unauthorized response.","content":{"application/vnd.goa.error":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"existing_increment_request: Too Many Requests response.","content":{"application/vnd.goa.error":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/counter":{"get":{"tags":["api"],"summary":"CounterGet api","operationId":"api#CounterGet","responses":{"200":{"description":"OK response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CounterInfo"},"example":{"count":1393021988,"last_increment_at":"Quia tenetur illum vitae vitae.","last_increment_by":"Culpa vel enim ut autem.","next_finalize_at":"Illo sapiente."}}}},"401":{"description":"unauthorized: Unauthorized response.","content":{"application/vnd.goa.error":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"existing_increment_request: Too Many Requests response.","content":{"application/vnd.goa.error":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"jwt_header_Authorization":["api"]}]},"post":{"tags":["api"],"summary":"CounterIncrement api","operationId":"api#CounterIncrement","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CounterIncrementRequestBody"},"example":{"user":"Quae cupiditate."}}}},"responses":{"202":{"description":"Accepted response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CounterInfo"},"example":{"count":198148861,"last_increment_at":"Aspernatur vero.","last_increment_by":"Non est qui velit corrupti aliquid placeat.","next_finalize_at":"Atque eligendi quisquam adipisci."}}}},"401":{"description":"unauthorized: Unauthorized response.","content":{"application/vnd.goa.error":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"existing_increment_request: Too Many Requests response.","content":{"application/vnd.goa.error":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"jwt_header_Authorization":["api"]}]}},"/api/v1/echo":{"post":{"tags":["api"],"summary":"Echo api","operationId":"api#Echo","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EchoRequestBody"},"example":{"text":"Tempora repellendus."}}}},"responses":{"200":{"description":"OK response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EchoRequestBody"},"example":{"text":"Tempore asperiores."}}}},"401":{"description":"unauthorized: Unauthorized response.","content":{"application/vnd.goa.error":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"existing_increment_request: Too Many Requests response.","content":{"application/vnd.goa.error":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/openapi.json":{"get":{"tags":["api"],"summary":"Download gen/http/openapi3.json","operationId":"api#/api/v1/openapi.json","responses":{"200":{"description":"File downloaded"}}}},"/login/google":{"get":{"tags":["web"],"summary":"LoginGoogle web","operationId":"web#LoginGoogle","responses":{"302":{"description":"Found response.","headers":{"Location":{"schema":{"type":"string","example":"Mollitia similique dignissimos."},"example":"Exercitationem omnis perferendis ipsa dolor eum."},"Set-Cookie":{"schema":{"type":"string","example":"Consequuntur quo excepturi eos dolor voluptatem."},"example":"Explicabo veritatis labore quidem deserunt enim qui."}}},"401":{"description":"unauthorized: Unauthorized response.","content":{"application/vnd.goa.error":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/login/google/callback":{"get":{"tags":["web"],"summary":"LoginGoogleCallback web","operationId":"web#LoginGoogleCallback","parameters":[{"name":"code","in":"query","allowEmptyValue":true,"required":true,"schema":{"type":"string","example":"Aut ab."},"example":"Officiis consequuntur mollitia provident recusandae."},{"name":"state","in":"query","allowEmptyValue":true,"required":true,"schema":{"type":"string","example":"Distinctio libero veritatis sint accusamus nulla."},"example":"Expedita sint inventore possimus cumque magni."},{"name":"countup.session","in":"cookie","allowEmptyValue":true,"required":true,"schema":{"type":"string","example":"Deleniti facere eius eos."},"example":"Facere tempora amet."}],"responses":{"302":{"description":"Found response.","headers":{"Location":{"schema":{"type":"string","example":"Omnis distinctio adipisci."},"example":"Soluta necessitatibus ad modi esse."},"Set-Cookie":{"schema":{"type":"string","example":"Iusto architecto corporis commodi aut minus."},"example":"Quo aut."}}},"401":{"description":"unauthorized: Unauthorized response.","content":{"application/vnd.goa.error":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/logout":{"get":{"tags":["web"],"summary":"Logout web","operationId":"web#Logout","parameters":[{"name":"countup.session","in":"cookie","allowEmptyValue":true,"required":true,"schema":{"type":"string","example":"Illo qui sit tenetur accusamus tempore laboriosam."},"example":"Beatae voluptatibus ut qui facilis."}],"responses":{"302":{"description":"Found response.","headers":{"Location":{"schema":{"type":"string","example":"Ea voluptas sit consectetur alias libero saepe."},"example":"Error earum ex."},"Set-Cookie":{"schema":{"type":"string","example":"Necessitatibus cupiditate molestiae hic sint consequatur ipsa."},"example":"Molestiae asperiores omnis laborum ea voluptatum dignissimos."}}},"401":{"description":"unauthorized: Unauthorized response.","content":{"application/vnd.goa.error":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/session/token":{"get":{"tags":["web"],"summary":"SessionToken web","operationId":"web#SessionToken","parameters":[{"name":"countup.session","in":"cookie","allowEmptyValue":true,"required":true,"schema":{"type":"string","example":"Aut omnis atque qui quae voluptatem quibusdam."},"example":"Voluptas at rerum et alias."}],"responses":{"200":{"description":"OK response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthTokenResponseBody"},"example":{"token":"Itaque laborum et."}}}},"401":{"description":"unauthorized: Unauthorized response.","content":{"application/vnd.goa.error":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/static/*":{"get":{"tags":["web"],"summary":"Download static/","operationId":"web#/static/*","responses":{"200":{"description":"File downloaded"}}}}},"components":{"schemas":{"AuthTokenRequestBody":{"type":"object","properties":{"access_token":{"type":"string","example":"Unde eligendi et sint velit."},"provider":{"type":"string","example":"google","enum":["google"]}},"example":{"access_token":"Eius quasi fugiat quia facilis sint eligendi.","provider":"google"},"required":["provider","access_token"]},"AuthTokenResponseBody":{"type":"object","properties":{"token":{"type":"string","example":"Sit impedit."}},"example":{"token":"Nulla corrupti non expedita aliquam illo cum."},"required":["token"]},"CounterIncrementRequestBody":{"type":"object","properties":{"user":{"type":"string","example":"Atque illo sequi non aut dolorem."}},"example":{"user":"Doloribus voluptatem hic totam repudiandae."},"required":["user"]},"CounterInfo":{"type":"object","properties":{"count":{"type":"integer","example":271655255,"format":"int32"},"last_increment_at":{"type":"string","example":"Rerum sunt."},"last_increment_by":{"type":"string","example":"Perspiciatis laborum distinctio rem ipsam."},"next_finalize_at":{"type":"string","example":"Distinctio et rerum."}},"example":{"count":1309385137,"last_increment_at":"Sed velit.","last_increment_by":"Nihil totam ipsum.","next_finalize_at":"Itaque temporibus eius voluptatem aliquam aut mollitia."},"required":["count","last_increment_by","last_increment_at","next_finalize_at"]},"EchoRequestBody":{"type":"object","properties":{"text":{"type":"string","example":"Enim alias sunt."}},"example":{"text":"Dolores velit."},"required":["text"]},"Error":{"type":"object","properties":{"fault":{"type":"boolean","description":"Is the error a server-side fault?","example":true},"id":{"type":"string","description":"ID is a unique identifier for this particular occurrence of the problem.","example":"123abc"},"message":{"type":"string","description":"Message is a human-readable explanation specific to this occurrence of the problem.","example":"parameter 'p' must be an integer"},"name":{"type":"string","description":"Name is the name of this class of errors.","example":"bad_request"},"temporary":{"type":"boolean","description":"Is the error temporary?","example":true},"timeout":{"type":"boolean","description":"Is the error a timeout?","example":true}},"example":{"fault":true,"id":"123abc","message":"parameter 'p' must be an integer","name":"bad_request","temporary":true,"timeout":false},"required":["name","id","message","temporary","timeout","fault"]}},"securitySchemes":{"jwt_header_Authorization":{"type":"http","scheme":"bearer"}}},"tags":[{"name":"api"},{"name":"web"}]} \ No newline at end of file diff --git a/app/api/v1/gen/http/openapi3.yaml b/app/api/v1/gen/http/openapi3.yaml index 659f61e..a6243d1 100644 --- a/app/api/v1/gen/http/openapi3.yaml +++ b/app/api/v1/gen/http/openapi3.yaml @@ -11,8 +11,8 @@ paths: get: tags: - web - summary: index web - operationId: web#index + summary: Index web + operationId: web#Index responses: "200": description: OK response. @@ -21,80 +21,57 @@ paths: schema: type: string example: - - 78 + - 86 + - 111 + - 108 + - 117 + - 112 + - 116 + - 97 + - 116 - 101 - 109 - - 111 - 32 - - 109 + - 108 - 97 - - 103 - - 110 - - 105 - - 32 - - 100 + - 98 + - 111 + - 114 - 117 - - 99 - - 105 - 109 - - 117 - - 115 - 32 + - 111 + - 102 + - 102 - 105 - - 109 - - 112 - - 101 - - 100 + - 99 - 105 - - 116 + - 105 + - 115 - 46 format: binary example: - - 77 - - 111 - - 108 - - 108 - - 105 - - 116 - - 105 - - 97 - - 32 - - 100 - - 101 - - 108 - - 101 - - 110 - - 105 + - 79 + - 112 - 116 - 105 + - 111 - 32 - 101 - - 120 - - 112 - - 101 - - 100 - - 105 - 116 - - 97 - - 32 - - 97 - - 99 - - 99 - - 117 - - 115 - - 97 - - 110 - - 116 - - 105 - - 117 - - 109 - 46 + "401": + description: 'unauthorized: Unauthorized response.' + content: + application/vnd.goa.error: + schema: + $ref: '#/components/schemas/Error' /another: get: tags: - web - summary: another web - operationId: web#another + summary: Another web + operationId: web#Another responses: "200": description: OK response. @@ -103,65 +80,123 @@ paths: schema: type: string example: - - 78 + - 82 + - 101 + - 112 + - 101 + - 108 + - 108 + - 101 + - 110 + - 100 - 117 - - 109 + - 115 + - 32 - 113 - 117 - - 97 - - 109 - - 32 - - 100 - 111 - - 108 + - 32 + - 101 + - 114 + - 114 - 111 - 114 - - 105 - - 98 - - 117 - - 115 - - 32 - - 115 - - 117 - - 115 - - 99 - - 105 - - 112 - - 105 - - 116 - 46 format: binary example: - - 73 - - 110 - - 118 + - 80 - 101 - - 110 - - 116 - - 111 - 114 - - 101 - - 32 - - 108 + - 115 + - 112 + - 105 + - 99 + - 105 - 97 - - 98 - - 111 - - 114 - - 101 + - 116 + - 105 + - 115 + - 32 + - 117 + - 116 - 32 - 101 - - 114 - - 114 + - 115 + - 116 + - 32 + - 113 + - 117 - 111 - - 114 + - 100 - 32 + - 109 - 97 + - 103 - 110 - 105 - - 109 + - 32 + - 110 + - 101 + - 99 + - 101 + - 115 + - 115 - 105 + - 116 + - 97 + - 116 + - 105 + - 98 + - 117 + - 115 + - 32 + - 101 + - 115 + - 116 - 46 - /counter: + "401": + description: 'unauthorized: Unauthorized response.' + content: + application/vnd.goa.error: + schema: + $ref: '#/components/schemas/Error' + /api/v1/auth/token: + post: + tags: + - api + summary: AuthToken api + operationId: api#AuthToken + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/AuthTokenRequestBody' + example: + access_token: Omnis cumque est asperiores dolorem. + provider: google + responses: + "200": + description: OK response. + content: + application/json: + schema: + $ref: '#/components/schemas/AuthTokenResponseBody' + example: + token: Tempore laboriosam maiores autem. + "401": + description: 'unauthorized: Unauthorized response.' + content: + application/vnd.goa.error: + schema: + $ref: '#/components/schemas/Error' + "429": + description: 'existing_increment_request: Too Many Requests response.' + content: + application/vnd.goa.error: + schema: + $ref: '#/components/schemas/Error' + /api/v1/counter: get: tags: - api @@ -175,10 +210,10 @@ paths: schema: $ref: '#/components/schemas/CounterInfo' example: - count: 1425094436 - last_increment_at: Iste et distinctio accusantium. - last_increment_by: Sequi ipsa aliquam esse. - next_finalize_at: A non. + count: 1393021988 + last_increment_at: Quia tenetur illum vitae vitae. + last_increment_by: Culpa vel enim ut autem. + next_finalize_at: Illo sapiente. "401": description: 'unauthorized: Unauthorized response.' content: @@ -191,7 +226,9 @@ paths: application/vnd.goa.error: schema: $ref: '#/components/schemas/Error' - /counter/inc: + security: + - jwt_header_Authorization: + - api post: tags: - api @@ -204,7 +241,7 @@ paths: schema: $ref: '#/components/schemas/CounterIncrementRequestBody' example: - user: Nihil doloribus et sed sequi consequatur. + user: Quae cupiditate. responses: "202": description: Accepted response. @@ -213,10 +250,10 @@ paths: schema: $ref: '#/components/schemas/CounterInfo' example: - count: 278214526 - last_increment_at: Inventore accusantium. - last_increment_by: Laborum vel mollitia aut. - next_finalize_at: Voluptas ut eius. + count: 198148861 + last_increment_at: Aspernatur vero. + last_increment_by: Non est qui velit corrupti aliquid placeat. + next_finalize_at: Atque eligendi quisquam adipisci. "401": description: 'unauthorized: Unauthorized response.' content: @@ -229,7 +266,10 @@ paths: application/vnd.goa.error: schema: $ref: '#/components/schemas/Error' - /echo: + security: + - jwt_header_Authorization: + - api + /api/v1/echo: post: tags: - api @@ -242,7 +282,7 @@ paths: schema: $ref: '#/components/schemas/EchoRequestBody' example: - text: Vel omnis quo sit. + text: Tempora repellendus. responses: "200": description: OK response. @@ -251,7 +291,7 @@ paths: schema: $ref: '#/components/schemas/EchoRequestBody' example: - text: Distinctio illo. + text: Tempore asperiores. "401": description: 'unauthorized: Unauthorized response.' content: @@ -264,41 +304,203 @@ paths: application/vnd.goa.error: schema: $ref: '#/components/schemas/Error' - /openapi.json: + /api/v1/openapi.json: get: tags: - api summary: Download gen/http/openapi3.json - operationId: api#/openapi.json + operationId: api#/api/v1/openapi.json responses: "200": description: File downloaded - /static/{*path}: + /login/google: get: tags: - web - summary: Download static/ - operationId: web#/static/{*path} + summary: LoginGoogle web + operationId: web#LoginGoogle + responses: + "302": + description: Found response. + headers: + Location: + schema: + type: string + example: Mollitia similique dignissimos. + example: Exercitationem omnis perferendis ipsa dolor eum. + Set-Cookie: + schema: + type: string + example: Consequuntur quo excepturi eos dolor voluptatem. + example: Explicabo veritatis labore quidem deserunt enim qui. + "401": + description: 'unauthorized: Unauthorized response.' + content: + application/vnd.goa.error: + schema: + $ref: '#/components/schemas/Error' + /login/google/callback: + get: + tags: + - web + summary: LoginGoogleCallback web + operationId: web#LoginGoogleCallback parameters: - - name: path - in: path - description: Relative file path + - name: code + in: query + allowEmptyValue: true + required: true + schema: + type: string + example: Aut ab. + example: Officiis consequuntur mollitia provident recusandae. + - name: state + in: query + allowEmptyValue: true required: true + schema: + type: string + example: Distinctio libero veritatis sint accusamus nulla. + example: Expedita sint inventore possimus cumque magni. + - name: countup.session + in: cookie + allowEmptyValue: true + required: true + schema: + type: string + example: Deleniti facere eius eos. + example: Facere tempora amet. + responses: + "302": + description: Found response. + headers: + Location: + schema: + type: string + example: Omnis distinctio adipisci. + example: Soluta necessitatibus ad modi esse. + Set-Cookie: + schema: + type: string + example: Iusto architecto corporis commodi aut minus. + example: Quo aut. + "401": + description: 'unauthorized: Unauthorized response.' + content: + application/vnd.goa.error: + schema: + $ref: '#/components/schemas/Error' + /logout: + get: + tags: + - web + summary: Logout web + operationId: web#Logout + parameters: + - name: countup.session + in: cookie + allowEmptyValue: true + required: true + schema: + type: string + example: Illo qui sit tenetur accusamus tempore laboriosam. + example: Beatae voluptatibus ut qui facilis. + responses: + "302": + description: Found response. + headers: + Location: + schema: + type: string + example: Ea voluptas sit consectetur alias libero saepe. + example: Error earum ex. + Set-Cookie: + schema: + type: string + example: Necessitatibus cupiditate molestiae hic sint consequatur ipsa. + example: Molestiae asperiores omnis laborum ea voluptatum dignissimos. + "401": + description: 'unauthorized: Unauthorized response.' + content: + application/vnd.goa.error: + schema: + $ref: '#/components/schemas/Error' + /session/token: + get: + tags: + - web + summary: SessionToken web + operationId: web#SessionToken + parameters: + - name: countup.session + in: cookie + allowEmptyValue: true + required: true + schema: + type: string + example: Aut omnis atque qui quae voluptatem quibusdam. + example: Voluptas at rerum et alias. responses: "200": - description: File not found - "404": - description: File not found + description: OK response. + content: + application/json: + schema: + $ref: '#/components/schemas/AuthTokenResponseBody' + example: + token: Itaque laborum et. + "401": + description: 'unauthorized: Unauthorized response.' + content: + application/vnd.goa.error: + schema: + $ref: '#/components/schemas/Error' + /static/*: + get: + tags: + - web + summary: Download static/ + operationId: web#/static/* + responses: + "200": + description: File downloaded components: schemas: + AuthTokenRequestBody: + type: object + properties: + access_token: + type: string + example: Unde eligendi et sint velit. + provider: + type: string + example: google + enum: + - google + example: + access_token: Eius quasi fugiat quia facilis sint eligendi. + provider: google + required: + - provider + - access_token + AuthTokenResponseBody: + type: object + properties: + token: + type: string + example: Sit impedit. + example: + token: Nulla corrupti non expedita aliquam illo cum. + required: + - token CounterIncrementRequestBody: type: object properties: user: type: string - example: Omnis debitis. + example: Atque illo sequi non aut dolorem. example: - user: Eos minima dolorem id sunt voluptates voluptas. + user: Doloribus voluptatem hic totam repudiandae. required: - user CounterInfo: @@ -306,22 +508,22 @@ components: properties: count: type: integer - example: 495281550 + example: 271655255 format: int32 last_increment_at: type: string - example: Aut tenetur eos. + example: Rerum sunt. last_increment_by: type: string - example: Consectetur odio. + example: Perspiciatis laborum distinctio rem ipsam. next_finalize_at: type: string - example: Laborum et veniam et illum quaerat et. + example: Distinctio et rerum. example: - count: 1773148578 - last_increment_at: Voluptatum omnis possimus saepe deleniti. - last_increment_by: Corporis est sunt voluptatem reprehenderit neque modi. - next_finalize_at: Rerum facere veritatis. + count: 1309385137 + last_increment_at: Sed velit. + last_increment_by: Nihil totam ipsum. + next_finalize_at: Itaque temporibus eius voluptatem aliquam aut mollitia. required: - count - last_increment_by @@ -332,9 +534,9 @@ components: properties: text: type: string - example: Cumque maxime dolore hic laboriosam. + example: Enim alias sunt. example: - text: Aut recusandae cum. + text: Dolores velit. required: - text Error: @@ -363,9 +565,9 @@ components: timeout: type: boolean description: Is the error a timeout? - example: false + example: true example: - fault: false + fault: true id: 123abc message: parameter 'p' must be an integer name: bad_request @@ -378,6 +580,10 @@ components: - temporary - timeout - fault + securitySchemes: + jwt_header_Authorization: + type: http + scheme: bearer tags: - name: api - name: web diff --git a/app/api/v1/gen/http/web/client/cli.go b/app/api/v1/gen/http/web/client/cli.go index fafe96a..5d68586 100644 --- a/app/api/v1/gen/http/web/client/cli.go +++ b/app/api/v1/gen/http/web/client/cli.go @@ -6,3 +6,56 @@ // $ goa gen github.com/jace-ys/countup/api/v1 -o api/v1 package client + +import ( + web "github.com/jace-ys/countup/api/v1/gen/web" +) + +// BuildLoginGoogleCallbackPayload builds the payload for the web +// LoginGoogleCallback endpoint from CLI flags. +func BuildLoginGoogleCallbackPayload(webLoginGoogleCallbackCode string, webLoginGoogleCallbackState string, webLoginGoogleCallbackSessionCookie string) (*web.LoginGoogleCallbackPayload, error) { + var code string + { + code = webLoginGoogleCallbackCode + } + var state string + { + state = webLoginGoogleCallbackState + } + var sessionCookie string + { + sessionCookie = webLoginGoogleCallbackSessionCookie + } + v := &web.LoginGoogleCallbackPayload{} + v.Code = code + v.State = state + v.SessionCookie = sessionCookie + + return v, nil +} + +// BuildLogoutPayload builds the payload for the web Logout endpoint from CLI +// flags. +func BuildLogoutPayload(webLogoutSessionCookie string) (*web.LogoutPayload, error) { + var sessionCookie string + { + sessionCookie = webLogoutSessionCookie + } + v := &web.LogoutPayload{} + v.SessionCookie = sessionCookie + + return v, nil +} + +// BuildSessionTokenPayload builds the payload for the web SessionToken +// endpoint from CLI flags. +func BuildSessionTokenPayload(webSessionTokenSessionCookie string) (*web.SessionTokenPayload, error) { + var sessionCookie string + { + sessionCookie = webSessionTokenSessionCookie + } + v := &web.SessionTokenPayload{} + v.SessionCookie = sessionCookie + + return v, nil +} diff --git a/app/api/v1/gen/http/web/client/client.go b/app/api/v1/gen/http/web/client/client.go index df4b614..5f40d5c 100644 --- a/app/api/v1/gen/http/web/client/client.go +++ b/app/api/v1/gen/http/web/client/client.go @@ -17,13 +17,28 @@ import ( // Client lists the web service endpoint HTTP clients. type Client struct { - // Index Doer is the HTTP client used to make requests to the index endpoint. + // Index Doer is the HTTP client used to make requests to the Index endpoint. IndexDoer goahttp.Doer - // Another Doer is the HTTP client used to make requests to the another + // Another Doer is the HTTP client used to make requests to the Another // endpoint. AnotherDoer goahttp.Doer + // LoginGoogle Doer is the HTTP client used to make requests to the LoginGoogle + // endpoint. + LoginGoogleDoer goahttp.Doer + + // LoginGoogleCallback Doer is the HTTP client used to make requests to the + // LoginGoogleCallback endpoint. + LoginGoogleCallbackDoer goahttp.Doer + + // Logout Doer is the HTTP client used to make requests to the Logout endpoint. + LogoutDoer goahttp.Doer + + // SessionToken Doer is the HTTP client used to make requests to the + // SessionToken endpoint. + SessionTokenDoer goahttp.Doer + // RestoreResponseBody controls whether the response bodies are reset after // decoding so they can be read again. RestoreResponseBody bool @@ -44,17 +59,21 @@ func NewClient( restoreBody bool, ) *Client { return &Client{ - IndexDoer: doer, - AnotherDoer: doer, - RestoreResponseBody: restoreBody, - scheme: scheme, - host: host, - decoder: dec, - encoder: enc, + IndexDoer: doer, + AnotherDoer: doer, + LoginGoogleDoer: doer, + LoginGoogleCallbackDoer: doer, + LogoutDoer: doer, + SessionTokenDoer: doer, + RestoreResponseBody: restoreBody, + scheme: scheme, + host: host, + decoder: dec, + encoder: enc, } } -// Index returns an endpoint that makes HTTP requests to the web service index +// Index returns an endpoint that makes HTTP requests to the web service Index // server. func (c *Client) Index() goa.Endpoint { var ( @@ -67,14 +86,14 @@ func (c *Client) Index() goa.Endpoint { } resp, err := c.IndexDoer.Do(req) if err != nil { - return nil, goahttp.ErrRequestError("web", "index", err) + return nil, goahttp.ErrRequestError("web", "Index", err) } return decodeResponse(resp) } } // Another returns an endpoint that makes HTTP requests to the web service -// another server. +// Another server. func (c *Client) Another() goa.Endpoint { var ( decodeResponse = DecodeAnotherResponse(c.decoder, c.RestoreResponseBody) @@ -86,7 +105,98 @@ func (c *Client) Another() goa.Endpoint { } resp, err := c.AnotherDoer.Do(req) if err != nil { - return nil, goahttp.ErrRequestError("web", "another", err) + return nil, goahttp.ErrRequestError("web", "Another", err) + } + return decodeResponse(resp) + } +} + +// LoginGoogle returns an endpoint that makes HTTP requests to the web service +// LoginGoogle server. +func (c *Client) LoginGoogle() goa.Endpoint { + var ( + decodeResponse = DecodeLoginGoogleResponse(c.decoder, c.RestoreResponseBody) + ) + return func(ctx context.Context, v any) (any, error) { + req, err := c.BuildLoginGoogleRequest(ctx, v) + if err != nil { + return nil, err + } + resp, err := c.LoginGoogleDoer.Do(req) + if err != nil { + return nil, goahttp.ErrRequestError("web", "LoginGoogle", err) + } + return decodeResponse(resp) + } +} + +// LoginGoogleCallback returns an endpoint that makes HTTP requests to the web +// service LoginGoogleCallback server. +func (c *Client) LoginGoogleCallback() goa.Endpoint { + var ( + encodeRequest = EncodeLoginGoogleCallbackRequest(c.encoder) + decodeResponse = DecodeLoginGoogleCallbackResponse(c.decoder, c.RestoreResponseBody) + ) + return func(ctx context.Context, v any) (any, error) { + req, err := c.BuildLoginGoogleCallbackRequest(ctx, v) + if err != nil { + return nil, err + } + err = encodeRequest(req, v) + if err != nil { + return nil, err + } + resp, err := c.LoginGoogleCallbackDoer.Do(req) + if err != nil { + return nil, goahttp.ErrRequestError("web", "LoginGoogleCallback", err) + } + return decodeResponse(resp) + } +} + +// Logout returns an endpoint that makes HTTP requests to the web service +// Logout server. +func (c *Client) Logout() goa.Endpoint { + var ( + encodeRequest = EncodeLogoutRequest(c.encoder) + decodeResponse = DecodeLogoutResponse(c.decoder, c.RestoreResponseBody) + ) + return func(ctx context.Context, v any) (any, error) { + req, err := c.BuildLogoutRequest(ctx, v) + if err != nil { + return nil, err + } + err = encodeRequest(req, v) + if err != nil { + return nil, err + } + resp, err := c.LogoutDoer.Do(req) + if err != nil { + return nil, goahttp.ErrRequestError("web", "Logout", err) + } + return decodeResponse(resp) + } +} + +// SessionToken returns an endpoint that makes HTTP requests to the web service +// SessionToken server. +func (c *Client) SessionToken() goa.Endpoint { + var ( + encodeRequest = EncodeSessionTokenRequest(c.encoder) + decodeResponse = DecodeSessionTokenResponse(c.decoder, c.RestoreResponseBody) + ) + return func(ctx context.Context, v any) (any, error) { + req, err := c.BuildSessionTokenRequest(ctx, v) + if err != nil { + return nil, err + } + err = encodeRequest(req, v) + if err != nil { + return nil, err + } + resp, err := c.SessionTokenDoer.Do(req) + if err != nil { + return nil, goahttp.ErrRequestError("web", "SessionToken", err) } return decodeResponse(resp) } diff --git a/app/api/v1/gen/http/web/client/encode_decode.go b/app/api/v1/gen/http/web/client/encode_decode.go index ebee749..2df36b0 100644 --- a/app/api/v1/gen/http/web/client/encode_decode.go +++ b/app/api/v1/gen/http/web/client/encode_decode.go @@ -14,16 +14,18 @@ import ( "net/http" "net/url" + web "github.com/jace-ys/countup/api/v1/gen/web" goahttp "goa.design/goa/v3/http" + goa "goa.design/goa/v3/pkg" ) // BuildIndexRequest instantiates a HTTP request object with method and path -// set to call the "web" service "index" endpoint +// set to call the "web" service "Index" endpoint func (c *Client) BuildIndexRequest(ctx context.Context, v any) (*http.Request, error) { u := &url.URL{Scheme: c.scheme, Host: c.host, Path: IndexWebPath()} req, err := http.NewRequest("GET", u.String(), nil) if err != nil { - return nil, goahttp.ErrInvalidURL("web", "index", u.String(), err) + return nil, goahttp.ErrInvalidURL("web", "Index", u.String(), err) } if ctx != nil { req = req.WithContext(ctx) @@ -33,8 +35,11 @@ func (c *Client) BuildIndexRequest(ctx context.Context, v any) (*http.Request, e } // DecodeIndexResponse returns a decoder for responses returned by the web -// index endpoint. restoreBody controls whether the response body should be +// Index endpoint. restoreBody controls whether the response body should be // restored after having been read. +// DecodeIndexResponse may return the following errors: +// - "unauthorized" (type *goa.ServiceError): http.StatusUnauthorized +// - error: internal error func DecodeIndexResponse(decoder func(*http.Response) goahttp.Decoder, restoreBody bool) func(*http.Response) (any, error) { return func(resp *http.Response) (any, error) { if restoreBody { @@ -57,23 +62,37 @@ func DecodeIndexResponse(decoder func(*http.Response) goahttp.Decoder, restoreBo ) err = decoder(resp).Decode(&body) if err != nil { - return nil, goahttp.ErrDecodingError("web", "index", err) + return nil, goahttp.ErrDecodingError("web", "Index", err) } return body, nil + case http.StatusUnauthorized: + var ( + body IndexUnauthorizedResponseBody + err error + ) + err = decoder(resp).Decode(&body) + if err != nil { + return nil, goahttp.ErrDecodingError("web", "Index", err) + } + err = ValidateIndexUnauthorizedResponseBody(&body) + if err != nil { + return nil, goahttp.ErrValidationError("web", "Index", err) + } + return nil, NewIndexUnauthorized(&body) default: body, _ := io.ReadAll(resp.Body) - return nil, goahttp.ErrInvalidResponse("web", "index", resp.StatusCode, string(body)) + return nil, goahttp.ErrInvalidResponse("web", "Index", resp.StatusCode, string(body)) } } } // BuildAnotherRequest instantiates a HTTP request object with method and path -// set to call the "web" service "another" endpoint +// set to call the "web" service "Another" endpoint func (c *Client) BuildAnotherRequest(ctx context.Context, v any) (*http.Request, error) { u := &url.URL{Scheme: c.scheme, Host: c.host, Path: AnotherWebPath()} req, err := http.NewRequest("GET", u.String(), nil) if err != nil { - return nil, goahttp.ErrInvalidURL("web", "another", u.String(), err) + return nil, goahttp.ErrInvalidURL("web", "Another", u.String(), err) } if ctx != nil { req = req.WithContext(ctx) @@ -83,8 +102,11 @@ func (c *Client) BuildAnotherRequest(ctx context.Context, v any) (*http.Request, } // DecodeAnotherResponse returns a decoder for responses returned by the web -// another endpoint. restoreBody controls whether the response body should be +// Another endpoint. restoreBody controls whether the response body should be // restored after having been read. +// DecodeAnotherResponse may return the following errors: +// - "unauthorized" (type *goa.ServiceError): http.StatusUnauthorized +// - error: internal error func DecodeAnotherResponse(decoder func(*http.Response) goahttp.Decoder, restoreBody bool) func(*http.Response) (any, error) { return func(resp *http.Response) (any, error) { if restoreBody { @@ -107,12 +129,423 @@ func DecodeAnotherResponse(decoder func(*http.Response) goahttp.Decoder, restore ) err = decoder(resp).Decode(&body) if err != nil { - return nil, goahttp.ErrDecodingError("web", "another", err) + return nil, goahttp.ErrDecodingError("web", "Another", err) } return body, nil + case http.StatusUnauthorized: + var ( + body AnotherUnauthorizedResponseBody + err error + ) + err = decoder(resp).Decode(&body) + if err != nil { + return nil, goahttp.ErrDecodingError("web", "Another", err) + } + err = ValidateAnotherUnauthorizedResponseBody(&body) + if err != nil { + return nil, goahttp.ErrValidationError("web", "Another", err) + } + return nil, NewAnotherUnauthorized(&body) + default: + body, _ := io.ReadAll(resp.Body) + return nil, goahttp.ErrInvalidResponse("web", "Another", resp.StatusCode, string(body)) + } + } +} + +// BuildLoginGoogleRequest instantiates a HTTP request object with method and +// path set to call the "web" service "LoginGoogle" endpoint +func (c *Client) BuildLoginGoogleRequest(ctx context.Context, v any) (*http.Request, error) { + u := &url.URL{Scheme: c.scheme, Host: c.host, Path: LoginGoogleWebPath()} + req, err := http.NewRequest("GET", u.String(), nil) + if err != nil { + return nil, goahttp.ErrInvalidURL("web", "LoginGoogle", u.String(), err) + } + if ctx != nil { + req = req.WithContext(ctx) + } + + return req, nil +} + +// DecodeLoginGoogleResponse returns a decoder for responses returned by the +// web LoginGoogle endpoint. restoreBody controls whether the response body +// should be restored after having been read. +// DecodeLoginGoogleResponse may return the following errors: +// - "unauthorized" (type *goa.ServiceError): http.StatusUnauthorized +// - error: internal error +func DecodeLoginGoogleResponse(decoder func(*http.Response) goahttp.Decoder, restoreBody bool) func(*http.Response) (any, error) { + return func(resp *http.Response) (any, error) { + if restoreBody { + b, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + resp.Body = io.NopCloser(bytes.NewBuffer(b)) + defer func() { + resp.Body = io.NopCloser(bytes.NewBuffer(b)) + }() + } else { + defer resp.Body.Close() + } + switch resp.StatusCode { + case http.StatusFound: + var ( + redirectURL string + err error + ) + redirectURLRaw := resp.Header.Get("Location") + if redirectURLRaw == "" { + err = goa.MergeErrors(err, goa.MissingFieldError("redirect_url", "header")) + } + redirectURL = redirectURLRaw + var ( + sessionCookie string + sessionCookieRaw string + + cookies = resp.Cookies() + ) + for _, c := range cookies { + switch c.Name { + case "countup.session": + sessionCookieRaw = c.Value + } + } + if sessionCookieRaw == "" { + err = goa.MergeErrors(err, goa.MissingFieldError("session_cookie", "cookie")) + } + sessionCookie = sessionCookieRaw + if err != nil { + return nil, goahttp.ErrValidationError("web", "LoginGoogle", err) + } + res := NewLoginGoogleResultFound(redirectURL, sessionCookie) + return res, nil + case http.StatusUnauthorized: + var ( + body LoginGoogleUnauthorizedResponseBody + err error + ) + err = decoder(resp).Decode(&body) + if err != nil { + return nil, goahttp.ErrDecodingError("web", "LoginGoogle", err) + } + err = ValidateLoginGoogleUnauthorizedResponseBody(&body) + if err != nil { + return nil, goahttp.ErrValidationError("web", "LoginGoogle", err) + } + return nil, NewLoginGoogleUnauthorized(&body) + default: + body, _ := io.ReadAll(resp.Body) + return nil, goahttp.ErrInvalidResponse("web", "LoginGoogle", resp.StatusCode, string(body)) + } + } +} + +// BuildLoginGoogleCallbackRequest instantiates a HTTP request object with +// method and path set to call the "web" service "LoginGoogleCallback" endpoint +func (c *Client) BuildLoginGoogleCallbackRequest(ctx context.Context, v any) (*http.Request, error) { + u := &url.URL{Scheme: c.scheme, Host: c.host, Path: LoginGoogleCallbackWebPath()} + req, err := http.NewRequest("GET", u.String(), nil) + if err != nil { + return nil, goahttp.ErrInvalidURL("web", "LoginGoogleCallback", u.String(), err) + } + if ctx != nil { + req = req.WithContext(ctx) + } + + return req, nil +} + +// EncodeLoginGoogleCallbackRequest returns an encoder for requests sent to the +// web LoginGoogleCallback server. +func EncodeLoginGoogleCallbackRequest(encoder func(*http.Request) goahttp.Encoder) func(*http.Request, any) error { + return func(req *http.Request, v any) error { + p, ok := v.(*web.LoginGoogleCallbackPayload) + if !ok { + return goahttp.ErrInvalidType("web", "LoginGoogleCallback", "*web.LoginGoogleCallbackPayload", v) + } + { + v := p.SessionCookie + req.AddCookie(&http.Cookie{ + Name: "countup.session", + Value: v, + }) + } + values := req.URL.Query() + values.Add("code", p.Code) + values.Add("state", p.State) + req.URL.RawQuery = values.Encode() + return nil + } +} + +// DecodeLoginGoogleCallbackResponse returns a decoder for responses returned +// by the web LoginGoogleCallback endpoint. restoreBody controls whether the +// response body should be restored after having been read. +// DecodeLoginGoogleCallbackResponse may return the following errors: +// - "unauthorized" (type *goa.ServiceError): http.StatusUnauthorized +// - error: internal error +func DecodeLoginGoogleCallbackResponse(decoder func(*http.Response) goahttp.Decoder, restoreBody bool) func(*http.Response) (any, error) { + return func(resp *http.Response) (any, error) { + if restoreBody { + b, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + resp.Body = io.NopCloser(bytes.NewBuffer(b)) + defer func() { + resp.Body = io.NopCloser(bytes.NewBuffer(b)) + }() + } else { + defer resp.Body.Close() + } + switch resp.StatusCode { + case http.StatusFound: + var ( + redirectURL string + err error + ) + redirectURLRaw := resp.Header.Get("Location") + if redirectURLRaw == "" { + err = goa.MergeErrors(err, goa.MissingFieldError("redirect_url", "header")) + } + redirectURL = redirectURLRaw + var ( + sessionCookie string + sessionCookieRaw string + + cookies = resp.Cookies() + ) + for _, c := range cookies { + switch c.Name { + case "countup.session": + sessionCookieRaw = c.Value + } + } + if sessionCookieRaw == "" { + err = goa.MergeErrors(err, goa.MissingFieldError("session_cookie", "cookie")) + } + sessionCookie = sessionCookieRaw + if err != nil { + return nil, goahttp.ErrValidationError("web", "LoginGoogleCallback", err) + } + res := NewLoginGoogleCallbackResultFound(redirectURL, sessionCookie) + return res, nil + case http.StatusUnauthorized: + var ( + body LoginGoogleCallbackUnauthorizedResponseBody + err error + ) + err = decoder(resp).Decode(&body) + if err != nil { + return nil, goahttp.ErrDecodingError("web", "LoginGoogleCallback", err) + } + err = ValidateLoginGoogleCallbackUnauthorizedResponseBody(&body) + if err != nil { + return nil, goahttp.ErrValidationError("web", "LoginGoogleCallback", err) + } + return nil, NewLoginGoogleCallbackUnauthorized(&body) + default: + body, _ := io.ReadAll(resp.Body) + return nil, goahttp.ErrInvalidResponse("web", "LoginGoogleCallback", resp.StatusCode, string(body)) + } + } +} + +// BuildLogoutRequest instantiates a HTTP request object with method and path +// set to call the "web" service "Logout" endpoint +func (c *Client) BuildLogoutRequest(ctx context.Context, v any) (*http.Request, error) { + u := &url.URL{Scheme: c.scheme, Host: c.host, Path: LogoutWebPath()} + req, err := http.NewRequest("GET", u.String(), nil) + if err != nil { + return nil, goahttp.ErrInvalidURL("web", "Logout", u.String(), err) + } + if ctx != nil { + req = req.WithContext(ctx) + } + + return req, nil +} + +// EncodeLogoutRequest returns an encoder for requests sent to the web Logout +// server. +func EncodeLogoutRequest(encoder func(*http.Request) goahttp.Encoder) func(*http.Request, any) error { + return func(req *http.Request, v any) error { + p, ok := v.(*web.LogoutPayload) + if !ok { + return goahttp.ErrInvalidType("web", "Logout", "*web.LogoutPayload", v) + } + { + v := p.SessionCookie + req.AddCookie(&http.Cookie{ + Name: "countup.session", + Value: v, + }) + } + return nil + } +} + +// DecodeLogoutResponse returns a decoder for responses returned by the web +// Logout endpoint. restoreBody controls whether the response body should be +// restored after having been read. +// DecodeLogoutResponse may return the following errors: +// - "unauthorized" (type *goa.ServiceError): http.StatusUnauthorized +// - error: internal error +func DecodeLogoutResponse(decoder func(*http.Response) goahttp.Decoder, restoreBody bool) func(*http.Response) (any, error) { + return func(resp *http.Response) (any, error) { + if restoreBody { + b, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + resp.Body = io.NopCloser(bytes.NewBuffer(b)) + defer func() { + resp.Body = io.NopCloser(bytes.NewBuffer(b)) + }() + } else { + defer resp.Body.Close() + } + switch resp.StatusCode { + case http.StatusFound: + var ( + redirectURL string + err error + ) + redirectURLRaw := resp.Header.Get("Location") + if redirectURLRaw == "" { + err = goa.MergeErrors(err, goa.MissingFieldError("redirect_url", "header")) + } + redirectURL = redirectURLRaw + var ( + sessionCookie string + sessionCookieRaw string + + cookies = resp.Cookies() + ) + for _, c := range cookies { + switch c.Name { + case "countup.session": + sessionCookieRaw = c.Value + } + } + if sessionCookieRaw == "" { + err = goa.MergeErrors(err, goa.MissingFieldError("session_cookie", "cookie")) + } + sessionCookie = sessionCookieRaw + if err != nil { + return nil, goahttp.ErrValidationError("web", "Logout", err) + } + res := NewLogoutResultFound(redirectURL, sessionCookie) + return res, nil + case http.StatusUnauthorized: + var ( + body LogoutUnauthorizedResponseBody + err error + ) + err = decoder(resp).Decode(&body) + if err != nil { + return nil, goahttp.ErrDecodingError("web", "Logout", err) + } + err = ValidateLogoutUnauthorizedResponseBody(&body) + if err != nil { + return nil, goahttp.ErrValidationError("web", "Logout", err) + } + return nil, NewLogoutUnauthorized(&body) + default: + body, _ := io.ReadAll(resp.Body) + return nil, goahttp.ErrInvalidResponse("web", "Logout", resp.StatusCode, string(body)) + } + } +} + +// BuildSessionTokenRequest instantiates a HTTP request object with method and +// path set to call the "web" service "SessionToken" endpoint +func (c *Client) BuildSessionTokenRequest(ctx context.Context, v any) (*http.Request, error) { + u := &url.URL{Scheme: c.scheme, Host: c.host, Path: SessionTokenWebPath()} + req, err := http.NewRequest("GET", u.String(), nil) + if err != nil { + return nil, goahttp.ErrInvalidURL("web", "SessionToken", u.String(), err) + } + if ctx != nil { + req = req.WithContext(ctx) + } + + return req, nil +} + +// EncodeSessionTokenRequest returns an encoder for requests sent to the web +// SessionToken server. +func EncodeSessionTokenRequest(encoder func(*http.Request) goahttp.Encoder) func(*http.Request, any) error { + return func(req *http.Request, v any) error { + p, ok := v.(*web.SessionTokenPayload) + if !ok { + return goahttp.ErrInvalidType("web", "SessionToken", "*web.SessionTokenPayload", v) + } + { + v := p.SessionCookie + req.AddCookie(&http.Cookie{ + Name: "countup.session", + Value: v, + }) + } + return nil + } +} + +// DecodeSessionTokenResponse returns a decoder for responses returned by the +// web SessionToken endpoint. restoreBody controls whether the response body +// should be restored after having been read. +// DecodeSessionTokenResponse may return the following errors: +// - "unauthorized" (type *goa.ServiceError): http.StatusUnauthorized +// - error: internal error +func DecodeSessionTokenResponse(decoder func(*http.Response) goahttp.Decoder, restoreBody bool) func(*http.Response) (any, error) { + return func(resp *http.Response) (any, error) { + if restoreBody { + b, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + resp.Body = io.NopCloser(bytes.NewBuffer(b)) + defer func() { + resp.Body = io.NopCloser(bytes.NewBuffer(b)) + }() + } else { + defer resp.Body.Close() + } + switch resp.StatusCode { + case http.StatusOK: + var ( + body SessionTokenResponseBody + err error + ) + err = decoder(resp).Decode(&body) + if err != nil { + return nil, goahttp.ErrDecodingError("web", "SessionToken", err) + } + err = ValidateSessionTokenResponseBody(&body) + if err != nil { + return nil, goahttp.ErrValidationError("web", "SessionToken", err) + } + res := NewSessionTokenResultOK(&body) + return res, nil + case http.StatusUnauthorized: + var ( + body SessionTokenUnauthorizedResponseBody + err error + ) + err = decoder(resp).Decode(&body) + if err != nil { + return nil, goahttp.ErrDecodingError("web", "SessionToken", err) + } + err = ValidateSessionTokenUnauthorizedResponseBody(&body) + if err != nil { + return nil, goahttp.ErrValidationError("web", "SessionToken", err) + } + return nil, NewSessionTokenUnauthorized(&body) default: body, _ := io.ReadAll(resp.Body) - return nil, goahttp.ErrInvalidResponse("web", "another", resp.StatusCode, string(body)) + return nil, goahttp.ErrInvalidResponse("web", "SessionToken", resp.StatusCode, string(body)) } } } diff --git a/app/api/v1/gen/http/web/client/paths.go b/app/api/v1/gen/http/web/client/paths.go index 6ea65ce..1a563cf 100644 --- a/app/api/v1/gen/http/web/client/paths.go +++ b/app/api/v1/gen/http/web/client/paths.go @@ -7,12 +7,32 @@ package client -// IndexWebPath returns the URL path to the web service index HTTP endpoint. +// IndexWebPath returns the URL path to the web service Index HTTP endpoint. func IndexWebPath() string { return "/" } -// AnotherWebPath returns the URL path to the web service another HTTP endpoint. +// AnotherWebPath returns the URL path to the web service Another HTTP endpoint. func AnotherWebPath() string { return "/another" } + +// LoginGoogleWebPath returns the URL path to the web service LoginGoogle HTTP endpoint. +func LoginGoogleWebPath() string { + return "/login/google" +} + +// LoginGoogleCallbackWebPath returns the URL path to the web service LoginGoogleCallback HTTP endpoint. +func LoginGoogleCallbackWebPath() string { + return "/login/google/callback" +} + +// LogoutWebPath returns the URL path to the web service Logout HTTP endpoint. +func LogoutWebPath() string { + return "/logout" +} + +// SessionTokenWebPath returns the URL path to the web service SessionToken HTTP endpoint. +func SessionTokenWebPath() string { + return "/session/token" +} diff --git a/app/api/v1/gen/http/web/client/types.go b/app/api/v1/gen/http/web/client/types.go index 0f1ccd1..3fda6cc 100644 --- a/app/api/v1/gen/http/web/client/types.go +++ b/app/api/v1/gen/http/web/client/types.go @@ -6,3 +6,405 @@ // $ goa gen github.com/jace-ys/countup/api/v1 -o api/v1 package client + +import ( + web "github.com/jace-ys/countup/api/v1/gen/web" + goa "goa.design/goa/v3/pkg" +) + +// SessionTokenResponseBody is the type of the "web" service "SessionToken" +// endpoint HTTP response body. +type SessionTokenResponseBody struct { + Token *string `form:"token,omitempty" json:"token,omitempty" xml:"token,omitempty"` +} + +// IndexUnauthorizedResponseBody is the type of the "web" service "Index" +// endpoint HTTP response body for the "unauthorized" error. +type IndexUnauthorizedResponseBody struct { + // Name is the name of this class of errors. + Name *string `form:"name,omitempty" json:"name,omitempty" xml:"name,omitempty"` + // ID is a unique identifier for this particular occurrence of the problem. + ID *string `form:"id,omitempty" json:"id,omitempty" xml:"id,omitempty"` + // Message is a human-readable explanation specific to this occurrence of the + // problem. + Message *string `form:"message,omitempty" json:"message,omitempty" xml:"message,omitempty"` + // Is the error temporary? + Temporary *bool `form:"temporary,omitempty" json:"temporary,omitempty" xml:"temporary,omitempty"` + // Is the error a timeout? + Timeout *bool `form:"timeout,omitempty" json:"timeout,omitempty" xml:"timeout,omitempty"` + // Is the error a server-side fault? + Fault *bool `form:"fault,omitempty" json:"fault,omitempty" xml:"fault,omitempty"` +} + +// AnotherUnauthorizedResponseBody is the type of the "web" service "Another" +// endpoint HTTP response body for the "unauthorized" error. +type AnotherUnauthorizedResponseBody struct { + // Name is the name of this class of errors. + Name *string `form:"name,omitempty" json:"name,omitempty" xml:"name,omitempty"` + // ID is a unique identifier for this particular occurrence of the problem. + ID *string `form:"id,omitempty" json:"id,omitempty" xml:"id,omitempty"` + // Message is a human-readable explanation specific to this occurrence of the + // problem. + Message *string `form:"message,omitempty" json:"message,omitempty" xml:"message,omitempty"` + // Is the error temporary? + Temporary *bool `form:"temporary,omitempty" json:"temporary,omitempty" xml:"temporary,omitempty"` + // Is the error a timeout? + Timeout *bool `form:"timeout,omitempty" json:"timeout,omitempty" xml:"timeout,omitempty"` + // Is the error a server-side fault? + Fault *bool `form:"fault,omitempty" json:"fault,omitempty" xml:"fault,omitempty"` +} + +// LoginGoogleUnauthorizedResponseBody is the type of the "web" service +// "LoginGoogle" endpoint HTTP response body for the "unauthorized" error. +type LoginGoogleUnauthorizedResponseBody struct { + // Name is the name of this class of errors. + Name *string `form:"name,omitempty" json:"name,omitempty" xml:"name,omitempty"` + // ID is a unique identifier for this particular occurrence of the problem. + ID *string `form:"id,omitempty" json:"id,omitempty" xml:"id,omitempty"` + // Message is a human-readable explanation specific to this occurrence of the + // problem. + Message *string `form:"message,omitempty" json:"message,omitempty" xml:"message,omitempty"` + // Is the error temporary? + Temporary *bool `form:"temporary,omitempty" json:"temporary,omitempty" xml:"temporary,omitempty"` + // Is the error a timeout? + Timeout *bool `form:"timeout,omitempty" json:"timeout,omitempty" xml:"timeout,omitempty"` + // Is the error a server-side fault? + Fault *bool `form:"fault,omitempty" json:"fault,omitempty" xml:"fault,omitempty"` +} + +// LoginGoogleCallbackUnauthorizedResponseBody is the type of the "web" service +// "LoginGoogleCallback" endpoint HTTP response body for the "unauthorized" +// error. +type LoginGoogleCallbackUnauthorizedResponseBody struct { + // Name is the name of this class of errors. + Name *string `form:"name,omitempty" json:"name,omitempty" xml:"name,omitempty"` + // ID is a unique identifier for this particular occurrence of the problem. + ID *string `form:"id,omitempty" json:"id,omitempty" xml:"id,omitempty"` + // Message is a human-readable explanation specific to this occurrence of the + // problem. + Message *string `form:"message,omitempty" json:"message,omitempty" xml:"message,omitempty"` + // Is the error temporary? + Temporary *bool `form:"temporary,omitempty" json:"temporary,omitempty" xml:"temporary,omitempty"` + // Is the error a timeout? + Timeout *bool `form:"timeout,omitempty" json:"timeout,omitempty" xml:"timeout,omitempty"` + // Is the error a server-side fault? + Fault *bool `form:"fault,omitempty" json:"fault,omitempty" xml:"fault,omitempty"` +} + +// LogoutUnauthorizedResponseBody is the type of the "web" service "Logout" +// endpoint HTTP response body for the "unauthorized" error. +type LogoutUnauthorizedResponseBody struct { + // Name is the name of this class of errors. + Name *string `form:"name,omitempty" json:"name,omitempty" xml:"name,omitempty"` + // ID is a unique identifier for this particular occurrence of the problem. + ID *string `form:"id,omitempty" json:"id,omitempty" xml:"id,omitempty"` + // Message is a human-readable explanation specific to this occurrence of the + // problem. + Message *string `form:"message,omitempty" json:"message,omitempty" xml:"message,omitempty"` + // Is the error temporary? + Temporary *bool `form:"temporary,omitempty" json:"temporary,omitempty" xml:"temporary,omitempty"` + // Is the error a timeout? + Timeout *bool `form:"timeout,omitempty" json:"timeout,omitempty" xml:"timeout,omitempty"` + // Is the error a server-side fault? + Fault *bool `form:"fault,omitempty" json:"fault,omitempty" xml:"fault,omitempty"` +} + +// SessionTokenUnauthorizedResponseBody is the type of the "web" service +// "SessionToken" endpoint HTTP response body for the "unauthorized" error. +type SessionTokenUnauthorizedResponseBody struct { + // Name is the name of this class of errors. + Name *string `form:"name,omitempty" json:"name,omitempty" xml:"name,omitempty"` + // ID is a unique identifier for this particular occurrence of the problem. + ID *string `form:"id,omitempty" json:"id,omitempty" xml:"id,omitempty"` + // Message is a human-readable explanation specific to this occurrence of the + // problem. + Message *string `form:"message,omitempty" json:"message,omitempty" xml:"message,omitempty"` + // Is the error temporary? + Temporary *bool `form:"temporary,omitempty" json:"temporary,omitempty" xml:"temporary,omitempty"` + // Is the error a timeout? + Timeout *bool `form:"timeout,omitempty" json:"timeout,omitempty" xml:"timeout,omitempty"` + // Is the error a server-side fault? + Fault *bool `form:"fault,omitempty" json:"fault,omitempty" xml:"fault,omitempty"` +} + +// NewIndexUnauthorized builds a web service Index endpoint unauthorized error. +func NewIndexUnauthorized(body *IndexUnauthorizedResponseBody) *goa.ServiceError { + v := &goa.ServiceError{ + Name: *body.Name, + ID: *body.ID, + Message: *body.Message, + Temporary: *body.Temporary, + Timeout: *body.Timeout, + Fault: *body.Fault, + } + + return v +} + +// NewAnotherUnauthorized builds a web service Another endpoint unauthorized +// error. +func NewAnotherUnauthorized(body *AnotherUnauthorizedResponseBody) *goa.ServiceError { + v := &goa.ServiceError{ + Name: *body.Name, + ID: *body.ID, + Message: *body.Message, + Temporary: *body.Temporary, + Timeout: *body.Timeout, + Fault: *body.Fault, + } + + return v +} + +// NewLoginGoogleResultFound builds a "web" service "LoginGoogle" endpoint +// result from a HTTP "Found" response. +func NewLoginGoogleResultFound(redirectURL string, sessionCookie string) *web.LoginGoogleResult { + v := &web.LoginGoogleResult{} + v.RedirectURL = redirectURL + v.SessionCookie = sessionCookie + + return v +} + +// NewLoginGoogleUnauthorized builds a web service LoginGoogle endpoint +// unauthorized error. +func NewLoginGoogleUnauthorized(body *LoginGoogleUnauthorizedResponseBody) *goa.ServiceError { + v := &goa.ServiceError{ + Name: *body.Name, + ID: *body.ID, + Message: *body.Message, + Temporary: *body.Temporary, + Timeout: *body.Timeout, + Fault: *body.Fault, + } + + return v +} + +// NewLoginGoogleCallbackResultFound builds a "web" service +// "LoginGoogleCallback" endpoint result from a HTTP "Found" response. +func NewLoginGoogleCallbackResultFound(redirectURL string, sessionCookie string) *web.LoginGoogleCallbackResult { + v := &web.LoginGoogleCallbackResult{} + v.RedirectURL = redirectURL + v.SessionCookie = sessionCookie + + return v +} + +// NewLoginGoogleCallbackUnauthorized builds a web service LoginGoogleCallback +// endpoint unauthorized error. +func NewLoginGoogleCallbackUnauthorized(body *LoginGoogleCallbackUnauthorizedResponseBody) *goa.ServiceError { + v := &goa.ServiceError{ + Name: *body.Name, + ID: *body.ID, + Message: *body.Message, + Temporary: *body.Temporary, + Timeout: *body.Timeout, + Fault: *body.Fault, + } + + return v +} + +// NewLogoutResultFound builds a "web" service "Logout" endpoint result from a +// HTTP "Found" response. +func NewLogoutResultFound(redirectURL string, sessionCookie string) *web.LogoutResult { + v := &web.LogoutResult{} + v.RedirectURL = redirectURL + v.SessionCookie = sessionCookie + + return v +} + +// NewLogoutUnauthorized builds a web service Logout endpoint unauthorized +// error. +func NewLogoutUnauthorized(body *LogoutUnauthorizedResponseBody) *goa.ServiceError { + v := &goa.ServiceError{ + Name: *body.Name, + ID: *body.ID, + Message: *body.Message, + Temporary: *body.Temporary, + Timeout: *body.Timeout, + Fault: *body.Fault, + } + + return v +} + +// NewSessionTokenResultOK builds a "web" service "SessionToken" endpoint +// result from a HTTP "OK" response. +func NewSessionTokenResultOK(body *SessionTokenResponseBody) *web.SessionTokenResult { + v := &web.SessionTokenResult{ + Token: *body.Token, + } + + return v +} + +// NewSessionTokenUnauthorized builds a web service SessionToken endpoint +// unauthorized error. +func NewSessionTokenUnauthorized(body *SessionTokenUnauthorizedResponseBody) *goa.ServiceError { + v := &goa.ServiceError{ + Name: *body.Name, + ID: *body.ID, + Message: *body.Message, + Temporary: *body.Temporary, + Timeout: *body.Timeout, + Fault: *body.Fault, + } + + return v +} + +// ValidateSessionTokenResponseBody runs the validations defined on +// SessionTokenResponseBody +func ValidateSessionTokenResponseBody(body *SessionTokenResponseBody) (err error) { + if body.Token == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("token", "body")) + } + return +} + +// ValidateIndexUnauthorizedResponseBody runs the validations defined on +// Index_unauthorized_Response_Body +func ValidateIndexUnauthorizedResponseBody(body *IndexUnauthorizedResponseBody) (err error) { + if body.Name == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("name", "body")) + } + if body.ID == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("id", "body")) + } + if body.Message == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("message", "body")) + } + if body.Temporary == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("temporary", "body")) + } + if body.Timeout == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("timeout", "body")) + } + if body.Fault == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("fault", "body")) + } + return +} + +// ValidateAnotherUnauthorizedResponseBody runs the validations defined on +// Another_unauthorized_Response_Body +func ValidateAnotherUnauthorizedResponseBody(body *AnotherUnauthorizedResponseBody) (err error) { + if body.Name == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("name", "body")) + } + if body.ID == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("id", "body")) + } + if body.Message == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("message", "body")) + } + if body.Temporary == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("temporary", "body")) + } + if body.Timeout == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("timeout", "body")) + } + if body.Fault == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("fault", "body")) + } + return +} + +// ValidateLoginGoogleUnauthorizedResponseBody runs the validations defined on +// LoginGoogle_unauthorized_Response_Body +func ValidateLoginGoogleUnauthorizedResponseBody(body *LoginGoogleUnauthorizedResponseBody) (err error) { + if body.Name == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("name", "body")) + } + if body.ID == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("id", "body")) + } + if body.Message == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("message", "body")) + } + if body.Temporary == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("temporary", "body")) + } + if body.Timeout == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("timeout", "body")) + } + if body.Fault == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("fault", "body")) + } + return +} + +// ValidateLoginGoogleCallbackUnauthorizedResponseBody runs the validations +// defined on LoginGoogleCallback_unauthorized_Response_Body +func ValidateLoginGoogleCallbackUnauthorizedResponseBody(body *LoginGoogleCallbackUnauthorizedResponseBody) (err error) { + if body.Name == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("name", "body")) + } + if body.ID == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("id", "body")) + } + if body.Message == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("message", "body")) + } + if body.Temporary == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("temporary", "body")) + } + if body.Timeout == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("timeout", "body")) + } + if body.Fault == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("fault", "body")) + } + return +} + +// ValidateLogoutUnauthorizedResponseBody runs the validations defined on +// Logout_unauthorized_Response_Body +func ValidateLogoutUnauthorizedResponseBody(body *LogoutUnauthorizedResponseBody) (err error) { + if body.Name == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("name", "body")) + } + if body.ID == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("id", "body")) + } + if body.Message == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("message", "body")) + } + if body.Temporary == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("temporary", "body")) + } + if body.Timeout == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("timeout", "body")) + } + if body.Fault == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("fault", "body")) + } + return +} + +// ValidateSessionTokenUnauthorizedResponseBody runs the validations defined on +// SessionToken_unauthorized_Response_Body +func ValidateSessionTokenUnauthorizedResponseBody(body *SessionTokenUnauthorizedResponseBody) (err error) { + if body.Name == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("name", "body")) + } + if body.ID == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("id", "body")) + } + if body.Message == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("message", "body")) + } + if body.Temporary == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("temporary", "body")) + } + if body.Timeout == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("timeout", "body")) + } + if body.Fault == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("fault", "body")) + } + return +} diff --git a/app/api/v1/gen/http/web/server/encode_decode.go b/app/api/v1/gen/http/web/server/encode_decode.go index 57b11ff..b970fa3 100644 --- a/app/api/v1/gen/http/web/server/encode_decode.go +++ b/app/api/v1/gen/http/web/server/encode_decode.go @@ -9,13 +9,16 @@ package server import ( "context" + "errors" "net/http" + web "github.com/jace-ys/countup/api/v1/gen/web" goahttp "goa.design/goa/v3/http" + goa "goa.design/goa/v3/pkg" ) // EncodeIndexResponse returns an encoder for responses returned by the web -// index endpoint. +// Index endpoint. func EncodeIndexResponse(encoder func(context.Context, http.ResponseWriter) goahttp.Encoder) func(context.Context, http.ResponseWriter, any) error { return func(ctx context.Context, w http.ResponseWriter, v any) error { res, _ := v.([]byte) @@ -27,8 +30,37 @@ func EncodeIndexResponse(encoder func(context.Context, http.ResponseWriter) goah } } +// EncodeIndexError returns an encoder for errors returned by the Index web +// endpoint. +func EncodeIndexError(encoder func(context.Context, http.ResponseWriter) goahttp.Encoder, formatter func(ctx context.Context, err error) goahttp.Statuser) func(context.Context, http.ResponseWriter, error) error { + encodeError := goahttp.ErrorEncoder(encoder, formatter) + return func(ctx context.Context, w http.ResponseWriter, v error) error { + var en goa.GoaErrorNamer + if !errors.As(v, &en) { + return encodeError(ctx, w, v) + } + switch en.GoaErrorName() { + case "unauthorized": + var res *goa.ServiceError + errors.As(v, &res) + enc := encoder(ctx, w) + var body any + if formatter != nil { + body = formatter(ctx, res) + } else { + body = NewIndexUnauthorizedResponseBody(res) + } + w.Header().Set("goa-error", res.GoaErrorName()) + w.WriteHeader(http.StatusUnauthorized) + return enc.Encode(body) + default: + return encodeError(ctx, w, v) + } + } +} + // EncodeAnotherResponse returns an encoder for responses returned by the web -// another endpoint. +// Another endpoint. func EncodeAnotherResponse(encoder func(context.Context, http.ResponseWriter) goahttp.Encoder) func(context.Context, http.ResponseWriter, any) error { return func(ctx context.Context, w http.ResponseWriter, v any) error { res, _ := v.([]byte) @@ -39,3 +71,304 @@ func EncodeAnotherResponse(encoder func(context.Context, http.ResponseWriter) go return enc.Encode(body) } } + +// EncodeAnotherError returns an encoder for errors returned by the Another web +// endpoint. +func EncodeAnotherError(encoder func(context.Context, http.ResponseWriter) goahttp.Encoder, formatter func(ctx context.Context, err error) goahttp.Statuser) func(context.Context, http.ResponseWriter, error) error { + encodeError := goahttp.ErrorEncoder(encoder, formatter) + return func(ctx context.Context, w http.ResponseWriter, v error) error { + var en goa.GoaErrorNamer + if !errors.As(v, &en) { + return encodeError(ctx, w, v) + } + switch en.GoaErrorName() { + case "unauthorized": + var res *goa.ServiceError + errors.As(v, &res) + enc := encoder(ctx, w) + var body any + if formatter != nil { + body = formatter(ctx, res) + } else { + body = NewAnotherUnauthorizedResponseBody(res) + } + w.Header().Set("goa-error", res.GoaErrorName()) + w.WriteHeader(http.StatusUnauthorized) + return enc.Encode(body) + default: + return encodeError(ctx, w, v) + } + } +} + +// EncodeLoginGoogleResponse returns an encoder for responses returned by the +// web LoginGoogle endpoint. +func EncodeLoginGoogleResponse(encoder func(context.Context, http.ResponseWriter) goahttp.Encoder) func(context.Context, http.ResponseWriter, any) error { + return func(ctx context.Context, w http.ResponseWriter, v any) error { + res, _ := v.(*web.LoginGoogleResult) + w.Header().Set("Location", res.RedirectURL) + sessionCookie := res.SessionCookie + http.SetCookie(w, &http.Cookie{ + Name: "countup.session", + Value: sessionCookie, + MaxAge: 86400, + Path: "/", + HttpOnly: true, + SameSite: http.SameSiteLaxMode, + }) + w.WriteHeader(http.StatusFound) + return nil + } +} + +// EncodeLoginGoogleError returns an encoder for errors returned by the +// LoginGoogle web endpoint. +func EncodeLoginGoogleError(encoder func(context.Context, http.ResponseWriter) goahttp.Encoder, formatter func(ctx context.Context, err error) goahttp.Statuser) func(context.Context, http.ResponseWriter, error) error { + encodeError := goahttp.ErrorEncoder(encoder, formatter) + return func(ctx context.Context, w http.ResponseWriter, v error) error { + var en goa.GoaErrorNamer + if !errors.As(v, &en) { + return encodeError(ctx, w, v) + } + switch en.GoaErrorName() { + case "unauthorized": + var res *goa.ServiceError + errors.As(v, &res) + enc := encoder(ctx, w) + var body any + if formatter != nil { + body = formatter(ctx, res) + } else { + body = NewLoginGoogleUnauthorizedResponseBody(res) + } + w.Header().Set("goa-error", res.GoaErrorName()) + w.WriteHeader(http.StatusUnauthorized) + return enc.Encode(body) + default: + return encodeError(ctx, w, v) + } + } +} + +// EncodeLoginGoogleCallbackResponse returns an encoder for responses returned +// by the web LoginGoogleCallback endpoint. +func EncodeLoginGoogleCallbackResponse(encoder func(context.Context, http.ResponseWriter) goahttp.Encoder) func(context.Context, http.ResponseWriter, any) error { + return func(ctx context.Context, w http.ResponseWriter, v any) error { + res, _ := v.(*web.LoginGoogleCallbackResult) + w.Header().Set("Location", res.RedirectURL) + sessionCookie := res.SessionCookie + http.SetCookie(w, &http.Cookie{ + Name: "countup.session", + Value: sessionCookie, + MaxAge: 86400, + Path: "/", + HttpOnly: true, + SameSite: http.SameSiteLaxMode, + }) + w.WriteHeader(http.StatusFound) + return nil + } +} + +// DecodeLoginGoogleCallbackRequest returns a decoder for requests sent to the +// web LoginGoogleCallback endpoint. +func DecodeLoginGoogleCallbackRequest(mux goahttp.Muxer, decoder func(*http.Request) goahttp.Decoder) func(*http.Request) (any, error) { + return func(r *http.Request) (any, error) { + var ( + code string + state string + sessionCookie string + err error + c *http.Cookie + ) + qp := r.URL.Query() + code = qp.Get("code") + if code == "" { + err = goa.MergeErrors(err, goa.MissingFieldError("code", "query string")) + } + state = qp.Get("state") + if state == "" { + err = goa.MergeErrors(err, goa.MissingFieldError("state", "query string")) + } + c, err = r.Cookie("countup.session") + if err == http.ErrNoCookie { + err = goa.MergeErrors(err, goa.MissingFieldError("session_cookie", "cookie")) + } else { + sessionCookie = c.Value + } + if err != nil { + return nil, err + } + payload := NewLoginGoogleCallbackPayload(code, state, sessionCookie) + + return payload, nil + } +} + +// EncodeLoginGoogleCallbackError returns an encoder for errors returned by the +// LoginGoogleCallback web endpoint. +func EncodeLoginGoogleCallbackError(encoder func(context.Context, http.ResponseWriter) goahttp.Encoder, formatter func(ctx context.Context, err error) goahttp.Statuser) func(context.Context, http.ResponseWriter, error) error { + encodeError := goahttp.ErrorEncoder(encoder, formatter) + return func(ctx context.Context, w http.ResponseWriter, v error) error { + var en goa.GoaErrorNamer + if !errors.As(v, &en) { + return encodeError(ctx, w, v) + } + switch en.GoaErrorName() { + case "unauthorized": + var res *goa.ServiceError + errors.As(v, &res) + enc := encoder(ctx, w) + var body any + if formatter != nil { + body = formatter(ctx, res) + } else { + body = NewLoginGoogleCallbackUnauthorizedResponseBody(res) + } + w.Header().Set("goa-error", res.GoaErrorName()) + w.WriteHeader(http.StatusUnauthorized) + return enc.Encode(body) + default: + return encodeError(ctx, w, v) + } + } +} + +// EncodeLogoutResponse returns an encoder for responses returned by the web +// Logout endpoint. +func EncodeLogoutResponse(encoder func(context.Context, http.ResponseWriter) goahttp.Encoder) func(context.Context, http.ResponseWriter, any) error { + return func(ctx context.Context, w http.ResponseWriter, v any) error { + res, _ := v.(*web.LogoutResult) + w.Header().Set("Location", res.RedirectURL) + sessionCookie := res.SessionCookie + http.SetCookie(w, &http.Cookie{ + Name: "countup.session", + Value: sessionCookie, + MaxAge: 86400, + Path: "/", + HttpOnly: true, + SameSite: http.SameSiteLaxMode, + }) + w.WriteHeader(http.StatusFound) + return nil + } +} + +// DecodeLogoutRequest returns a decoder for requests sent to the web Logout +// endpoint. +func DecodeLogoutRequest(mux goahttp.Muxer, decoder func(*http.Request) goahttp.Decoder) func(*http.Request) (any, error) { + return func(r *http.Request) (any, error) { + var ( + sessionCookie string + err error + c *http.Cookie + ) + c, err = r.Cookie("countup.session") + if err == http.ErrNoCookie { + err = goa.MergeErrors(err, goa.MissingFieldError("session_cookie", "cookie")) + } else { + sessionCookie = c.Value + } + if err != nil { + return nil, err + } + payload := NewLogoutPayload(sessionCookie) + + return payload, nil + } +} + +// EncodeLogoutError returns an encoder for errors returned by the Logout web +// endpoint. +func EncodeLogoutError(encoder func(context.Context, http.ResponseWriter) goahttp.Encoder, formatter func(ctx context.Context, err error) goahttp.Statuser) func(context.Context, http.ResponseWriter, error) error { + encodeError := goahttp.ErrorEncoder(encoder, formatter) + return func(ctx context.Context, w http.ResponseWriter, v error) error { + var en goa.GoaErrorNamer + if !errors.As(v, &en) { + return encodeError(ctx, w, v) + } + switch en.GoaErrorName() { + case "unauthorized": + var res *goa.ServiceError + errors.As(v, &res) + enc := encoder(ctx, w) + var body any + if formatter != nil { + body = formatter(ctx, res) + } else { + body = NewLogoutUnauthorizedResponseBody(res) + } + w.Header().Set("goa-error", res.GoaErrorName()) + w.WriteHeader(http.StatusUnauthorized) + return enc.Encode(body) + default: + return encodeError(ctx, w, v) + } + } +} + +// EncodeSessionTokenResponse returns an encoder for responses returned by the +// web SessionToken endpoint. +func EncodeSessionTokenResponse(encoder func(context.Context, http.ResponseWriter) goahttp.Encoder) func(context.Context, http.ResponseWriter, any) error { + return func(ctx context.Context, w http.ResponseWriter, v any) error { + res, _ := v.(*web.SessionTokenResult) + ctx = context.WithValue(ctx, goahttp.ContentTypeKey, "application/json") + enc := encoder(ctx, w) + body := NewSessionTokenResponseBody(res) + w.WriteHeader(http.StatusOK) + return enc.Encode(body) + } +} + +// DecodeSessionTokenRequest returns a decoder for requests sent to the web +// SessionToken endpoint. +func DecodeSessionTokenRequest(mux goahttp.Muxer, decoder func(*http.Request) goahttp.Decoder) func(*http.Request) (any, error) { + return func(r *http.Request) (any, error) { + var ( + sessionCookie string + err error + c *http.Cookie + ) + c, err = r.Cookie("countup.session") + if err == http.ErrNoCookie { + err = goa.MergeErrors(err, goa.MissingFieldError("session_cookie", "cookie")) + } else { + sessionCookie = c.Value + } + if err != nil { + return nil, err + } + payload := NewSessionTokenPayload(sessionCookie) + + return payload, nil + } +} + +// EncodeSessionTokenError returns an encoder for errors returned by the +// SessionToken web endpoint. +func EncodeSessionTokenError(encoder func(context.Context, http.ResponseWriter) goahttp.Encoder, formatter func(ctx context.Context, err error) goahttp.Statuser) func(context.Context, http.ResponseWriter, error) error { + encodeError := goahttp.ErrorEncoder(encoder, formatter) + return func(ctx context.Context, w http.ResponseWriter, v error) error { + var en goa.GoaErrorNamer + if !errors.As(v, &en) { + return encodeError(ctx, w, v) + } + switch en.GoaErrorName() { + case "unauthorized": + var res *goa.ServiceError + errors.As(v, &res) + enc := encoder(ctx, w) + var body any + if formatter != nil { + body = formatter(ctx, res) + } else { + body = NewSessionTokenUnauthorizedResponseBody(res) + } + w.Header().Set("goa-error", res.GoaErrorName()) + w.WriteHeader(http.StatusUnauthorized) + return enc.Encode(body) + default: + return encodeError(ctx, w, v) + } + } +} diff --git a/app/api/v1/gen/http/web/server/paths.go b/app/api/v1/gen/http/web/server/paths.go index a72edb5..ee25941 100644 --- a/app/api/v1/gen/http/web/server/paths.go +++ b/app/api/v1/gen/http/web/server/paths.go @@ -7,12 +7,32 @@ package server -// IndexWebPath returns the URL path to the web service index HTTP endpoint. +// IndexWebPath returns the URL path to the web service Index HTTP endpoint. func IndexWebPath() string { return "/" } -// AnotherWebPath returns the URL path to the web service another HTTP endpoint. +// AnotherWebPath returns the URL path to the web service Another HTTP endpoint. func AnotherWebPath() string { return "/another" } + +// LoginGoogleWebPath returns the URL path to the web service LoginGoogle HTTP endpoint. +func LoginGoogleWebPath() string { + return "/login/google" +} + +// LoginGoogleCallbackWebPath returns the URL path to the web service LoginGoogleCallback HTTP endpoint. +func LoginGoogleCallbackWebPath() string { + return "/login/google/callback" +} + +// LogoutWebPath returns the URL path to the web service Logout HTTP endpoint. +func LogoutWebPath() string { + return "/logout" +} + +// SessionTokenWebPath returns the URL path to the web service SessionToken HTTP endpoint. +func SessionTokenWebPath() string { + return "/session/token" +} diff --git a/app/api/v1/gen/http/web/server/server.go b/app/api/v1/gen/http/web/server/server.go index b1225c6..94cb90a 100644 --- a/app/api/v1/gen/http/web/server/server.go +++ b/app/api/v1/gen/http/web/server/server.go @@ -19,10 +19,14 @@ import ( // Server lists the web service endpoint HTTP handlers. type Server struct { - Mounts []*MountPoint - Index http.Handler - Another http.Handler - Static http.Handler + Mounts []*MountPoint + Index http.Handler + Another http.Handler + LoginGoogle http.Handler + LoginGoogleCallback http.Handler + Logout http.Handler + SessionToken http.Handler + Static http.Handler } // MountPoint holds information about the mounted endpoints. @@ -54,16 +58,24 @@ func New( if fileSystemStatic == nil { fileSystemStatic = http.Dir(".") } - fileSystemStatic = appendPrefix(fileSystemStatic, "/static/") + fileSystemStatic = appendPrefix(fileSystemStatic, "/static") return &Server{ Mounts: []*MountPoint{ {"Index", "GET", "/"}, {"Another", "GET", "/another"}, - {"Serve static/", "GET", "/static"}, + {"LoginGoogle", "GET", "/login/google"}, + {"LoginGoogleCallback", "GET", "/login/google/callback"}, + {"Logout", "GET", "/logout"}, + {"SessionToken", "GET", "/session/token"}, + {"Serve static/", "GET", "/static/*"}, }, - Index: NewIndexHandler(e.Index, mux, decoder, encoder, errhandler, formatter), - Another: NewAnotherHandler(e.Another, mux, decoder, encoder, errhandler, formatter), - Static: http.FileServer(fileSystemStatic), + Index: NewIndexHandler(e.Index, mux, decoder, encoder, errhandler, formatter), + Another: NewAnotherHandler(e.Another, mux, decoder, encoder, errhandler, formatter), + LoginGoogle: NewLoginGoogleHandler(e.LoginGoogle, mux, decoder, encoder, errhandler, formatter), + LoginGoogleCallback: NewLoginGoogleCallbackHandler(e.LoginGoogleCallback, mux, decoder, encoder, errhandler, formatter), + Logout: NewLogoutHandler(e.Logout, mux, decoder, encoder, errhandler, formatter), + SessionToken: NewSessionTokenHandler(e.SessionToken, mux, decoder, encoder, errhandler, formatter), + Static: http.FileServer(fileSystemStatic), } } @@ -74,6 +86,10 @@ func (s *Server) Service() string { return "web" } func (s *Server) Use(m func(http.Handler) http.Handler) { s.Index = m(s.Index) s.Another = m(s.Another) + s.LoginGoogle = m(s.LoginGoogle) + s.LoginGoogleCallback = m(s.LoginGoogleCallback) + s.Logout = m(s.Logout) + s.SessionToken = m(s.SessionToken) } // MethodNames returns the methods served. @@ -83,6 +99,10 @@ func (s *Server) MethodNames() []string { return web.MethodNames[:] } func Mount(mux goahttp.Muxer, h *Server) { MountIndexHandler(mux, h.Index) MountAnotherHandler(mux, h.Another) + MountLoginGoogleHandler(mux, h.LoginGoogle) + MountLoginGoogleCallbackHandler(mux, h.LoginGoogleCallback) + MountLogoutHandler(mux, h.Logout) + MountSessionTokenHandler(mux, h.SessionToken) MountStatic(mux, http.StripPrefix("/static", h.Static)) } @@ -91,7 +111,7 @@ func (s *Server) Mount(mux goahttp.Muxer) { Mount(mux, s) } -// MountIndexHandler configures the mux to serve the "web" service "index" +// MountIndexHandler configures the mux to serve the "web" service "Index" // endpoint. func MountIndexHandler(mux goahttp.Muxer, h http.Handler) { f, ok := h.(http.HandlerFunc) @@ -104,7 +124,7 @@ func MountIndexHandler(mux goahttp.Muxer, h http.Handler) { } // NewIndexHandler creates a HTTP handler which loads the HTTP request and -// calls the "web" service "index" endpoint. +// calls the "web" service "Index" endpoint. func NewIndexHandler( endpoint goa.Endpoint, mux goahttp.Muxer, @@ -115,11 +135,11 @@ func NewIndexHandler( ) http.Handler { var ( encodeResponse = EncodeIndexResponse(encoder) - encodeError = goahttp.ErrorEncoder(encoder, formatter) + encodeError = EncodeIndexError(encoder, formatter) ) return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ctx := context.WithValue(r.Context(), goahttp.AcceptTypeKey, r.Header.Get("Accept")) - ctx = context.WithValue(ctx, goa.MethodKey, "index") + ctx = context.WithValue(ctx, goa.MethodKey, "Index") ctx = context.WithValue(ctx, goa.ServiceKey, "web") var err error res, err := endpoint(ctx, nil) @@ -135,7 +155,7 @@ func NewIndexHandler( }) } -// MountAnotherHandler configures the mux to serve the "web" service "another" +// MountAnotherHandler configures the mux to serve the "web" service "Another" // endpoint. func MountAnotherHandler(mux goahttp.Muxer, h http.Handler) { f, ok := h.(http.HandlerFunc) @@ -148,7 +168,7 @@ func MountAnotherHandler(mux goahttp.Muxer, h http.Handler) { } // NewAnotherHandler creates a HTTP handler which loads the HTTP request and -// calls the "web" service "another" endpoint. +// calls the "web" service "Another" endpoint. func NewAnotherHandler( endpoint goa.Endpoint, mux goahttp.Muxer, @@ -159,11 +179,11 @@ func NewAnotherHandler( ) http.Handler { var ( encodeResponse = EncodeAnotherResponse(encoder) - encodeError = goahttp.ErrorEncoder(encoder, formatter) + encodeError = EncodeAnotherError(encoder, formatter) ) return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ctx := context.WithValue(r.Context(), goahttp.AcceptTypeKey, r.Header.Get("Accept")) - ctx = context.WithValue(ctx, goa.MethodKey, "another") + ctx = context.WithValue(ctx, goa.MethodKey, "Another") ctx = context.WithValue(ctx, goa.ServiceKey, "web") var err error res, err := endpoint(ctx, nil) @@ -179,6 +199,203 @@ func NewAnotherHandler( }) } +// MountLoginGoogleHandler configures the mux to serve the "web" service +// "LoginGoogle" endpoint. +func MountLoginGoogleHandler(mux goahttp.Muxer, h http.Handler) { + f, ok := h.(http.HandlerFunc) + if !ok { + f = func(w http.ResponseWriter, r *http.Request) { + h.ServeHTTP(w, r) + } + } + mux.Handle("GET", "/login/google", f) +} + +// NewLoginGoogleHandler creates a HTTP handler which loads the HTTP request +// and calls the "web" service "LoginGoogle" endpoint. +func NewLoginGoogleHandler( + endpoint goa.Endpoint, + mux goahttp.Muxer, + decoder func(*http.Request) goahttp.Decoder, + encoder func(context.Context, http.ResponseWriter) goahttp.Encoder, + errhandler func(context.Context, http.ResponseWriter, error), + formatter func(ctx context.Context, err error) goahttp.Statuser, +) http.Handler { + var ( + encodeResponse = EncodeLoginGoogleResponse(encoder) + encodeError = EncodeLoginGoogleError(encoder, formatter) + ) + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ctx := context.WithValue(r.Context(), goahttp.AcceptTypeKey, r.Header.Get("Accept")) + ctx = context.WithValue(ctx, goa.MethodKey, "LoginGoogle") + ctx = context.WithValue(ctx, goa.ServiceKey, "web") + var err error + res, err := endpoint(ctx, nil) + if err != nil { + if err := encodeError(ctx, w, err); err != nil { + errhandler(ctx, w, err) + } + return + } + if err := encodeResponse(ctx, w, res); err != nil { + errhandler(ctx, w, err) + } + }) +} + +// MountLoginGoogleCallbackHandler configures the mux to serve the "web" +// service "LoginGoogleCallback" endpoint. +func MountLoginGoogleCallbackHandler(mux goahttp.Muxer, h http.Handler) { + f, ok := h.(http.HandlerFunc) + if !ok { + f = func(w http.ResponseWriter, r *http.Request) { + h.ServeHTTP(w, r) + } + } + mux.Handle("GET", "/login/google/callback", f) +} + +// NewLoginGoogleCallbackHandler creates a HTTP handler which loads the HTTP +// request and calls the "web" service "LoginGoogleCallback" endpoint. +func NewLoginGoogleCallbackHandler( + endpoint goa.Endpoint, + mux goahttp.Muxer, + decoder func(*http.Request) goahttp.Decoder, + encoder func(context.Context, http.ResponseWriter) goahttp.Encoder, + errhandler func(context.Context, http.ResponseWriter, error), + formatter func(ctx context.Context, err error) goahttp.Statuser, +) http.Handler { + var ( + decodeRequest = DecodeLoginGoogleCallbackRequest(mux, decoder) + encodeResponse = EncodeLoginGoogleCallbackResponse(encoder) + encodeError = EncodeLoginGoogleCallbackError(encoder, formatter) + ) + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ctx := context.WithValue(r.Context(), goahttp.AcceptTypeKey, r.Header.Get("Accept")) + ctx = context.WithValue(ctx, goa.MethodKey, "LoginGoogleCallback") + ctx = context.WithValue(ctx, goa.ServiceKey, "web") + payload, err := decodeRequest(r) + if err != nil { + if err := encodeError(ctx, w, err); err != nil { + errhandler(ctx, w, err) + } + return + } + res, err := endpoint(ctx, payload) + if err != nil { + if err := encodeError(ctx, w, err); err != nil { + errhandler(ctx, w, err) + } + return + } + if err := encodeResponse(ctx, w, res); err != nil { + errhandler(ctx, w, err) + } + }) +} + +// MountLogoutHandler configures the mux to serve the "web" service "Logout" +// endpoint. +func MountLogoutHandler(mux goahttp.Muxer, h http.Handler) { + f, ok := h.(http.HandlerFunc) + if !ok { + f = func(w http.ResponseWriter, r *http.Request) { + h.ServeHTTP(w, r) + } + } + mux.Handle("GET", "/logout", f) +} + +// NewLogoutHandler creates a HTTP handler which loads the HTTP request and +// calls the "web" service "Logout" endpoint. +func NewLogoutHandler( + endpoint goa.Endpoint, + mux goahttp.Muxer, + decoder func(*http.Request) goahttp.Decoder, + encoder func(context.Context, http.ResponseWriter) goahttp.Encoder, + errhandler func(context.Context, http.ResponseWriter, error), + formatter func(ctx context.Context, err error) goahttp.Statuser, +) http.Handler { + var ( + decodeRequest = DecodeLogoutRequest(mux, decoder) + encodeResponse = EncodeLogoutResponse(encoder) + encodeError = EncodeLogoutError(encoder, formatter) + ) + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ctx := context.WithValue(r.Context(), goahttp.AcceptTypeKey, r.Header.Get("Accept")) + ctx = context.WithValue(ctx, goa.MethodKey, "Logout") + ctx = context.WithValue(ctx, goa.ServiceKey, "web") + payload, err := decodeRequest(r) + if err != nil { + if err := encodeError(ctx, w, err); err != nil { + errhandler(ctx, w, err) + } + return + } + res, err := endpoint(ctx, payload) + if err != nil { + if err := encodeError(ctx, w, err); err != nil { + errhandler(ctx, w, err) + } + return + } + if err := encodeResponse(ctx, w, res); err != nil { + errhandler(ctx, w, err) + } + }) +} + +// MountSessionTokenHandler configures the mux to serve the "web" service +// "SessionToken" endpoint. +func MountSessionTokenHandler(mux goahttp.Muxer, h http.Handler) { + f, ok := h.(http.HandlerFunc) + if !ok { + f = func(w http.ResponseWriter, r *http.Request) { + h.ServeHTTP(w, r) + } + } + mux.Handle("GET", "/session/token", f) +} + +// NewSessionTokenHandler creates a HTTP handler which loads the HTTP request +// and calls the "web" service "SessionToken" endpoint. +func NewSessionTokenHandler( + endpoint goa.Endpoint, + mux goahttp.Muxer, + decoder func(*http.Request) goahttp.Decoder, + encoder func(context.Context, http.ResponseWriter) goahttp.Encoder, + errhandler func(context.Context, http.ResponseWriter, error), + formatter func(ctx context.Context, err error) goahttp.Statuser, +) http.Handler { + var ( + decodeRequest = DecodeSessionTokenRequest(mux, decoder) + encodeResponse = EncodeSessionTokenResponse(encoder) + encodeError = EncodeSessionTokenError(encoder, formatter) + ) + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ctx := context.WithValue(r.Context(), goahttp.AcceptTypeKey, r.Header.Get("Accept")) + ctx = context.WithValue(ctx, goa.MethodKey, "SessionToken") + ctx = context.WithValue(ctx, goa.ServiceKey, "web") + payload, err := decodeRequest(r) + if err != nil { + if err := encodeError(ctx, w, err); err != nil { + errhandler(ctx, w, err) + } + return + } + res, err := endpoint(ctx, payload) + if err != nil { + if err := encodeError(ctx, w, err); err != nil { + errhandler(ctx, w, err) + } + return + } + if err := encodeResponse(ctx, w, res); err != nil { + errhandler(ctx, w, err) + } + }) +} + // appendFS is a custom implementation of fs.FS that appends a specified prefix // to the file paths before delegating the Open call to the underlying fs.FS. type appendFS struct { @@ -190,6 +407,8 @@ type appendFS struct { // passing it to the underlying fs.FS. func (s appendFS) Open(name string) (http.File, error) { switch name { + case "/*": + name = "/static" } return s.fs.Open(path.Join(s.prefix, name)) } @@ -200,8 +419,7 @@ func appendPrefix(fsys http.FileSystem, prefix string) http.FileSystem { return appendFS{prefix: prefix, fs: fsys} } -// MountStatic configures the mux to serve GET request made to "/static". +// MountStatic configures the mux to serve GET request made to "/static/*". func MountStatic(mux goahttp.Muxer, h http.Handler) { - mux.Handle("GET", "/static/", h.ServeHTTP) - mux.Handle("GET", "/static/{*path}", h.ServeHTTP) + mux.Handle("GET", "/static/*", h.ServeHTTP) } diff --git a/app/api/v1/gen/http/web/server/types.go b/app/api/v1/gen/http/web/server/types.go index 573bc71..339a653 100644 --- a/app/api/v1/gen/http/web/server/types.go +++ b/app/api/v1/gen/http/web/server/types.go @@ -6,3 +6,243 @@ // $ goa gen github.com/jace-ys/countup/api/v1 -o api/v1 package server + +import ( + web "github.com/jace-ys/countup/api/v1/gen/web" + goa "goa.design/goa/v3/pkg" +) + +// SessionTokenResponseBody is the type of the "web" service "SessionToken" +// endpoint HTTP response body. +type SessionTokenResponseBody struct { + Token string `form:"token" json:"token" xml:"token"` +} + +// IndexUnauthorizedResponseBody is the type of the "web" service "Index" +// endpoint HTTP response body for the "unauthorized" error. +type IndexUnauthorizedResponseBody struct { + // Name is the name of this class of errors. + Name string `form:"name" json:"name" xml:"name"` + // ID is a unique identifier for this particular occurrence of the problem. + ID string `form:"id" json:"id" xml:"id"` + // Message is a human-readable explanation specific to this occurrence of the + // problem. + Message string `form:"message" json:"message" xml:"message"` + // Is the error temporary? + Temporary bool `form:"temporary" json:"temporary" xml:"temporary"` + // Is the error a timeout? + Timeout bool `form:"timeout" json:"timeout" xml:"timeout"` + // Is the error a server-side fault? + Fault bool `form:"fault" json:"fault" xml:"fault"` +} + +// AnotherUnauthorizedResponseBody is the type of the "web" service "Another" +// endpoint HTTP response body for the "unauthorized" error. +type AnotherUnauthorizedResponseBody struct { + // Name is the name of this class of errors. + Name string `form:"name" json:"name" xml:"name"` + // ID is a unique identifier for this particular occurrence of the problem. + ID string `form:"id" json:"id" xml:"id"` + // Message is a human-readable explanation specific to this occurrence of the + // problem. + Message string `form:"message" json:"message" xml:"message"` + // Is the error temporary? + Temporary bool `form:"temporary" json:"temporary" xml:"temporary"` + // Is the error a timeout? + Timeout bool `form:"timeout" json:"timeout" xml:"timeout"` + // Is the error a server-side fault? + Fault bool `form:"fault" json:"fault" xml:"fault"` +} + +// LoginGoogleUnauthorizedResponseBody is the type of the "web" service +// "LoginGoogle" endpoint HTTP response body for the "unauthorized" error. +type LoginGoogleUnauthorizedResponseBody struct { + // Name is the name of this class of errors. + Name string `form:"name" json:"name" xml:"name"` + // ID is a unique identifier for this particular occurrence of the problem. + ID string `form:"id" json:"id" xml:"id"` + // Message is a human-readable explanation specific to this occurrence of the + // problem. + Message string `form:"message" json:"message" xml:"message"` + // Is the error temporary? + Temporary bool `form:"temporary" json:"temporary" xml:"temporary"` + // Is the error a timeout? + Timeout bool `form:"timeout" json:"timeout" xml:"timeout"` + // Is the error a server-side fault? + Fault bool `form:"fault" json:"fault" xml:"fault"` +} + +// LoginGoogleCallbackUnauthorizedResponseBody is the type of the "web" service +// "LoginGoogleCallback" endpoint HTTP response body for the "unauthorized" +// error. +type LoginGoogleCallbackUnauthorizedResponseBody struct { + // Name is the name of this class of errors. + Name string `form:"name" json:"name" xml:"name"` + // ID is a unique identifier for this particular occurrence of the problem. + ID string `form:"id" json:"id" xml:"id"` + // Message is a human-readable explanation specific to this occurrence of the + // problem. + Message string `form:"message" json:"message" xml:"message"` + // Is the error temporary? + Temporary bool `form:"temporary" json:"temporary" xml:"temporary"` + // Is the error a timeout? + Timeout bool `form:"timeout" json:"timeout" xml:"timeout"` + // Is the error a server-side fault? + Fault bool `form:"fault" json:"fault" xml:"fault"` +} + +// LogoutUnauthorizedResponseBody is the type of the "web" service "Logout" +// endpoint HTTP response body for the "unauthorized" error. +type LogoutUnauthorizedResponseBody struct { + // Name is the name of this class of errors. + Name string `form:"name" json:"name" xml:"name"` + // ID is a unique identifier for this particular occurrence of the problem. + ID string `form:"id" json:"id" xml:"id"` + // Message is a human-readable explanation specific to this occurrence of the + // problem. + Message string `form:"message" json:"message" xml:"message"` + // Is the error temporary? + Temporary bool `form:"temporary" json:"temporary" xml:"temporary"` + // Is the error a timeout? + Timeout bool `form:"timeout" json:"timeout" xml:"timeout"` + // Is the error a server-side fault? + Fault bool `form:"fault" json:"fault" xml:"fault"` +} + +// SessionTokenUnauthorizedResponseBody is the type of the "web" service +// "SessionToken" endpoint HTTP response body for the "unauthorized" error. +type SessionTokenUnauthorizedResponseBody struct { + // Name is the name of this class of errors. + Name string `form:"name" json:"name" xml:"name"` + // ID is a unique identifier for this particular occurrence of the problem. + ID string `form:"id" json:"id" xml:"id"` + // Message is a human-readable explanation specific to this occurrence of the + // problem. + Message string `form:"message" json:"message" xml:"message"` + // Is the error temporary? + Temporary bool `form:"temporary" json:"temporary" xml:"temporary"` + // Is the error a timeout? + Timeout bool `form:"timeout" json:"timeout" xml:"timeout"` + // Is the error a server-side fault? + Fault bool `form:"fault" json:"fault" xml:"fault"` +} + +// NewSessionTokenResponseBody builds the HTTP response body from the result of +// the "SessionToken" endpoint of the "web" service. +func NewSessionTokenResponseBody(res *web.SessionTokenResult) *SessionTokenResponseBody { + body := &SessionTokenResponseBody{ + Token: res.Token, + } + return body +} + +// NewIndexUnauthorizedResponseBody builds the HTTP response body from the +// result of the "Index" endpoint of the "web" service. +func NewIndexUnauthorizedResponseBody(res *goa.ServiceError) *IndexUnauthorizedResponseBody { + body := &IndexUnauthorizedResponseBody{ + Name: res.Name, + ID: res.ID, + Message: res.Message, + Temporary: res.Temporary, + Timeout: res.Timeout, + Fault: res.Fault, + } + return body +} + +// NewAnotherUnauthorizedResponseBody builds the HTTP response body from the +// result of the "Another" endpoint of the "web" service. +func NewAnotherUnauthorizedResponseBody(res *goa.ServiceError) *AnotherUnauthorizedResponseBody { + body := &AnotherUnauthorizedResponseBody{ + Name: res.Name, + ID: res.ID, + Message: res.Message, + Temporary: res.Temporary, + Timeout: res.Timeout, + Fault: res.Fault, + } + return body +} + +// NewLoginGoogleUnauthorizedResponseBody builds the HTTP response body from +// the result of the "LoginGoogle" endpoint of the "web" service. +func NewLoginGoogleUnauthorizedResponseBody(res *goa.ServiceError) *LoginGoogleUnauthorizedResponseBody { + body := &LoginGoogleUnauthorizedResponseBody{ + Name: res.Name, + ID: res.ID, + Message: res.Message, + Temporary: res.Temporary, + Timeout: res.Timeout, + Fault: res.Fault, + } + return body +} + +// NewLoginGoogleCallbackUnauthorizedResponseBody builds the HTTP response body +// from the result of the "LoginGoogleCallback" endpoint of the "web" service. +func NewLoginGoogleCallbackUnauthorizedResponseBody(res *goa.ServiceError) *LoginGoogleCallbackUnauthorizedResponseBody { + body := &LoginGoogleCallbackUnauthorizedResponseBody{ + Name: res.Name, + ID: res.ID, + Message: res.Message, + Temporary: res.Temporary, + Timeout: res.Timeout, + Fault: res.Fault, + } + return body +} + +// NewLogoutUnauthorizedResponseBody builds the HTTP response body from the +// result of the "Logout" endpoint of the "web" service. +func NewLogoutUnauthorizedResponseBody(res *goa.ServiceError) *LogoutUnauthorizedResponseBody { + body := &LogoutUnauthorizedResponseBody{ + Name: res.Name, + ID: res.ID, + Message: res.Message, + Temporary: res.Temporary, + Timeout: res.Timeout, + Fault: res.Fault, + } + return body +} + +// NewSessionTokenUnauthorizedResponseBody builds the HTTP response body from +// the result of the "SessionToken" endpoint of the "web" service. +func NewSessionTokenUnauthorizedResponseBody(res *goa.ServiceError) *SessionTokenUnauthorizedResponseBody { + body := &SessionTokenUnauthorizedResponseBody{ + Name: res.Name, + ID: res.ID, + Message: res.Message, + Temporary: res.Temporary, + Timeout: res.Timeout, + Fault: res.Fault, + } + return body +} + +// NewLoginGoogleCallbackPayload builds a web service LoginGoogleCallback +// endpoint payload. +func NewLoginGoogleCallbackPayload(code string, state string, sessionCookie string) *web.LoginGoogleCallbackPayload { + v := &web.LoginGoogleCallbackPayload{} + v.Code = code + v.State = state + v.SessionCookie = sessionCookie + + return v +} + +// NewLogoutPayload builds a web service Logout endpoint payload. +func NewLogoutPayload(sessionCookie string) *web.LogoutPayload { + v := &web.LogoutPayload{} + v.SessionCookie = sessionCookie + + return v +} + +// NewSessionTokenPayload builds a web service SessionToken endpoint payload. +func NewSessionTokenPayload(sessionCookie string) *web.SessionTokenPayload { + v := &web.SessionTokenPayload{} + v.SessionCookie = sessionCookie + + return v +} diff --git a/app/api/v1/gen/web/client.go b/app/api/v1/gen/web/client.go index 2a2f38a..2fc6620 100644 --- a/app/api/v1/gen/web/client.go +++ b/app/api/v1/gen/web/client.go @@ -15,19 +15,30 @@ import ( // Client is the "web" service client. type Client struct { - IndexEndpoint goa.Endpoint - AnotherEndpoint goa.Endpoint + IndexEndpoint goa.Endpoint + AnotherEndpoint goa.Endpoint + LoginGoogleEndpoint goa.Endpoint + LoginGoogleCallbackEndpoint goa.Endpoint + LogoutEndpoint goa.Endpoint + SessionTokenEndpoint goa.Endpoint } // NewClient initializes a "web" service client given the endpoints. -func NewClient(index, another goa.Endpoint) *Client { +func NewClient(index, another, loginGoogle, loginGoogleCallback, logout, sessionToken goa.Endpoint) *Client { return &Client{ - IndexEndpoint: index, - AnotherEndpoint: another, + IndexEndpoint: index, + AnotherEndpoint: another, + LoginGoogleEndpoint: loginGoogle, + LoginGoogleCallbackEndpoint: loginGoogleCallback, + LogoutEndpoint: logout, + SessionTokenEndpoint: sessionToken, } } -// Index calls the "index" endpoint of the "web" service. +// Index calls the "Index" endpoint of the "web" service. +// Index may return the following errors: +// - "unauthorized" (type *goa.ServiceError) +// - error: internal error func (c *Client) Index(ctx context.Context) (res []byte, err error) { var ires any ires, err = c.IndexEndpoint(ctx, nil) @@ -37,7 +48,10 @@ func (c *Client) Index(ctx context.Context) (res []byte, err error) { return ires.([]byte), nil } -// Another calls the "another" endpoint of the "web" service. +// Another calls the "Another" endpoint of the "web" service. +// Another may return the following errors: +// - "unauthorized" (type *goa.ServiceError) +// - error: internal error func (c *Client) Another(ctx context.Context) (res []byte, err error) { var ires any ires, err = c.AnotherEndpoint(ctx, nil) @@ -46,3 +60,56 @@ func (c *Client) Another(ctx context.Context) (res []byte, err error) { } return ires.([]byte), nil } + +// LoginGoogle calls the "LoginGoogle" endpoint of the "web" service. +// LoginGoogle may return the following errors: +// - "unauthorized" (type *goa.ServiceError) +// - error: internal error +func (c *Client) LoginGoogle(ctx context.Context) (res *LoginGoogleResult, err error) { + var ires any + ires, err = c.LoginGoogleEndpoint(ctx, nil) + if err != nil { + return + } + return ires.(*LoginGoogleResult), nil +} + +// LoginGoogleCallback calls the "LoginGoogleCallback" endpoint of the "web" +// service. +// LoginGoogleCallback may return the following errors: +// - "unauthorized" (type *goa.ServiceError) +// - error: internal error +func (c *Client) LoginGoogleCallback(ctx context.Context, p *LoginGoogleCallbackPayload) (res *LoginGoogleCallbackResult, err error) { + var ires any + ires, err = c.LoginGoogleCallbackEndpoint(ctx, p) + if err != nil { + return + } + return ires.(*LoginGoogleCallbackResult), nil +} + +// Logout calls the "Logout" endpoint of the "web" service. +// Logout may return the following errors: +// - "unauthorized" (type *goa.ServiceError) +// - error: internal error +func (c *Client) Logout(ctx context.Context, p *LogoutPayload) (res *LogoutResult, err error) { + var ires any + ires, err = c.LogoutEndpoint(ctx, p) + if err != nil { + return + } + return ires.(*LogoutResult), nil +} + +// SessionToken calls the "SessionToken" endpoint of the "web" service. +// SessionToken may return the following errors: +// - "unauthorized" (type *goa.ServiceError) +// - error: internal error +func (c *Client) SessionToken(ctx context.Context, p *SessionTokenPayload) (res *SessionTokenResult, err error) { + var ires any + ires, err = c.SessionTokenEndpoint(ctx, p) + if err != nil { + return + } + return ires.(*SessionTokenResult), nil +} diff --git a/app/api/v1/gen/web/endpoints.go b/app/api/v1/gen/web/endpoints.go index 280b9d1..cd5e9db 100644 --- a/app/api/v1/gen/web/endpoints.go +++ b/app/api/v1/gen/web/endpoints.go @@ -15,15 +15,23 @@ import ( // Endpoints wraps the "web" service endpoints. type Endpoints struct { - Index goa.Endpoint - Another goa.Endpoint + Index goa.Endpoint + Another goa.Endpoint + LoginGoogle goa.Endpoint + LoginGoogleCallback goa.Endpoint + Logout goa.Endpoint + SessionToken goa.Endpoint } // NewEndpoints wraps the methods of the "web" service with endpoints. func NewEndpoints(s Service) *Endpoints { return &Endpoints{ - Index: NewIndexEndpoint(s), - Another: NewAnotherEndpoint(s), + Index: NewIndexEndpoint(s), + Another: NewAnotherEndpoint(s), + LoginGoogle: NewLoginGoogleEndpoint(s), + LoginGoogleCallback: NewLoginGoogleCallbackEndpoint(s), + Logout: NewLogoutEndpoint(s), + SessionToken: NewSessionTokenEndpoint(s), } } @@ -31,9 +39,13 @@ func NewEndpoints(s Service) *Endpoints { func (e *Endpoints) Use(m func(goa.Endpoint) goa.Endpoint) { e.Index = m(e.Index) e.Another = m(e.Another) + e.LoginGoogle = m(e.LoginGoogle) + e.LoginGoogleCallback = m(e.LoginGoogleCallback) + e.Logout = m(e.Logout) + e.SessionToken = m(e.SessionToken) } -// NewIndexEndpoint returns an endpoint function that calls the method "index" +// NewIndexEndpoint returns an endpoint function that calls the method "Index" // of service "web". func NewIndexEndpoint(s Service) goa.Endpoint { return func(ctx context.Context, req any) (any, error) { @@ -42,9 +54,44 @@ func NewIndexEndpoint(s Service) goa.Endpoint { } // NewAnotherEndpoint returns an endpoint function that calls the method -// "another" of service "web". +// "Another" of service "web". func NewAnotherEndpoint(s Service) goa.Endpoint { return func(ctx context.Context, req any) (any, error) { return s.Another(ctx) } } + +// NewLoginGoogleEndpoint returns an endpoint function that calls the method +// "LoginGoogle" of service "web". +func NewLoginGoogleEndpoint(s Service) goa.Endpoint { + return func(ctx context.Context, req any) (any, error) { + return s.LoginGoogle(ctx) + } +} + +// NewLoginGoogleCallbackEndpoint returns an endpoint function that calls the +// method "LoginGoogleCallback" of service "web". +func NewLoginGoogleCallbackEndpoint(s Service) goa.Endpoint { + return func(ctx context.Context, req any) (any, error) { + p := req.(*LoginGoogleCallbackPayload) + return s.LoginGoogleCallback(ctx, p) + } +} + +// NewLogoutEndpoint returns an endpoint function that calls the method +// "Logout" of service "web". +func NewLogoutEndpoint(s Service) goa.Endpoint { + return func(ctx context.Context, req any) (any, error) { + p := req.(*LogoutPayload) + return s.Logout(ctx, p) + } +} + +// NewSessionTokenEndpoint returns an endpoint function that calls the method +// "SessionToken" of service "web". +func NewSessionTokenEndpoint(s Service) goa.Endpoint { + return func(ctx context.Context, req any) (any, error) { + p := req.(*SessionTokenPayload) + return s.SessionToken(ctx, p) + } +} diff --git a/app/api/v1/gen/web/service.go b/app/api/v1/gen/web/service.go index 6aaa0e1..40cbf10 100644 --- a/app/api/v1/gen/web/service.go +++ b/app/api/v1/gen/web/service.go @@ -9,14 +9,24 @@ package web import ( "context" + + goa "goa.design/goa/v3/pkg" ) // Service is the web service interface. type Service interface { - // Index implements index. + // Index implements Index. Index(context.Context) (res []byte, err error) - // Another implements another. + // Another implements Another. Another(context.Context) (res []byte, err error) + // LoginGoogle implements LoginGoogle. + LoginGoogle(context.Context) (res *LoginGoogleResult, err error) + // LoginGoogleCallback implements LoginGoogleCallback. + LoginGoogleCallback(context.Context, *LoginGoogleCallbackPayload) (res *LoginGoogleCallbackResult, err error) + // Logout implements Logout. + Logout(context.Context, *LogoutPayload) (res *LogoutResult, err error) + // SessionToken implements SessionToken. + SessionToken(context.Context, *SessionTokenPayload) (res *SessionTokenResult, err error) } // APIName is the name of the API as defined in the design. @@ -33,4 +43,52 @@ const ServiceName = "web" // MethodNames lists the service method names as defined in the design. These // are the same values that are set in the endpoint request contexts under the // MethodKey key. -var MethodNames = [2]string{"index", "another"} +var MethodNames = [6]string{"Index", "Another", "LoginGoogle", "LoginGoogleCallback", "Logout", "SessionToken"} + +// LoginGoogleCallbackPayload is the payload type of the web service +// LoginGoogleCallback method. +type LoginGoogleCallbackPayload struct { + Code string + State string + SessionCookie string +} + +// LoginGoogleCallbackResult is the result type of the web service +// LoginGoogleCallback method. +type LoginGoogleCallbackResult struct { + RedirectURL string + SessionCookie string +} + +// LoginGoogleResult is the result type of the web service LoginGoogle method. +type LoginGoogleResult struct { + RedirectURL string + SessionCookie string +} + +// LogoutPayload is the payload type of the web service Logout method. +type LogoutPayload struct { + SessionCookie string +} + +// LogoutResult is the result type of the web service Logout method. +type LogoutResult struct { + RedirectURL string + SessionCookie string +} + +// SessionTokenPayload is the payload type of the web service SessionToken +// method. +type SessionTokenPayload struct { + SessionCookie string +} + +// SessionTokenResult is the result type of the web service SessionToken method. +type SessionTokenResult struct { + Token string +} + +// MakeUnauthorized builds a goa.ServiceError from an error. +func MakeUnauthorized(err error) *goa.ServiceError { + return goa.NewServiceError(err, "unauthorized", false, false, false) +} diff --git a/app/cmd/countup/server.go b/app/cmd/countup/server.go index cb3a252..9c98202 100644 --- a/app/cmd/countup/server.go +++ b/app/cmd/countup/server.go @@ -5,15 +5,19 @@ import ( "fmt" "time" + "github.com/gorilla/securecookie" + "github.com/markbates/goth/providers/google" + apiv1 "github.com/jace-ys/countup/api/v1" genapi "github.com/jace-ys/countup/api/v1/gen/api" + grpcapiclient "github.com/jace-ys/countup/api/v1/gen/grpc/api/client" apipb "github.com/jace-ys/countup/api/v1/gen/grpc/api/pb" grpcapi "github.com/jace-ys/countup/api/v1/gen/grpc/api/server" httpapi "github.com/jace-ys/countup/api/v1/gen/http/api/server" httpweb "github.com/jace-ys/countup/api/v1/gen/http/web/server" genweb "github.com/jace-ys/countup/api/v1/gen/web" "github.com/jace-ys/countup/internal/app" - "github.com/jace-ys/countup/internal/endpoints" + "github.com/jace-ys/countup/internal/endpoint" "github.com/jace-ys/countup/internal/handler/api" "github.com/jace-ys/countup/internal/handler/web" "github.com/jace-ys/countup/internal/instrument" @@ -45,6 +49,12 @@ type ServerCmd struct { Counter struct { FinalizeWindow time.Duration `env:"FINALIZE_WINDOW" default:"1m" help:"Time period to wait before finalizing counter increments."` } `embed:"" envprefix:"COUNTER_" prefix:"counter."` + + OAuth struct { + ClientID string `env:"CLIENT_ID" required:"" help:"Client ID for the Google OAuth2 configuration."` + ClientSecret string `env:"CLIENT_SECRET" required:"" help:"Client secret for the Google OAuth2 configuration."` + RedirectURL string `env:"REDIRECT_URL" default:"http://localhost:8080/login/google/callback" help:"URL to redirect to upon successful OAuth2 authentication."` + } `embed:"" envprefix:"OAUTH_" prefix:"oauth."` } func (c *ServerCmd) Run(ctx context.Context, g *Globals) error { @@ -69,48 +79,65 @@ func (c *ServerCmd) Run(ctx context.Context, g *Globals) error { return fmt.Errorf("init worker pool: %w", err) } - httpsrv := app.NewHTTPServer(ctx, "app.http", c.Port) - grpcsrv := app.NewGRPCServer[apipb.APIServer](ctx, "app.grpc", c.Port+1) + auth := google.New(c.OAuth.ClientID, c.OAuth.ClientSecret, c.OAuth.RedirectURL, + "https://www.googleapis.com/auth/userinfo.email", + ) + + counterSvc := counter.New(db, worker, counterstore.New(), c.Counter.FinalizeWindow) + // userSvc := user.New(db, counterstore.New()) + + httpSrv := app.NewHTTPServer(ctx, "app.http", c.Port) + grpcSrv := app.NewGRPCServer[apipb.APIServer](ctx, "app.grpc", c.Port+1) admin := app.NewAdminServer(ctx, c.AdminPort, g.Debug) - admin.Administer(httpsrv, grpcsrv, worker) + admin.Administer(httpSrv, grpcSrv, worker) { - countersvc := counter.New(db, worker, counterstore.New(), c.Counter.FinalizeWindow) - // usersvc := counter.New(db, counterstore.New()) - - handler, err := api.NewHandler(worker, countersvc) + handler, err := api.NewHandler(worker, auth, counterSvc) if err != nil { - return fmt.Errorf("init handler: %w", err) + return fmt.Errorf("init api handler: %w", err) } admin.Administer(handler) - ep := endpoints.Goa(genapi.NewEndpoints).Adapt(handler) + ep := endpoint.Goa(genapi.NewEndpoints).Adapt(handler) { transport := transport.GoaHTTP(httpapi.New, httpapi.Mount) - httpsrv.RegisterHandler("/api/v1", transport.Adapt(ctx, ep, apiv1.OpenAPIFS)) + httpSrv.RegisterHandler("/api/v1", transport.Adapt(ctx, ep, apiv1.OpenAPIFS)) } { transport := transport.GoaGRPC(grpcapi.New) - grpcsrv.RegisterHandler(&apipb.API_ServiceDesc, transport.Adapt(ctx, ep)) + grpcSrv.RegisterHandler(&apipb.API_ServiceDesc, transport.Adapt(ctx, ep)) } } { - handler, err := web.NewHandler() + tc, err := transport.GoaGRPCClient(grpcapiclient.NewClient).Adapt(grpcSrv.Addr()) + if err != nil { + return fmt.Errorf("init api client: %w", err) + } + defer tc.Close() + + apiClient := genapi.NewClient( + tc.Client().AuthToken(), + tc.Client().CounterGet(), + tc.Client().CounterIncrement(), + tc.Client().Echo(), + ) + + handler, err := web.NewHandler(auth, apiClient, securecookie.GenerateRandomKey(32)) if err != nil { - return fmt.Errorf("init web: %w", err) + return fmt.Errorf("init web handler: %w", err) } admin.Administer(handler) - ep := endpoints.Goa(genweb.NewEndpoints).Adapt(handler) + ep := endpoint.Goa(genweb.NewEndpoints).Adapt(handler) transport := transport.GoaHTTP(httpweb.New, httpweb.Mount) - httpsrv.RegisterHandler("/", transport.Adapt(ctx, ep, web.StaticFS)) + httpSrv.RegisterHandler("/", transport.Adapt(ctx, ep, web.StaticFS)) } - err = app.New(httpsrv, grpcsrv, admin, worker).Run(ctx) + err = app.New(httpSrv, grpcSrv, admin, worker).Run(ctx) if err != nil { slog.Error(ctx, "encountered error while running app", err) return fmt.Errorf("app run: %w", err) diff --git a/app/go.mod b/app/go.mod index 560f040..91b9d9b 100644 --- a/app/go.mod +++ b/app/go.mod @@ -7,8 +7,10 @@ require ( github.com/alexliesenfeld/health v0.8.0 github.com/exaring/otelpgx v0.6.2 github.com/go-chi/chi/v5 v5.1.0 + github.com/gorilla/securecookie v1.1.2 github.com/jackc/pgerrcode v0.0.0-20240316143900-6e2875d9b438 github.com/jackc/pgx/v5 v5.7.1 + github.com/markbates/goth v1.80.0 github.com/pressly/goose/v3 v3.22.1 github.com/riverqueue/river v0.13.0 github.com/riverqueue/river/riverdriver/riverpgxv5 v0.13.0 @@ -31,6 +33,7 @@ require ( ) require ( + cloud.google.com/go/compute/metadata v0.5.0 // indirect github.com/aws/smithy-go v1.22.0 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect @@ -64,6 +67,7 @@ require ( golang.org/x/crypto v0.28.0 // indirect golang.org/x/mod v0.21.0 // indirect golang.org/x/net v0.30.0 // indirect + golang.org/x/oauth2 v0.22.0 // indirect golang.org/x/sys v0.26.0 // indirect golang.org/x/term v0.25.0 // indirect golang.org/x/text v0.19.0 // indirect diff --git a/app/go.sum b/app/go.sum index 694f93a..2142938 100644 --- a/app/go.sum +++ b/app/go.sum @@ -1,3 +1,5 @@ +cloud.google.com/go/compute/metadata v0.5.0 h1:Zr0eK8JbFv6+Wi4ilXAR8FJ3wyNdpxHKJNPos6LTZOY= +cloud.google.com/go/compute/metadata v0.5.0/go.mod h1:aHnloV2TPI38yx4s9+wAZhHykWvVCfu7hQbF+9CWoiY= github.com/alecthomas/assert/v2 v2.10.0 h1:jjRCHsj6hBJhkmhznrCzoNpbA3zqy0fYiUcYZP/GkPY= github.com/alecthomas/assert/v2 v2.10.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= github.com/alecthomas/kong v1.2.1 h1:E8jH4Tsgv6wCRX2nGrdPyHDUCSG83WH2qE4XLACD33Q= @@ -30,8 +32,12 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= 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.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= +github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 h1:asbCHRVmodnJTuQ3qamDwqVOIjwqUPTYmYuemVOx+Ys= @@ -60,6 +66,8 @@ github.com/manveru/faker v0.0.0-20171103152722-9fbc68a78c4d h1:Zj+PHjnhRYWBK6RqC github.com/manveru/faker v0.0.0-20171103152722-9fbc68a78c4d/go.mod h1:WZy8Q5coAB1zhY9AOBJP0O6J4BuDfbupUDavKY+I3+s= github.com/manveru/gobdd v0.0.0-20131210092515-f1a17fdd710b h1:3E44bLeN8uKYdfQqVQycPnaVviZdBLbizFhU49mtbe4= github.com/manveru/gobdd v0.0.0-20131210092515-f1a17fdd710b/go.mod h1:Bj8LjjP0ReT1eKt5QlKjwgi5AFm5mI6O1A2G4ChI0Ag= +github.com/markbates/goth v1.80.0 h1:NnvatczZDzOs1hn9Ug+dVYf2Viwwkp/ZDX5K+GLjan8= +github.com/markbates/goth v1.80.0/go.mod h1:4/GYHo+W6NWisrMPZnq0Yr2Q70UntNLn7KXEFhrIdAY= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mfridman/interpolate v0.0.2 h1:pnuTK7MQIxxFz1Gr+rjSIx9u7qVjf5VOoM/u6BbAxPY= @@ -153,6 +161,8 @@ golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0= golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= +golang.org/x/oauth2 v0.22.0 h1:BzDx2FehcG7jJwgWLELCdmLuxk2i+x9UDpSiss2u0ZA= +golang.org/x/oauth2 v0.22.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= diff --git a/app/internal/app/admin.go b/app/internal/app/admin.go index 85427df..c8a1644 100644 --- a/app/internal/app/admin.go +++ b/app/internal/app/admin.go @@ -57,12 +57,12 @@ func (s *AdminServer) Serve(ctx context.Context) error { func (s *AdminServer) router(ctx context.Context) http.Handler { s.mux.Get("/healthz", health.NewHandler(healthz.NewChecker(s.checks...))) - goamux := goahttp.NewMuxer() - debug.MountPprofHandlers(debug.Adapt(goamux), debug.WithPrefix("/pprof")) + mux := goahttp.NewMuxer() + debug.MountPprofHandlers(debug.Adapt(mux), debug.WithPrefix("/pprof")) if s.debug { - debug.MountDebugLogEnabler(debug.Adapt(goamux), debug.WithPath("/settings")) + debug.MountDebugLogEnabler(debug.Adapt(mux), debug.WithPath("/settings")) } - s.mux.Mount("/debug", goamux) + s.mux.Mount("/debug", mux) logCtx := log.With(ctx, slog.KV("server", s.Name())) return chainMiddleware(s.mux, diff --git a/app/internal/app/http.go b/app/internal/app/http.go index bea276f..8592fe6 100644 --- a/app/internal/app/http.go +++ b/app/internal/app/http.go @@ -12,6 +12,7 @@ import ( "go.opentelemetry.io/otel/attribute" "goa.design/clue/debug" "goa.design/clue/log" + "goa.design/goa/v3/http/middleware" "github.com/jace-ys/countup/internal/healthz" "github.com/jace-ys/countup/internal/slog" @@ -42,11 +43,8 @@ func NewHTTPServer(ctx context.Context, name string, port int) *HTTPServer { } func (s *HTTPServer) RegisterHandler(path string, h http.Handler) { - if path == "/" { - s.mux.Mount(path, h) - } else { - s.mux.Mount(path, http.StripPrefix(path, h)) - } + pattern := path + "*" + s.mux.Handle(pattern, h) } var _ Server = (*HTTPServer)(nil) @@ -82,6 +80,7 @@ func (s *HTTPServer) router(ctx context.Context) http.Handler { return chainMiddleware(s.mux, withPathFilter(telemetry.HTTP(attribute.String("http.server.name", s.Name())), excludedPaths), recovery.HTTP(logCtx), + withPathFilter(middleware.PopulateRequestContext(), excludedPaths), withPathFilter(idgen.HTTP(), excludedPaths), withPathFilter(slog.HTTP(logCtx), excludedPaths), withPathFilter(debug.HTTP(), excludedPaths), diff --git a/app/internal/endpoints/goa.go b/app/internal/endpoint/goa.go similarity index 82% rename from app/internal/endpoints/goa.go rename to app/internal/endpoint/goa.go index 4d3616d..b3428ad 100644 --- a/app/internal/endpoints/goa.go +++ b/app/internal/endpoint/goa.go @@ -1,12 +1,12 @@ -package endpoints +package endpoint import ( "goa.design/clue/debug" "goa.design/clue/log" goa "goa.design/goa/v3/pkg" - "github.com/jace-ys/countup/internal/endpoints/middleware/goaerror" - "github.com/jace-ys/countup/internal/endpoints/middleware/tracer" + "github.com/jace-ys/countup/internal/endpoint/middleware/goaerror" + "github.com/jace-ys/countup/internal/endpoint/middleware/tracer" ) type GoaAdapter[S any, E GoaEndpoints] struct { diff --git a/app/internal/endpoints/middleware/goaerror/reporter.go b/app/internal/endpoint/middleware/goaerror/reporter.go similarity index 100% rename from app/internal/endpoints/middleware/goaerror/reporter.go rename to app/internal/endpoint/middleware/goaerror/reporter.go diff --git a/app/internal/endpoints/middleware/tracer/endpoint.go b/app/internal/endpoint/middleware/tracer/endpoint.go similarity index 83% rename from app/internal/endpoints/middleware/tracer/endpoint.go rename to app/internal/endpoint/middleware/tracer/endpoint.go index df5fa2b..15503cc 100644 --- a/app/internal/endpoints/middleware/tracer/endpoint.go +++ b/app/internal/endpoint/middleware/tracer/endpoint.go @@ -22,7 +22,8 @@ func Endpoint(e goa.Endpoint) goa.Endpoint { method = m } - ctx, span := instrument.OTel.Tracer().Start(ctx, fmt.Sprintf("goa.endpoint/%s.%s", service, method)) + source := fmt.Sprintf("goa.endpoint/%s.%s", service, method) + ctx, span := instrument.OTel.Tracer().Start(ctx, source) span.SetAttributes(attribute.String("endpoint.service", service), attribute.String("endpoint.method", method)) defer span.End() diff --git a/app/internal/handler/api/auth.go b/app/internal/handler/api/auth.go new file mode 100644 index 0000000..9c16f54 --- /dev/null +++ b/app/internal/handler/api/auth.go @@ -0,0 +1,23 @@ +package api + +import ( + "context" + "fmt" + + "goa.design/goa/v3/security" + + "github.com/jace-ys/countup/api/v1/gen/api" +) + +var _ api.Auther = (*Handler)(nil) + +func (h *Handler) JWTAuth(ctx context.Context, token string, schema *security.JWTScheme) (context.Context, error) { + fmt.Println("token", token) + return ctx, nil +} + +func (h *Handler) AuthToken(ctx context.Context, req *api.AuthTokenPayload) (*api.AuthTokenResult, error) { + return &api.AuthTokenResult{ + Token: req.AccessToken, + }, nil +} diff --git a/app/internal/handler/api/increment.go b/app/internal/handler/api/counter.go similarity index 75% rename from app/internal/handler/api/increment.go rename to app/internal/handler/api/counter.go index ce2bb12..09d89a8 100644 --- a/app/internal/handler/api/increment.go +++ b/app/internal/handler/api/counter.go @@ -10,7 +10,7 @@ import ( "github.com/jace-ys/countup/internal/service/counter" ) -func (h *Handler) CounterGet(ctx context.Context) (*api.CounterInfo, error) { +func (h *Handler) CounterGet(ctx context.Context, req *api.CounterGetPayload) (*api.CounterInfo, error) { info, err := h.counter.GetInfo(ctx) if err != nil { return nil, goa.Fault("get counter info: %s", err) @@ -26,11 +26,11 @@ func (h *Handler) CounterGet(ctx context.Context) (*api.CounterInfo, error) { func (h *Handler) CounterIncrement(ctx context.Context, req *api.CounterIncrementPayload) (*api.CounterInfo, error) { if err := h.counter.RequestIncrement(ctx, req.User); err != nil { - var multipleRequestErr *counter.MultipleIncrementRequestError + var multipleIncrementRequestErr *counter.MultipleIncrementRequestError switch { - case errors.As(err, &multipleRequestErr): + case errors.As(err, &multipleIncrementRequestErr): return nil, api.MakeExistingIncrementRequest( - errors.New("user already made an increment request in the current finalize window, please try again after the next finalize time"), + errors.New("user already made an increment request in the recent finalize window, please try again after the next finalize time"), ) default: return nil, goa.Fault("request increment: %s", err) diff --git a/app/internal/handler/api/handler.go b/app/internal/handler/api/handler.go index d7c9ac9..be16fcc 100644 --- a/app/internal/handler/api/handler.go +++ b/app/internal/handler/api/handler.go @@ -4,6 +4,7 @@ import ( "context" "github.com/alexliesenfeld/health" + "github.com/markbates/goth" "github.com/jace-ys/countup/api/v1/gen/api" "github.com/jace-ys/countup/internal/healthz" @@ -15,6 +16,7 @@ var _ api.Service = (*Handler)(nil) type Handler struct { workers *worker.Pool + auth goth.Provider counter CounterService } @@ -23,11 +25,12 @@ type CounterService interface { RequestIncrement(ctx context.Context, user string) error } -func NewHandler(workers *worker.Pool, counter CounterService) (*Handler, error) { +func NewHandler(workers *worker.Pool, auth goth.Provider, counter CounterService) (*Handler, error) { worker.Register(workers, &EchoWorker{}) return &Handler{ workers: workers, + auth: auth, counter: counter, }, nil } diff --git a/app/internal/handler/web/authn.go b/app/internal/handler/web/authn.go new file mode 100644 index 0000000..2ea3104 --- /dev/null +++ b/app/internal/handler/web/authn.go @@ -0,0 +1,111 @@ +package web + +import ( + "context" + "errors" + "fmt" + "net/url" + + "github.com/jace-ys/countup/api/v1/gen/api" + "github.com/jace-ys/countup/api/v1/gen/web" + "github.com/jace-ys/countup/internal/transport/middleware/idgen" +) + +const sessionName = "countup.session" + +type authSession struct { + State string + Token string + Session string +} + +func (h *Handler) LoginGoogle(ctx context.Context) (*web.LoginGoogleResult, error) { + state := idgen.RequestIDFromContext(ctx) + + session, err := h.authn.BeginAuth(state) + if err != nil { + return nil, err + } + + redirectURL, err := session.GetAuthURL() + if err != nil { + return nil, err + } + + sessionCookie, err := h.cookies.Encode(sessionName, &authSession{ + State: state, + Session: session.Marshal(), + }) + if err != nil { + return nil, err + } + + return &web.LoginGoogleResult{ + RedirectURL: redirectURL, + SessionCookie: sessionCookie, + }, nil +} + +func (h *Handler) LoginGoogleCallback(ctx context.Context, req *web.LoginGoogleCallbackPayload) (*web.LoginGoogleCallbackResult, error) { + var auth *authSession + if err := h.cookies.Decode(sessionName, req.SessionCookie, &auth); err != nil { + return nil, err + } + + if req.State != auth.State { + return nil, web.MakeUnauthorized(errors.New("invalid state")) + } + + session, err := h.authn.UnmarshalSession(auth.Session) + if err != nil { + return nil, err + } + + params := url.Values{} + params.Set("code", req.Code) + + accessToken, err := session.Authorize(h.authn, params) + if err != nil { + return nil, err + } + + res, err := h.api.AuthToken(ctx, &api.AuthTokenPayload{ + Provider: "google", + AccessToken: accessToken, + }) + if err != nil { + return nil, err + } + + sessionCookie, err := h.cookies.Encode(sessionName, &authSession{ + Token: res.Token, + }) + if err != nil { + return nil, err + } + + return &web.LoginGoogleCallbackResult{ + RedirectURL: "/", + SessionCookie: sessionCookie, + }, nil +} + +func (h *Handler) Logout(ctx context.Context, req *web.LogoutPayload) (*web.LogoutResult, error) { + return &web.LogoutResult{ + RedirectURL: "/", + SessionCookie: "", + }, nil +} + +func (h *Handler) SessionToken(ctx context.Context, req *web.SessionTokenPayload) (*web.SessionTokenResult, error) { + var auth *authSession + if err := h.cookies.Decode(sessionName, req.SessionCookie, &auth); err != nil { + return nil, err + } + + fmt.Printf("%+v\n", auth) + + return &web.SessionTokenResult{ + Token: auth.Token, + }, nil +} diff --git a/app/internal/handler/web/handler.go b/app/internal/handler/web/handler.go index aa9f268..c2c5c19 100644 --- a/app/internal/handler/web/handler.go +++ b/app/internal/handler/web/handler.go @@ -1,14 +1,16 @@ package web import ( - "bytes" "context" "embed" "fmt" "html/template" "github.com/alexliesenfeld/health" + "github.com/gorilla/securecookie" + "github.com/markbates/goth" + "github.com/jace-ys/countup/api/v1/gen/api" "github.com/jace-ys/countup/api/v1/gen/web" "github.com/jace-ys/countup/internal/healthz" ) @@ -24,17 +26,23 @@ var ( var _ web.Service = (*Handler)(nil) type Handler struct { - tmpls *template.Template + authn goth.Provider + cookies *securecookie.SecureCookie + api *api.Client + tmpls *template.Template } -func NewHandler() (*Handler, error) { +func NewHandler(authn goth.Provider, api *api.Client, sessionKey []byte) (*Handler, error) { tmpls, err := template.New("tmpls").ParseFS(templateFS, "**/*.html") if err != nil { return nil, fmt.Errorf("parse templates: %w", err) } return &Handler{ - tmpls: tmpls, + authn: authn, + cookies: securecookie.New(sessionKey, nil).MaxAge(86400), + api: api, + tmpls: tmpls, }, nil } @@ -50,20 +58,3 @@ func (h *Handler) HealthChecks() []health.Check { }, } } - -var commonVars = struct { - BaseAPIEndpoint string -}{ - BaseAPIEndpoint: "/api/v1", -} - -type renderData struct { - Vars any - Data any -} - -func (h *Handler) render(page string, data any) ([]byte, error) { - buf := &bytes.Buffer{} - err := h.tmpls.ExecuteTemplate(buf, page, renderData{commonVars, data}) - return buf.Bytes(), err -} diff --git a/app/internal/handler/web/pages.go b/app/internal/handler/web/pages.go index 24dcbf6..6322dfe 100644 --- a/app/internal/handler/web/pages.go +++ b/app/internal/handler/web/pages.go @@ -1,7 +1,9 @@ package web import ( + "bytes" "context" + "fmt" ) func (h *Handler) Index(ctx context.Context) ([]byte, error) { @@ -23,3 +25,15 @@ func (h *Handler) Another(ctx context.Context) ([]byte, error) { return h.render("another.html", data) } + +type renderData struct { + Data any +} + +func (h *Handler) render(page string, data any) ([]byte, error) { + buf := &bytes.Buffer{} + if err := h.tmpls.ExecuteTemplate(buf, page, renderData{data}); err != nil { + return nil, fmt.Errorf("render: %w", err) + } + return buf.Bytes(), nil +} diff --git a/app/internal/handler/web/templates/index.html b/app/internal/handler/web/templates/index.html index 3c1b229..8ff9222 100644 --- a/app/internal/handler/web/templates/index.html +++ b/app/internal/handler/web/templates/index.html @@ -10,10 +10,14 @@

Hello {{ .Data.Name }}

- +


+

+ +

+ \ No newline at end of file diff --git a/app/internal/healthz/grpc.go b/app/internal/healthz/grpc.go index 46232d6..bfeeb78 100644 --- a/app/internal/healthz/grpc.go +++ b/app/internal/healthz/grpc.go @@ -12,7 +12,7 @@ import ( func GRPCCheck(name, target string) health.Check { return health.Check{ - Name: fmt.Sprintf("grpc:%s", name), + Name: "grpc:" + name, Check: func(ctx context.Context) error { opts := []grpc.DialOption{ grpc.WithTransportCredentials(insecure.NewCredentials()), diff --git a/app/internal/healthz/http.go b/app/internal/healthz/http.go index 33df5eb..9ee4816 100644 --- a/app/internal/healthz/http.go +++ b/app/internal/healthz/http.go @@ -10,7 +10,7 @@ import ( func HTTPCheck(name, url string) health.Check { return health.Check{ - Name: fmt.Sprintf("http:%s", name), + Name: "http:" + name, Check: func(ctx context.Context) error { req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { diff --git a/app/internal/service/counter/finalize.go b/app/internal/service/counter/finalize.go index 147d6b2..4372267 100644 --- a/app/internal/service/counter/finalize.go +++ b/app/internal/service/counter/finalize.go @@ -36,12 +36,16 @@ func NewIncrementWorker(db *pgxpool.Pool, store counterstore.Querier) *FinalizeI } func (w *FinalizeIncrementWorker) Work(ctx context.Context, job *river.Job[FinalizeIncrementJobArgs]) error { + ctx = slog.With(ctx, slog.KV("finalize.window", job.Args.FinalizeWindow)) + tx, err := w.db.Begin(ctx) if err != nil { return fmt.Errorf("%w: tx begin: %w", ErrDBConn, err) } defer tx.Rollback(ctx) + slog.Info(ctx, "listing increment requests") + requests, err := w.store.ListIncrementRequests(ctx, tx) if err != nil { return fmt.Errorf("%w: %w", ErrListIncrementRequests, err) @@ -56,7 +60,6 @@ func (w *FinalizeIncrementWorker) Work(ctx context.Context, job *river.Job[Final case 1: slog.Info(ctx, "only one increment request in finalize window, incrementing counter", - slog.KV("finalize.window", job.Args.FinalizeWindow), slog.KV("finalize.user", requests[0].RequestedBy), ) @@ -76,7 +79,6 @@ func (w *FinalizeIncrementWorker) Work(ctx context.Context, job *river.Job[Final default: slog.Info(ctx, "multiple increment requests in finalize window, resetting counter", slog.KV("requests.count", len(requests)), - slog.KV("finalize.window", job.Args.FinalizeWindow), slog.KV("finalize.user", requests[0].RequestedBy), ) @@ -85,9 +87,9 @@ func (w *FinalizeIncrementWorker) Work(ctx context.Context, job *river.Job[Final } } - slog.Info(ctx, "truncating increment requests table") + slog.Info(ctx, "deleting increment requests") - if err := w.store.TruncateIncrementRequests(ctx, tx); err != nil { + if err := w.store.DeleteIncrementRequests(ctx, tx); err != nil { return fmt.Errorf("%w: %w", ErrTruncateIncrementRequests, err) } diff --git a/app/internal/service/counter/service.go b/app/internal/service/counter/service.go index ff380d2..51b8d96 100644 --- a/app/internal/service/counter/service.go +++ b/app/internal/service/counter/service.go @@ -39,14 +39,14 @@ func New(db *pgxpool.Pool, workers *worker.Pool, store counterstore.Querier, fin } func (s *Service) GetInfo(ctx context.Context) (*Info, error) { - slog.Info(ctx, "getting counter") - tx, err := s.db.Begin(ctx) if err != nil { return nil, fmt.Errorf("%w: tx begin: %w", ErrDBConn, err) } defer tx.Rollback(ctx) + slog.Info(ctx, "getting counter") + counter, err := s.store.GetCounter(ctx, tx) if err != nil { return nil, fmt.Errorf("%w: %w", ErrGetCounter, err) @@ -75,14 +75,13 @@ func (s *Service) RequestIncrement(ctx context.Context, user string) error { slog.Info(ctx, "inserting increment request") - existing, err := s.store.InsertIncrementRequest(ctx, tx, counterstore.InsertIncrementRequestParams{ + if err := s.store.InsertIncrementRequest(ctx, tx, counterstore.InsertIncrementRequestParams{ RequestedBy: user, RequestedAt: pgtype.Timestamptz{ Time: time.Now(), Valid: true, }, - }) - if err != nil { + }); err != nil { var pgErr *pgconn.PgError if errors.As(err, &pgErr) { switch pgErr.Code { @@ -93,9 +92,16 @@ func (s *Service) RequestIncrement(ctx context.Context, user string) error { return fmt.Errorf("%w: %w", ErrInsertIncrementRequest, err) } - if existing > 0 { + slog.Info(ctx, "listing increment request") + + requests, err := s.store.ListIncrementRequests(ctx, tx) + if err != nil { + return fmt.Errorf("%w: %w", ErrListIncrementRequests, err) + } + + if len(requests) > 1 { slog.Info(ctx, "existing increment requests in finalize window, skip enqueuing finalize job", - slog.KV("requests.count", existing), + slog.KV("requests.count", len(requests)), slog.KV("finalize.window", s.finalizeWindow), ) } else { diff --git a/app/internal/service/counter/store/counter.sql.go b/app/internal/service/counter/store/counter.sql.go index e7606cf..c420bc5 100644 --- a/app/internal/service/counter/store/counter.sql.go +++ b/app/internal/service/counter/store/counter.sql.go @@ -11,6 +11,15 @@ import ( "github.com/jackc/pgx/v5/pgtype" ) +const deleteIncrementRequests = `-- name: DeleteIncrementRequests :exec +TRUNCATE TABLE increment_requests +` + +func (q *Queries) DeleteIncrementRequests(ctx context.Context, db DBTX) error { + _, err := db.Exec(ctx, deleteIncrementRequests) + return err +} + const getCounter = `-- name: GetCounter :one SELECT id, count, last_increment_by, last_increment_at, next_finalize_at FROM counter @@ -50,16 +59,9 @@ func (q *Queries) IncrementCounter(ctx context.Context, db DBTX, arg IncrementCo return err } -const insertIncrementRequest = `-- name: InsertIncrementRequest :one -WITH inserted AS ( - INSERT INTO increment_requests (requested_by, requested_at) - VALUES ($1, $2) -), existing AS ( - SELECT COUNT(*) AS requests FROM increment_requests -) -SELECT - existing.requests AS existing_requests -FROM existing +const insertIncrementRequest = `-- name: InsertIncrementRequest :exec +INSERT INTO increment_requests (requested_by, requested_at) +VALUES ($1, $2) ` type InsertIncrementRequestParams struct { @@ -67,11 +69,9 @@ type InsertIncrementRequestParams struct { RequestedAt pgtype.Timestamptz } -func (q *Queries) InsertIncrementRequest(ctx context.Context, db DBTX, arg InsertIncrementRequestParams) (int64, error) { - row := db.QueryRow(ctx, insertIncrementRequest, arg.RequestedBy, arg.RequestedAt) - var existing_requests int64 - err := row.Scan(&existing_requests) - return existing_requests, err +func (q *Queries) InsertIncrementRequest(ctx context.Context, db DBTX, arg InsertIncrementRequestParams) error { + _, err := db.Exec(ctx, insertIncrementRequest, arg.RequestedBy, arg.RequestedAt) + return err } const listIncrementRequests = `-- name: ListIncrementRequests :many @@ -114,15 +114,6 @@ func (q *Queries) ResetCounter(ctx context.Context, db DBTX) error { return err } -const truncateIncrementRequests = `-- name: TruncateIncrementRequests :exec -TRUNCATE TABLE increment_requests -` - -func (q *Queries) TruncateIncrementRequests(ctx context.Context, db DBTX) error { - _, err := db.Exec(ctx, truncateIncrementRequests) - return err -} - const updateCounterFinalizeTime = `-- name: UpdateCounterFinalizeTime :exec UPDATE counter SET diff --git a/app/internal/service/counter/store/querier.go b/app/internal/service/counter/store/querier.go index 562a746..8064032 100644 --- a/app/internal/service/counter/store/querier.go +++ b/app/internal/service/counter/store/querier.go @@ -11,12 +11,12 @@ import ( ) type Querier interface { + DeleteIncrementRequests(ctx context.Context, db DBTX) error GetCounter(ctx context.Context, db DBTX) (Counter, error) IncrementCounter(ctx context.Context, db DBTX, arg IncrementCounterParams) error - InsertIncrementRequest(ctx context.Context, db DBTX, arg InsertIncrementRequestParams) (int64, error) + InsertIncrementRequest(ctx context.Context, db DBTX, arg InsertIncrementRequestParams) error ListIncrementRequests(ctx context.Context, db DBTX) ([]IncrementRequest, error) ResetCounter(ctx context.Context, db DBTX) error - TruncateIncrementRequests(ctx context.Context, db DBTX) error UpdateCounterFinalizeTime(ctx context.Context, db DBTX, nextFinalizeAt pgtype.Timestamptz) error } diff --git a/app/internal/service/user/service.go b/app/internal/service/user/service.go new file mode 100644 index 0000000..57fc395 --- /dev/null +++ b/app/internal/service/user/service.go @@ -0,0 +1,19 @@ +package user + +import ( + "github.com/jackc/pgx/v5/pgxpool" + + counterstore "github.com/jace-ys/countup/internal/service/counter/store" +) + +type Service struct { + db *pgxpool.Pool + store counterstore.Querier +} + +func New(db *pgxpool.Pool, store counterstore.Querier) *Service { + return &Service{ + db: db, + store: store, + } +} diff --git a/app/internal/slog/stdslog.go b/app/internal/slog/stdslog.go index ef5e0b5..101105b 100644 --- a/app/internal/slog/stdslog.go +++ b/app/internal/slog/stdslog.go @@ -3,7 +3,6 @@ package slog import ( "context" "errors" - "fmt" "log/slog" "goa.design/clue/log" @@ -57,7 +56,8 @@ func (l *StdSlogHandler) WithAttrs(attrs []slog.Attr) slog.Handler { kvs := make([]log.Fielder, len(attrs)) for i, attr := range attrs { if l.group != "" { - kvs[i] = KV(fmt.Sprintf("%s.%s", l.group, attr.Key), attr.Value.Any()) + k := l.group + "." + attr.Key + kvs[i] = KV(k, attr.Value.Any()) } else { kvs[i] = KV(attr.Key, attr.Value.Any()) } diff --git a/app/internal/transport/client.go b/app/internal/transport/client.go new file mode 100644 index 0000000..3769e26 --- /dev/null +++ b/app/internal/transport/client.go @@ -0,0 +1,83 @@ +package transport + +import ( + "fmt" + "net/http" + + goahttp "goa.design/goa/v3/http" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" +) + +type Client[C any] struct { + client *C + close func() error +} + +func (c *Client[C]) Client() *C { + return c.client +} + +func (c *Client[C]) Close() error { + if c.close == nil { + return nil + } + return c.close() +} + +type GoaGRPCClientAdapter[C any] struct { + newFunc GoaGRPCClientNewFunc[C] +} + +type GoaGRPCClientNewFunc[C any] func(cc *grpc.ClientConn, opts ...grpc.CallOption) *C + +func GoaGRPCClient[C any](newFunc GoaGRPCClientNewFunc[C]) *GoaGRPCClientAdapter[C] { + return &GoaGRPCClientAdapter[C]{ + newFunc: newFunc, + } +} + +func (a *GoaGRPCClientAdapter[C]) Adapt(addr string) (*Client[C], error) { + opts := []grpc.DialOption{ + grpc.WithTransportCredentials(insecure.NewCredentials()), + } + + conn, err := grpc.NewClient(addr, opts...) + if err != nil { + return nil, fmt.Errorf("create gRPC client: %w", err) + } + + return &Client[C]{ + close: conn.Close, + client: a.newFunc(conn), + }, nil +} + +type GoaHTTPClientAdapter[C any] struct { + newFunc GoaHTTPClientNewFunc[C] +} + +type GoaHTTPClientNewFunc[C any] func( + scheme string, + host string, + doer goahttp.Doer, + enc func(*http.Request) goahttp.Encoder, + dec func(*http.Response) goahttp.Decoder, + restoreBody bool, +) *C + +func GoaHTTPClient[C any](newFunc GoaHTTPClientNewFunc[C]) *GoaHTTPClientAdapter[C] { + return &GoaHTTPClientAdapter[C]{ + newFunc: newFunc, + } +} + +func (a *GoaHTTPClientAdapter[C]) Adapt(scheme, addr string) (*Client[C], error) { + doer := http.DefaultClient + enc := goahttp.RequestEncoder + dec := goahttp.ResponseDecoder + + return &Client[C]{ + client: a.newFunc(scheme, addr, doer, enc, dec, false), + }, nil +} diff --git a/app/internal/transport/goa.go b/app/internal/transport/goa.go index 716d97c..ce1b42a 100644 --- a/app/internal/transport/goa.go +++ b/app/internal/transport/goa.go @@ -9,45 +9,45 @@ import ( "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/codes" "go.opentelemetry.io/otel/trace" - "goa.design/clue/log" "goa.design/goa/v3/grpc" goahttp "goa.design/goa/v3/http" + "goa.design/goa/v3/http/middleware" goa "goa.design/goa/v3/pkg" - "github.com/jace-ys/countup/internal/endpoints" + "github.com/jace-ys/countup/internal/endpoint" "github.com/jace-ys/countup/internal/slog" ) -type GoaGRPCAdapter[E endpoints.GoaEndpoints, S any] struct { +type GoaGRPCAdapter[E endpoint.GoaEndpoints, S any] struct { newFunc GoaGRPCNewFunc[E, S] } -type GoaGRPCNewFunc[E endpoints.GoaEndpoints, S any] func(e E, uh grpc.UnaryHandler) S +type GoaGRPCNewFunc[E endpoint.GoaEndpoints, S any] func(e E, uh grpc.UnaryHandler) *S -func GoaGRPC[E endpoints.GoaEndpoints, S any](newFunc GoaGRPCNewFunc[E, S]) *GoaGRPCAdapter[E, S] { +func GoaGRPC[E endpoint.GoaEndpoints, S any](newFunc GoaGRPCNewFunc[E, S]) *GoaGRPCAdapter[E, S] { return &GoaGRPCAdapter[E, S]{ newFunc: newFunc, } } -func (a *GoaGRPCAdapter[E, S]) Adapt(ctx context.Context, ep E) S { +func (a *GoaGRPCAdapter[E, S]) Adapt(ctx context.Context, ep E) *S { srv := a.newFunc(ep, nil) return srv } -type GoaHTTPServer interface { +type goaHTTPServer interface { MethodNames() []string Mount(mux goahttp.Muxer) Service() string Use(m func(http.Handler) http.Handler) } -type GoaHTTPAdapter[E endpoints.GoaEndpoints, S GoaHTTPServer] struct { +type GoaHTTPAdapter[E endpoint.GoaEndpoints, S goaHTTPServer] struct { newFunc GoaHTTPNewFunc[E, S] mountFunc GoaHTTPMountFunc[S] } -type GoaHTTPNewFunc[E endpoints.GoaEndpoints, S GoaHTTPServer] func( +type GoaHTTPNewFunc[E endpoint.GoaEndpoints, S goaHTTPServer] func( e E, mux goahttp.Muxer, decoder func(*http.Request) goahttp.Decoder, @@ -57,12 +57,12 @@ type GoaHTTPNewFunc[E endpoints.GoaEndpoints, S GoaHTTPServer] func( files http.FileSystem, ) S -type GoaHTTPMountFunc[S GoaHTTPServer] func( +type GoaHTTPMountFunc[S goaHTTPServer] func( mux goahttp.Muxer, srv S, ) -func GoaHTTP[E endpoints.GoaEndpoints, S GoaHTTPServer](newFunc GoaHTTPNewFunc[E, S], mountFunc GoaHTTPMountFunc[S]) *GoaHTTPAdapter[E, S] { +func GoaHTTP[E endpoint.GoaEndpoints, S goaHTTPServer](newFunc GoaHTTPNewFunc[E, S], mountFunc GoaHTTPMountFunc[S]) *GoaHTTPAdapter[E, S] { return &GoaHTTPAdapter[E, S]{ newFunc: newFunc, mountFunc: mountFunc, @@ -76,8 +76,8 @@ func (a *GoaHTTPAdapter[E, S]) Adapt(ctx context.Context, ep E, files fs.FS) goa eh := func(ctx context.Context, w http.ResponseWriter, err error) { slog.Error(ctx, "failed to encode response", err, - slog.KV(log.GoaMethodKey, ctx.Value(goa.MethodKey)), - slog.KV(log.GoaServiceKey, ctx.Value(goa.ServiceKey)), + slog.KV("http.method", ctx.Value(middleware.RequestMethodKey)), + slog.KV("http.path", ctx.Value(middleware.RequestPathKey)), ) gerr := goa.Fault("failed to encode response") diff --git a/app/internal/transport/middleware/recovery/recovery.go b/app/internal/transport/middleware/recovery/recovery.go index 14e3e8d..593f66c 100644 --- a/app/internal/transport/middleware/recovery/recovery.go +++ b/app/internal/transport/middleware/recovery/recovery.go @@ -3,7 +3,6 @@ package recovery import ( "context" "errors" - "fmt" "net/http" "goa.design/clue/log" @@ -38,7 +37,7 @@ func HTTP(logCtx context.Context) func(http.Handler) http.Handler { } ctx := log.WithContext(r.Context(), logCtx) - source := fmt.Sprintf("%s %s", r.Method, r.URL.Path) + source := r.Method + " " + r.URL.Path instrument.EmitRecoveredPanicTelemetry(ctx, rvr, source) if r.Header.Get("Connection") != "Upgrade" { diff --git a/app/internal/transport/middleware/telemetry/http.go b/app/internal/transport/middleware/telemetry/http.go index 5e47fcb..a3104a4 100644 --- a/app/internal/transport/middleware/telemetry/http.go +++ b/app/internal/transport/middleware/telemetry/http.go @@ -1,7 +1,6 @@ package telemetry import ( - "fmt" "net/http" "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" @@ -12,7 +11,7 @@ import ( func HTTP(attrs ...attribute.KeyValue) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - operation := fmt.Sprintf("%s %s", r.Method, r.URL.Path) + operation := r.Method + " " + r.URL.Path opts := []otelhttp.Option{ otelhttp.WithSpanOptions(trace.WithAttributes(attrs...)), diff --git a/app/internal/worker/instrumented.go b/app/internal/worker/instrumented.go index 541382f..523d964 100644 --- a/app/internal/worker/instrumented.go +++ b/app/internal/worker/instrumented.go @@ -153,7 +153,7 @@ func (m *instrumentedJobInsertMiddleware) emitEnqueuedTelemetry(ctx context.Cont ), ) - attrset := attribute.NewSet(attrs...) - m.metrics.jobsEnqueuedTotal.Add(ctx, 1, metric.WithAttributeSet(attrset)) - m.metrics.jobsAvailableCount.Add(ctx, 1, metric.WithAttributeSet(attrset)) + set := attribute.NewSet(attrs...) + m.metrics.jobsEnqueuedTotal.Add(ctx, 1, metric.WithAttributeSet(set)) + m.metrics.jobsAvailableCount.Add(ctx, 1, metric.WithAttributeSet(set)) } diff --git a/app/internal/worker/pool.go b/app/internal/worker/pool.go index eb50a23..8bdd3f9 100644 --- a/app/internal/worker/pool.go +++ b/app/internal/worker/pool.go @@ -27,7 +27,6 @@ type Pool struct { name string pool *river.Client[pgx.Tx] workers *river.Workers - metrics *metrics } diff --git a/app/schema/counter.sql b/app/schema/counter.sql index 5170922..be8a982 100644 --- a/app/schema/counter.sql +++ b/app/schema/counter.sql @@ -31,16 +31,9 @@ WHERE id = 1; SELECT * FROM increment_requests; --- name: InsertIncrementRequest :one -WITH inserted AS ( - INSERT INTO increment_requests (requested_by, requested_at) - VALUES ($1, $2) -), existing AS ( - SELECT COUNT(*) AS requests FROM increment_requests -) -SELECT - existing.requests AS existing_requests -FROM existing; +-- name: InsertIncrementRequest :exec +INSERT INTO increment_requests (requested_by, requested_at) +VALUES ($1, $2); --- name: TruncateIncrementRequests :exec +-- name: DeleteIncrementRequests :exec TRUNCATE TABLE increment_requests; \ No newline at end of file diff --git a/app/schema/migrations/20241003194615_rivermigrate002.go b/app/schema/migrations/20241003194615_rivermigrate002.go index 738685c..fbcd262 100644 --- a/app/schema/migrations/20241003194615_rivermigrate002.go +++ b/app/schema/migrations/20241003194615_rivermigrate002.go @@ -13,14 +13,14 @@ func init() { } func upRiverMigrate002(ctx context.Context, tx *sql.Tx) error { - _, err := rivermigrator.Migrate(ctx, rivermigrate.DirectionUp, &rivermigrate.MigrateOpts{ + _, err := riverMigrate.Migrate(ctx, rivermigrate.DirectionUp, &rivermigrate.MigrateOpts{ TargetVersion: 2, }) return err } func downRiverMigrate002(ctx context.Context, tx *sql.Tx) error { - _, err := rivermigrator.Migrate(ctx, rivermigrate.DirectionDown, &rivermigrate.MigrateOpts{ + _, err := riverMigrate.Migrate(ctx, rivermigrate.DirectionDown, &rivermigrate.MigrateOpts{ TargetVersion: -1, }) return err diff --git a/app/schema/migrations/20241003194729_rivermigrate003.go b/app/schema/migrations/20241003194729_rivermigrate003.go index ab5875d..dfd8491 100644 --- a/app/schema/migrations/20241003194729_rivermigrate003.go +++ b/app/schema/migrations/20241003194729_rivermigrate003.go @@ -13,14 +13,14 @@ func init() { } func upRiverMigrate003(ctx context.Context, tx *sql.Tx) error { - _, err := rivermigrator.Migrate(ctx, rivermigrate.DirectionUp, &rivermigrate.MigrateOpts{ + _, err := riverMigrate.Migrate(ctx, rivermigrate.DirectionUp, &rivermigrate.MigrateOpts{ TargetVersion: 3, }) return err } func downRiverMigrate003(ctx context.Context, tx *sql.Tx) error { - _, err := rivermigrator.Migrate(ctx, rivermigrate.DirectionDown, &rivermigrate.MigrateOpts{ + _, err := riverMigrate.Migrate(ctx, rivermigrate.DirectionDown, &rivermigrate.MigrateOpts{ TargetVersion: 2, }) return err diff --git a/app/schema/migrations/20241003195032_rivermigrate004.go b/app/schema/migrations/20241003195032_rivermigrate004.go index 3505b29..2df056e 100644 --- a/app/schema/migrations/20241003195032_rivermigrate004.go +++ b/app/schema/migrations/20241003195032_rivermigrate004.go @@ -13,14 +13,14 @@ func init() { } func upRiverMigrate004(ctx context.Context, tx *sql.Tx) error { - _, err := rivermigrator.Migrate(ctx, rivermigrate.DirectionUp, &rivermigrate.MigrateOpts{ + _, err := riverMigrate.Migrate(ctx, rivermigrate.DirectionUp, &rivermigrate.MigrateOpts{ TargetVersion: 4, }) return err } func downRiverMigrate004(ctx context.Context, tx *sql.Tx) error { - _, err := rivermigrator.Migrate(ctx, rivermigrate.DirectionDown, &rivermigrate.MigrateOpts{ + _, err := riverMigrate.Migrate(ctx, rivermigrate.DirectionDown, &rivermigrate.MigrateOpts{ TargetVersion: 3, }) return err diff --git a/app/schema/migrations/20241003195147_rivermigrate005.go b/app/schema/migrations/20241003195147_rivermigrate005.go index 485decc..4bb40a2 100644 --- a/app/schema/migrations/20241003195147_rivermigrate005.go +++ b/app/schema/migrations/20241003195147_rivermigrate005.go @@ -13,14 +13,14 @@ func init() { } func upRiverMigrate005(ctx context.Context, tx *sql.Tx) error { - _, err := rivermigrator.Migrate(ctx, rivermigrate.DirectionUp, &rivermigrate.MigrateOpts{ + _, err := riverMigrate.Migrate(ctx, rivermigrate.DirectionUp, &rivermigrate.MigrateOpts{ TargetVersion: 5, }) return err } func downRiverMigrate005(ctx context.Context, tx *sql.Tx) error { - _, err := rivermigrator.Migrate(ctx, rivermigrate.DirectionDown, &rivermigrate.MigrateOpts{ + _, err := riverMigrate.Migrate(ctx, rivermigrate.DirectionDown, &rivermigrate.MigrateOpts{ TargetVersion: 4, }) return err diff --git a/app/schema/migrations/20241003195218_rivermigrate006.go b/app/schema/migrations/20241003195218_rivermigrate006.go index 7302f20..3b5eebf 100644 --- a/app/schema/migrations/20241003195218_rivermigrate006.go +++ b/app/schema/migrations/20241003195218_rivermigrate006.go @@ -13,14 +13,14 @@ func init() { } func upRiverMigrate006(ctx context.Context, tx *sql.Tx) error { - _, err := rivermigrator.Migrate(ctx, rivermigrate.DirectionUp, &rivermigrate.MigrateOpts{ + _, err := riverMigrate.Migrate(ctx, rivermigrate.DirectionUp, &rivermigrate.MigrateOpts{ TargetVersion: 6, }) return err } func downRiverMigrate006(ctx context.Context, tx *sql.Tx) error { - _, err := rivermigrator.Migrate(ctx, rivermigrate.DirectionDown, &rivermigrate.MigrateOpts{ + _, err := riverMigrate.Migrate(ctx, rivermigrate.DirectionDown, &rivermigrate.MigrateOpts{ TargetVersion: 5, }) return err diff --git a/app/schema/migrations/rivermigrate.go b/app/schema/migrations/rivermigrate.go index 2da4562..4efbc87 100644 --- a/app/schema/migrations/rivermigrate.go +++ b/app/schema/migrations/rivermigrate.go @@ -9,7 +9,7 @@ import ( "github.com/riverqueue/river/rivermigrate" ) -var rivermigrator *rivermigrate.Migrator[pgx.Tx] +var riverMigrate *rivermigrate.Migrator[pgx.Tx] func WithRiverMigrate(db *pgxpool.Pool) error { migrator, err := rivermigrate.New(riverpgxv5.New(db), &rivermigrate.Config{}) @@ -17,6 +17,6 @@ func WithRiverMigrate(db *pgxpool.Pool) error { return fmt.Errorf("init river migrator: %w", err) } - rivermigrator = migrator + riverMigrate = migrator return nil }