diff --git a/bin/generate_rest.sh b/bin/generate_rest.sh index 417d6663..b865aa24 100755 --- a/bin/generate_rest.sh +++ b/bin/generate_rest.sh @@ -25,4 +25,4 @@ echo "...generating zrok client" swagger generate client -P rest_model_zrok.Principal -f "$zrokSpec" -c rest_client_zrok -t "$zrokDir" -m "rest_model_zrok" echo "...generating js client" -openapi -s specs/zrok.yml -o ui/api -l js \ No newline at end of file +openapi -s specs/zrok.yml -o ui/src/api -l js diff --git a/controller/account.go b/controller/account.go index 8ed76a0a..fb6a22aa 100644 --- a/controller/account.go +++ b/controller/account.go @@ -14,7 +14,7 @@ func createAccountHandler(params identity.CreateAccountParams) middleware.Respon logrus.Infof("received account request for username '%v'", params.Body.Username) if params.Body == nil || params.Body.Username == "" || params.Body.Password == "" { logrus.Errorf("missing username or password") - return identity.NewCreateAccountBadRequest().WithPayload(rest_model_zrok.ErrorMessage("missing username or password")) + return identity.NewCreateAccountBadRequest().WithPayload("missing username or password") } token, err := generateApiToken() diff --git a/controller/controller.go b/controller/controller.go index cbbca0ee..b6246220 100644 --- a/controller/controller.go +++ b/controller/controller.go @@ -22,9 +22,10 @@ func Run(cfg *Config) error { api := operations.NewZrokAPI(swaggerSpec) api.KeyAuth = ZrokAuthenticate - api.MetadataVersionHandler = metadata.VersionHandlerFunc(versionHandler) api.IdentityCreateAccountHandler = identity.CreateAccountHandlerFunc(createAccountHandler) api.IdentityEnableHandler = identity.EnableHandlerFunc(enableHandler) + api.IdentityLoginHandler = identity.LoginHandlerFunc(loginHandler) + api.MetadataVersionHandler = metadata.VersionHandlerFunc(versionHandler) api.TunnelTunnelHandler = tunnel.TunnelHandlerFunc(tunnelHandler) api.TunnelUntunnelHandler = tunnel.UntunnelHandlerFunc(untunnelHandler) diff --git a/controller/login.go b/controller/login.go new file mode 100644 index 00000000..f5fa9e5f --- /dev/null +++ b/controller/login.go @@ -0,0 +1,35 @@ +package controller + +import ( + "github.com/go-openapi/runtime/middleware" + "github.com/openziti-test-kitchen/zrok/rest_model_zrok" + "github.com/openziti-test-kitchen/zrok/rest_server_zrok/operations/identity" + "github.com/sirupsen/logrus" +) + +func loginHandler(params identity.LoginParams) middleware.Responder { + if params.Body == nil || params.Body.Email == "" || params.Body.Password == "" { + logrus.Errorf("missing email or password") + return identity.NewLoginUnauthorized() + } + + logrus.Infof("received login request for email '%v'", params.Body.Email) + + tx, err := str.Begin() + if err != nil { + logrus.Errorf("error starting transaction: %v", err) + return identity.NewLoginUnauthorized() + } + defer func() { _ = tx.Rollback() }() + a, err := str.FindAccountWithUsername(params.Body.Email, tx) + if err != nil { + logrus.Errorf("error finding account '%v': %v", params.Body.Email, err) + return identity.NewLoginUnauthorized() + } + if a.Password != hashPassword(params.Body.Password) { + logrus.Errorf("password mismatch for account '%v'", params.Body.Email) + return identity.NewLoginUnauthorized() + } + + return identity.NewLoginOK().WithPayload(rest_model_zrok.LoginResponse(a.Token)) +} diff --git a/controller/store/account.go b/controller/store/account.go index ebb76085..5c545b22 100644 --- a/controller/store/account.go +++ b/controller/store/account.go @@ -36,6 +36,14 @@ func (self *Store) GetAccount(id int, tx *sqlx.Tx) (*Account, error) { return a, nil } +func (self *Store) FindAccountWithUsername(username string, tx *sqlx.Tx) (*Account, error) { + a := &Account{} + if err := tx.QueryRowx("select * from accounts where username = ?", username).StructScan(a); err != nil { + return nil, errors.Wrap(err, "error selecting account by username") + } + return a, nil +} + func (self *Store) FindAccountWithToken(token string, tx *sqlx.Tx) (*Account, error) { a := &Account{} if err := tx.QueryRowx("select * from accounts where token = ?", token).StructScan(a); err != nil { diff --git a/rest_client_zrok/identity/identity_client.go b/rest_client_zrok/identity/identity_client.go index 523e5f5f..941fbeeb 100644 --- a/rest_client_zrok/identity/identity_client.go +++ b/rest_client_zrok/identity/identity_client.go @@ -34,6 +34,8 @@ type ClientService interface { Enable(params *EnableParams, authInfo runtime.ClientAuthInfoWriter, opts ...ClientOption) (*EnableCreated, error) + Login(params *LoginParams, opts ...ClientOption) (*LoginOK, error) + SetTransport(transport runtime.ClientTransport) } @@ -114,6 +116,44 @@ func (a *Client) Enable(params *EnableParams, authInfo runtime.ClientAuthInfoWri panic(msg) } +/* + Login login API +*/ +func (a *Client) Login(params *LoginParams, opts ...ClientOption) (*LoginOK, error) { + // TODO: Validate the params before sending + if params == nil { + params = NewLoginParams() + } + op := &runtime.ClientOperation{ + ID: "login", + Method: "POST", + PathPattern: "/login", + ProducesMediaTypes: []string{"application/zrok.v1+json"}, + ConsumesMediaTypes: []string{"application/zrok.v1+json"}, + Schemes: []string{"http"}, + Params: params, + Reader: &LoginReader{formats: a.formats}, + Context: params.Context, + Client: params.HTTPClient, + } + for _, opt := range opts { + opt(op) + } + + result, err := a.transport.Submit(op) + if err != nil { + return nil, err + } + success, ok := result.(*LoginOK) + if ok { + return success, nil + } + // unexpected success response + // safeguard: normally, absent a default response, unknown success responses return an error above: so this is a codegen issue + msg := fmt.Sprintf("unexpected success response for login: API contract not enforced by server. Client expected to get an error, but got: %T", result) + panic(msg) +} + // SetTransport changes the transport on the client func (a *Client) SetTransport(transport runtime.ClientTransport) { a.transport = transport diff --git a/rest_client_zrok/identity/login_parameters.go b/rest_client_zrok/identity/login_parameters.go new file mode 100644 index 00000000..b4d3f9e7 --- /dev/null +++ b/rest_client_zrok/identity/login_parameters.go @@ -0,0 +1,148 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package identity + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "context" + "net/http" + "time" + + "github.com/go-openapi/errors" + "github.com/go-openapi/runtime" + cr "github.com/go-openapi/runtime/client" + "github.com/go-openapi/strfmt" + + "github.com/openziti-test-kitchen/zrok/rest_model_zrok" +) + +// NewLoginParams creates a new LoginParams object, +// with the default timeout for this client. +// +// Default values are not hydrated, since defaults are normally applied by the API server side. +// +// To enforce default values in parameter, use SetDefaults or WithDefaults. +func NewLoginParams() *LoginParams { + return &LoginParams{ + timeout: cr.DefaultTimeout, + } +} + +// NewLoginParamsWithTimeout creates a new LoginParams object +// with the ability to set a timeout on a request. +func NewLoginParamsWithTimeout(timeout time.Duration) *LoginParams { + return &LoginParams{ + timeout: timeout, + } +} + +// NewLoginParamsWithContext creates a new LoginParams object +// with the ability to set a context for a request. +func NewLoginParamsWithContext(ctx context.Context) *LoginParams { + return &LoginParams{ + Context: ctx, + } +} + +// NewLoginParamsWithHTTPClient creates a new LoginParams object +// with the ability to set a custom HTTPClient for a request. +func NewLoginParamsWithHTTPClient(client *http.Client) *LoginParams { + return &LoginParams{ + HTTPClient: client, + } +} + +/* LoginParams contains all the parameters to send to the API endpoint + for the login operation. + + Typically these are written to a http.Request. +*/ +type LoginParams struct { + + // Body. + Body *rest_model_zrok.LoginRequest + + timeout time.Duration + Context context.Context + HTTPClient *http.Client +} + +// WithDefaults hydrates default values in the login params (not the query body). +// +// All values with no default are reset to their zero value. +func (o *LoginParams) WithDefaults() *LoginParams { + o.SetDefaults() + return o +} + +// SetDefaults hydrates default values in the login params (not the query body). +// +// All values with no default are reset to their zero value. +func (o *LoginParams) SetDefaults() { + // no default values defined for this parameter +} + +// WithTimeout adds the timeout to the login params +func (o *LoginParams) WithTimeout(timeout time.Duration) *LoginParams { + o.SetTimeout(timeout) + return o +} + +// SetTimeout adds the timeout to the login params +func (o *LoginParams) SetTimeout(timeout time.Duration) { + o.timeout = timeout +} + +// WithContext adds the context to the login params +func (o *LoginParams) WithContext(ctx context.Context) *LoginParams { + o.SetContext(ctx) + return o +} + +// SetContext adds the context to the login params +func (o *LoginParams) SetContext(ctx context.Context) { + o.Context = ctx +} + +// WithHTTPClient adds the HTTPClient to the login params +func (o *LoginParams) WithHTTPClient(client *http.Client) *LoginParams { + o.SetHTTPClient(client) + return o +} + +// SetHTTPClient adds the HTTPClient to the login params +func (o *LoginParams) SetHTTPClient(client *http.Client) { + o.HTTPClient = client +} + +// WithBody adds the body to the login params +func (o *LoginParams) WithBody(body *rest_model_zrok.LoginRequest) *LoginParams { + o.SetBody(body) + return o +} + +// SetBody adds the body to the login params +func (o *LoginParams) SetBody(body *rest_model_zrok.LoginRequest) { + o.Body = body +} + +// WriteToRequest writes these params to a swagger request +func (o *LoginParams) WriteToRequest(r runtime.ClientRequest, reg strfmt.Registry) error { + + if err := r.SetTimeout(o.timeout); err != nil { + return err + } + var res []error + if o.Body != nil { + if err := r.SetBodyParam(o.Body); err != nil { + return err + } + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} diff --git a/rest_client_zrok/identity/login_responses.go b/rest_client_zrok/identity/login_responses.go new file mode 100644 index 00000000..3c6f4390 --- /dev/null +++ b/rest_client_zrok/identity/login_responses.go @@ -0,0 +1,92 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package identity + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "fmt" + "io" + + "github.com/go-openapi/runtime" + "github.com/go-openapi/strfmt" + + "github.com/openziti-test-kitchen/zrok/rest_model_zrok" +) + +// LoginReader is a Reader for the Login structure. +type LoginReader struct { + formats strfmt.Registry +} + +// ReadResponse reads a server response into the received o. +func (o *LoginReader) ReadResponse(response runtime.ClientResponse, consumer runtime.Consumer) (interface{}, error) { + switch response.Code() { + case 200: + result := NewLoginOK() + if err := result.readResponse(response, consumer, o.formats); err != nil { + return nil, err + } + return result, nil + case 401: + result := NewLoginUnauthorized() + if err := result.readResponse(response, consumer, o.formats); err != nil { + return nil, err + } + return nil, result + default: + return nil, runtime.NewAPIError("response status code does not match any response statuses defined for this endpoint in the swagger spec", response, response.Code()) + } +} + +// NewLoginOK creates a LoginOK with default headers values +func NewLoginOK() *LoginOK { + return &LoginOK{} +} + +/* LoginOK describes a response with status code 200, with default header values. + +login successful +*/ +type LoginOK struct { + Payload rest_model_zrok.LoginResponse +} + +func (o *LoginOK) Error() string { + return fmt.Sprintf("[POST /login][%d] loginOK %+v", 200, o.Payload) +} +func (o *LoginOK) GetPayload() rest_model_zrok.LoginResponse { + return o.Payload +} + +func (o *LoginOK) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error { + + // response payload + if err := consumer.Consume(response.Body(), &o.Payload); err != nil && err != io.EOF { + return err + } + + return nil +} + +// NewLoginUnauthorized creates a LoginUnauthorized with default headers values +func NewLoginUnauthorized() *LoginUnauthorized { + return &LoginUnauthorized{} +} + +/* LoginUnauthorized describes a response with status code 401, with default header values. + +invalid login +*/ +type LoginUnauthorized struct { +} + +func (o *LoginUnauthorized) Error() string { + return fmt.Sprintf("[POST /login][%d] loginUnauthorized ", 401) +} + +func (o *LoginUnauthorized) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error { + + return nil +} diff --git a/rest_model_zrok/login_request.go b/rest_model_zrok/login_request.go new file mode 100644 index 00000000..e82c6bd8 --- /dev/null +++ b/rest_model_zrok/login_request.go @@ -0,0 +1,53 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package rest_model_zrok + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "context" + + "github.com/go-openapi/strfmt" + "github.com/go-openapi/swag" +) + +// LoginRequest login request +// +// swagger:model loginRequest +type LoginRequest struct { + + // email + Email string `json:"email,omitempty"` + + // password + Password string `json:"password,omitempty"` +} + +// Validate validates this login request +func (m *LoginRequest) Validate(formats strfmt.Registry) error { + return nil +} + +// ContextValidate validates this login request based on context it is used +func (m *LoginRequest) ContextValidate(ctx context.Context, formats strfmt.Registry) error { + return nil +} + +// MarshalBinary interface implementation +func (m *LoginRequest) MarshalBinary() ([]byte, error) { + if m == nil { + return nil, nil + } + return swag.WriteJSON(m) +} + +// UnmarshalBinary interface implementation +func (m *LoginRequest) UnmarshalBinary(b []byte) error { + var res LoginRequest + if err := swag.ReadJSON(b, &res); err != nil { + return err + } + *m = res + return nil +} diff --git a/rest_model_zrok/login_response.go b/rest_model_zrok/login_response.go new file mode 100644 index 00000000..15f1dcd3 --- /dev/null +++ b/rest_model_zrok/login_response.go @@ -0,0 +1,27 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package rest_model_zrok + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "context" + + "github.com/go-openapi/strfmt" +) + +// LoginResponse login response +// +// swagger:model loginResponse +type LoginResponse string + +// Validate validates this login response +func (m LoginResponse) Validate(formats strfmt.Registry) error { + return nil +} + +// ContextValidate validates this login response based on context it is used +func (m LoginResponse) ContextValidate(ctx context.Context, formats strfmt.Registry) error { + return nil +} diff --git a/rest_server_zrok/embedded_spec.go b/rest_server_zrok/embedded_spec.go index 2f334bfb..e6ba34c4 100644 --- a/rest_server_zrok/embedded_spec.go +++ b/rest_server_zrok/embedded_spec.go @@ -105,6 +105,34 @@ func init() { } } }, + "/login": { + "post": { + "tags": [ + "identity" + ], + "operationId": "login", + "parameters": [ + { + "name": "body", + "in": "body", + "schema": { + "$ref": "#/definitions/loginRequest" + } + } + ], + "responses": { + "200": { + "description": "login successful", + "schema": { + "$ref": "#/definitions/loginResponse" + } + }, + "401": { + "description": "invalid login" + } + } + } + }, "/tunnel": { "post": { "security": [ @@ -237,6 +265,20 @@ func init() { "errorMessage": { "type": "string" }, + "loginRequest": { + "type": "object", + "properties": { + "email": { + "type": "string" + }, + "password": { + "type": "string" + } + } + }, + "loginResponse": { + "type": "string" + }, "principal": { "type": "object", "properties": { @@ -378,6 +420,34 @@ func init() { } } }, + "/login": { + "post": { + "tags": [ + "identity" + ], + "operationId": "login", + "parameters": [ + { + "name": "body", + "in": "body", + "schema": { + "$ref": "#/definitions/loginRequest" + } + } + ], + "responses": { + "200": { + "description": "login successful", + "schema": { + "$ref": "#/definitions/loginResponse" + } + }, + "401": { + "description": "invalid login" + } + } + } + }, "/tunnel": { "post": { "security": [ @@ -510,6 +580,20 @@ func init() { "errorMessage": { "type": "string" }, + "loginRequest": { + "type": "object", + "properties": { + "email": { + "type": "string" + }, + "password": { + "type": "string" + } + } + }, + "loginResponse": { + "type": "string" + }, "principal": { "type": "object", "properties": { diff --git a/rest_server_zrok/operations/identity/login.go b/rest_server_zrok/operations/identity/login.go new file mode 100644 index 00000000..cfab30bb --- /dev/null +++ b/rest_server_zrok/operations/identity/login.go @@ -0,0 +1,56 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package identity + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the generate command + +import ( + "net/http" + + "github.com/go-openapi/runtime/middleware" +) + +// LoginHandlerFunc turns a function with the right signature into a login handler +type LoginHandlerFunc func(LoginParams) middleware.Responder + +// Handle executing the request and returning a response +func (fn LoginHandlerFunc) Handle(params LoginParams) middleware.Responder { + return fn(params) +} + +// LoginHandler interface for that can handle valid login params +type LoginHandler interface { + Handle(LoginParams) middleware.Responder +} + +// NewLogin creates a new http.Handler for the login operation +func NewLogin(ctx *middleware.Context, handler LoginHandler) *Login { + return &Login{Context: ctx, Handler: handler} +} + +/* Login swagger:route POST /login identity login + +Login login API + +*/ +type Login struct { + Context *middleware.Context + Handler LoginHandler +} + +func (o *Login) ServeHTTP(rw http.ResponseWriter, r *http.Request) { + route, rCtx, _ := o.Context.RouteInfo(r) + if rCtx != nil { + *r = *rCtx + } + var Params = NewLoginParams() + if err := o.Context.BindValidRequest(r, route, &Params); err != nil { // bind params + o.Context.Respond(rw, r, route.Produces, route, err) + return + } + + res := o.Handler.Handle(Params) // actually handle the request + o.Context.Respond(rw, r, route.Produces, route, res) + +} diff --git a/rest_server_zrok/operations/identity/login_parameters.go b/rest_server_zrok/operations/identity/login_parameters.go new file mode 100644 index 00000000..1802bd96 --- /dev/null +++ b/rest_server_zrok/operations/identity/login_parameters.go @@ -0,0 +1,77 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package identity + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "context" + "net/http" + + "github.com/go-openapi/errors" + "github.com/go-openapi/runtime" + "github.com/go-openapi/runtime/middleware" + "github.com/go-openapi/validate" + + "github.com/openziti-test-kitchen/zrok/rest_model_zrok" +) + +// NewLoginParams creates a new LoginParams object +// +// There are no default values defined in the spec. +func NewLoginParams() LoginParams { + + return LoginParams{} +} + +// LoginParams contains all the bound params for the login operation +// typically these are obtained from a http.Request +// +// swagger:parameters login +type LoginParams struct { + + // HTTP Request Object + HTTPRequest *http.Request `json:"-"` + + /* + In: body + */ + Body *rest_model_zrok.LoginRequest +} + +// BindRequest both binds and validates a request, it assumes that complex things implement a Validatable(strfmt.Registry) error interface +// for simple values it will use straight method calls. +// +// To ensure default values, the struct must have been initialized with NewLoginParams() beforehand. +func (o *LoginParams) BindRequest(r *http.Request, route *middleware.MatchedRoute) error { + var res []error + + o.HTTPRequest = r + + if runtime.HasBody(r) { + defer r.Body.Close() + var body rest_model_zrok.LoginRequest + if err := route.Consumer.Consume(r.Body, &body); err != nil { + res = append(res, errors.NewParseError("body", "body", "", err)) + } else { + // validate body object + if err := body.Validate(route.Formats); err != nil { + res = append(res, err) + } + + ctx := validate.WithOperationRequest(context.Background()) + if err := body.ContextValidate(ctx, route.Formats); err != nil { + res = append(res, err) + } + + if len(res) == 0 { + o.Body = &body + } + } + } + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} diff --git a/rest_server_zrok/operations/identity/login_responses.go b/rest_server_zrok/operations/identity/login_responses.go new file mode 100644 index 00000000..1e7b1a7e --- /dev/null +++ b/rest_server_zrok/operations/identity/login_responses.go @@ -0,0 +1,80 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package identity + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "net/http" + + "github.com/go-openapi/runtime" + + "github.com/openziti-test-kitchen/zrok/rest_model_zrok" +) + +// LoginOKCode is the HTTP code returned for type LoginOK +const LoginOKCode int = 200 + +/*LoginOK login successful + +swagger:response loginOK +*/ +type LoginOK struct { + + /* + In: Body + */ + Payload rest_model_zrok.LoginResponse `json:"body,omitempty"` +} + +// NewLoginOK creates LoginOK with default headers values +func NewLoginOK() *LoginOK { + + return &LoginOK{} +} + +// WithPayload adds the payload to the login o k response +func (o *LoginOK) WithPayload(payload rest_model_zrok.LoginResponse) *LoginOK { + o.Payload = payload + return o +} + +// SetPayload sets the payload to the login o k response +func (o *LoginOK) SetPayload(payload rest_model_zrok.LoginResponse) { + o.Payload = payload +} + +// WriteResponse to the client +func (o *LoginOK) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) { + + rw.WriteHeader(200) + payload := o.Payload + if err := producer.Produce(rw, payload); err != nil { + panic(err) // let the recovery middleware deal with this + } +} + +// LoginUnauthorizedCode is the HTTP code returned for type LoginUnauthorized +const LoginUnauthorizedCode int = 401 + +/*LoginUnauthorized invalid login + +swagger:response loginUnauthorized +*/ +type LoginUnauthorized struct { +} + +// NewLoginUnauthorized creates LoginUnauthorized with default headers values +func NewLoginUnauthorized() *LoginUnauthorized { + + return &LoginUnauthorized{} +} + +// WriteResponse to the client +func (o *LoginUnauthorized) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) { + + rw.Header().Del(runtime.HeaderContentType) //Remove Content-Type on empty responses + + rw.WriteHeader(401) +} diff --git a/rest_server_zrok/operations/identity/login_urlbuilder.go b/rest_server_zrok/operations/identity/login_urlbuilder.go new file mode 100644 index 00000000..4b893bdf --- /dev/null +++ b/rest_server_zrok/operations/identity/login_urlbuilder.go @@ -0,0 +1,87 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package identity + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the generate command + +import ( + "errors" + "net/url" + golangswaggerpaths "path" +) + +// LoginURL generates an URL for the login operation +type LoginURL struct { + _basePath string +} + +// WithBasePath sets the base path for this url builder, only required when it's different from the +// base path specified in the swagger spec. +// When the value of the base path is an empty string +func (o *LoginURL) WithBasePath(bp string) *LoginURL { + o.SetBasePath(bp) + return o +} + +// SetBasePath sets the base path for this url builder, only required when it's different from the +// base path specified in the swagger spec. +// When the value of the base path is an empty string +func (o *LoginURL) SetBasePath(bp string) { + o._basePath = bp +} + +// Build a url path and query string +func (o *LoginURL) Build() (*url.URL, error) { + var _result url.URL + + var _path = "/login" + + _basePath := o._basePath + if _basePath == "" { + _basePath = "/api/v1" + } + _result.Path = golangswaggerpaths.Join(_basePath, _path) + + return &_result, nil +} + +// Must is a helper function to panic when the url builder returns an error +func (o *LoginURL) Must(u *url.URL, err error) *url.URL { + if err != nil { + panic(err) + } + if u == nil { + panic("url can't be nil") + } + return u +} + +// String returns the string representation of the path with query string +func (o *LoginURL) String() string { + return o.Must(o.Build()).String() +} + +// BuildFull builds a full url with scheme, host, path and query string +func (o *LoginURL) BuildFull(scheme, host string) (*url.URL, error) { + if scheme == "" { + return nil, errors.New("scheme is required for a full url on LoginURL") + } + if host == "" { + return nil, errors.New("host is required for a full url on LoginURL") + } + + base, err := o.Build() + if err != nil { + return nil, err + } + + base.Scheme = scheme + base.Host = host + return base, nil +} + +// StringFull returns the string representation of a complete url +func (o *LoginURL) StringFull(scheme, host string) string { + return o.Must(o.BuildFull(scheme, host)).String() +} diff --git a/rest_server_zrok/operations/zrok_api.go b/rest_server_zrok/operations/zrok_api.go index 99fdc953..4a676592 100644 --- a/rest_server_zrok/operations/zrok_api.go +++ b/rest_server_zrok/operations/zrok_api.go @@ -53,6 +53,9 @@ func NewZrokAPI(spec *loads.Document) *ZrokAPI { IdentityEnableHandler: identity.EnableHandlerFunc(func(params identity.EnableParams, principal *rest_model_zrok.Principal) middleware.Responder { return middleware.NotImplemented("operation identity.Enable has not yet been implemented") }), + IdentityLoginHandler: identity.LoginHandlerFunc(func(params identity.LoginParams) middleware.Responder { + return middleware.NotImplemented("operation identity.Login has not yet been implemented") + }), TunnelTunnelHandler: tunnel.TunnelHandlerFunc(func(params tunnel.TunnelParams, principal *rest_model_zrok.Principal) middleware.Responder { return middleware.NotImplemented("operation tunnel.Tunnel has not yet been implemented") }), @@ -116,6 +119,8 @@ type ZrokAPI struct { IdentityCreateAccountHandler identity.CreateAccountHandler // IdentityEnableHandler sets the operation handler for the enable operation IdentityEnableHandler identity.EnableHandler + // IdentityLoginHandler sets the operation handler for the login operation + IdentityLoginHandler identity.LoginHandler // TunnelTunnelHandler sets the operation handler for the tunnel operation TunnelTunnelHandler tunnel.TunnelHandler // TunnelUntunnelHandler sets the operation handler for the untunnel operation @@ -209,6 +214,9 @@ func (o *ZrokAPI) Validate() error { if o.IdentityEnableHandler == nil { unregistered = append(unregistered, "identity.EnableHandler") } + if o.IdentityLoginHandler == nil { + unregistered = append(unregistered, "identity.LoginHandler") + } if o.TunnelTunnelHandler == nil { unregistered = append(unregistered, "tunnel.TunnelHandler") } @@ -328,6 +336,10 @@ func (o *ZrokAPI) initHandlerCache() { if o.handlers["POST"] == nil { o.handlers["POST"] = make(map[string]http.Handler) } + o.handlers["POST"]["/login"] = identity.NewLogin(o.context, o.IdentityLoginHandler) + if o.handlers["POST"] == nil { + o.handlers["POST"] = make(map[string]http.Handler) + } o.handlers["POST"]["/tunnel"] = tunnel.NewTunnel(o.context, o.TunnelTunnelHandler) if o.handlers["DELETE"] == nil { o.handlers["DELETE"] = make(map[string]http.Handler) diff --git a/specs/zrok.yml b/specs/zrok.yml index 807ad57c..989fa82d 100644 --- a/specs/zrok.yml +++ b/specs/zrok.yml @@ -55,6 +55,23 @@ paths: description: internal server error schema: $ref: "#/definitions/errorMessage" + /login: + post: + tags: + - identity + operationId: login + parameters: + - name: body + in: body + schema: + $ref: "#/definitions/loginRequest" + responses: + 200: + description: login successful + schema: + $ref: "#/definitions/loginResponse" + 401: + description: invalid login /tunnel: post: tags: @@ -136,6 +153,15 @@ definitions: type: string errorMessage: type: string + loginRequest: + type: object + properties: + email: + type: string + password: + type: string + loginResponse: + type: string principal: type: object properties: diff --git a/ui/api/gateway/index.js b/ui/api/gateway/index.js deleted file mode 100644 index 881798d7..00000000 --- a/ui/api/gateway/index.js +++ /dev/null @@ -1,281 +0,0 @@ -// Auto-generated, edits will be overwritten -import spec from './spec' - -export class ServiceError extends Error {} - -let options = {} - -export function init(serviceOptions) { - options = serviceOptions -} - -export function request(op, parameters, attempt) { - if (!attempt) attempt = 1; - return acquireRights(op, spec, options) - .then(rights => { - parameters = parameters || {} - const baseUrl = getBaseUrl(spec) - let reqInfo = { parameters, baseUrl } - if (options.processRequest) { - reqInfo = options.processRequest(op, reqInfo) - } - const req = buildRequest(op, reqInfo.baseUrl, reqInfo.parameters, rights) - return makeFetchRequest(req) - .then(res => processResponse(req, res, attempt, options), e => processError(req, e)) - .then(outcome => outcome.retry ? request(op, parameters, attempt + 1) : outcome.res) - }) -} - -function acquireRights(op, spec, options) { - if (op.security && options.getAuthorization) { - return op.security.reduce((promise, security) => { - return promise.then(rights => { - const securityDefinition = spec.securityDefinitions[security.id] - return options.getAuthorization(security, securityDefinition, op) - .then(auth => { - rights[security.id] = auth - return rights - }) - }) - }, Promise.resolve({})) - } - return Promise.resolve({}) -} - -function makeFetchRequest(req) { - let fetchOptions = { - compress: true, - method: (req.method || 'get').toUpperCase(), - headers: req.headers, - body: req.body ? JSON.stringify(req.body) : undefined - } - - if (options.fetchOptions) { - const opts = options.fetchOptions - const headers = opts.headers - ? Object.assign(fetchOptions.headers, opts.headers) - : fetchOptions.headers - - fetchOptions = Object.assign({}, fetchOptions, opts) - fetchOptions.headers = headers - } - - let promise = fetch(req.url, fetchOptions) - return promise -} - -function buildRequest(op, baseUrl, parameters, rights) { - let paramGroups = groupParams(op, parameters) - paramGroups = applyAuthorization(paramGroups, rights, spec) - const url = buildUrl(op, baseUrl, paramGroups, spec) - const headers = buildHeaders(op, paramGroups) - const body = buildBody(parameters.body) - return { - method: op.method, - url, - headers, - body - } -} - -function groupParams(op, parameters) { - const groups = ['header', 'path', 'query', 'formData'].reduce((groups, name) => { - groups[name] = formatParamsGroup(groups[name]) - return groups - }, parameters) - if (!groups.header) groups.header = {} - return groups -} - -function formatParamsGroup(groups) { - return Object.keys(groups || {}).reduce((g, name) => { - const param = groups[name] - if (param !== undefined) { - g[name] = formatParam(param) - } - return g - }, {}) -} - -function formatParam(param) { - if (param === undefined || param === null) return '' - else if (param instanceof Date) return param.toJSON() - else if (Array.isArray(param)) return param - else return param.toString() -} - -function buildUrl(op, baseUrl, parameters, spec) { - let url = `${baseUrl}${op.path}` - if (parameters.path) { - url = Object.keys(parameters.path) - .reduce((url, name) => url.replace(`{${name}}`, parameters.path[name]), url) - } - const query = createQueryString(parameters.query) - return url + query -} - -function getBaseUrl(spec) { - return options.url || `${spec.schemes[0] || 'https'}://${spec.host}${spec.basePath}` -} - -function createQueryParam(name, value) { - const v = formatParam(value) - if (v && typeof v === 'string') return `${name}=${encodeURIComponent(v)}` - return name; -} - -function createQueryString(query) { - const names = Object.keys(query || {}) - if (!names.length) return '' - const params = names.map(name => ({name, value: query[name]})) - .reduce((acc, value) => { - if (Array.isArray(value.value)) { - return acc.concat(value.value) - } else { - acc.push(createQueryParam(value.name, value.value)) - return acc - } - }, []) - return '?' + params.sort().join('&') -} - -function buildHeaders(op, parameters) { - const headers = {} - - let accepts - if (op.accepts && op.accepts.length) accepts = op.accepts - else if (spec.accepts && spec.accepts.length) accepts = spec.accepts - else accepts = [ 'application/json' ] - - headers.Accept = accepts.join(', ') - - let contentType - if (op.contentTypes && op.contentTypes[0]) contentType = op.contentTypes[0] - else if (spec.contentTypes && spec.contentTypes[0]) contentType = spec.contentTypes[0] - if (contentType) headers['Content-Type'] = contentType - - return Object.assign(headers, parameters.header) -} - -function buildBody(bodyParams) { - if (bodyParams) { - if (bodyParams.body) return bodyParams.body - const key = Object.keys(bodyParams)[0] - if (key) return bodyParams[key] - } - return undefined -} - -function resolveAuthHeaderName(headerName){ - if (options.authorizationHeader && headerName.toLowerCase() === 'authorization') { - return options.authorizationHeader - } else { - return headerName - } -} - -function applyAuthorization(req, rights, spec) { - Object.keys(rights).forEach(name => { - const rightsInfo = rights[name] - const definition = spec.securityDefinitions[name] - switch (definition.type) { - case 'basic': - const creds = `${rightsInfo.username}:${rightsInfo.password}` - const token = (typeof window !== 'undefined' && window.btoa) - ? window.btoa(creds) - : new Buffer(creds).toString('base64') - req.header[resolveAuthHeaderName('Authorization')] = `Basic ${token}` - break - case 'oauth2': - req.header[resolveAuthHeaderName('Authorization')] = `Bearer ${rightsInfo.token}` - break - case 'apiKey': - if (definition.in === 'header') { - req.header[resolveAuthHeaderName(definition.name)] = rightsInfo.apiKey - } else if (definition.in === 'query') { - req.query[definition.name] = rightsInfo.apiKey - } else { - throw new Error(`Api key must be in header or query not '${definition.in}'`) - } - break - default: - throw new Error(`Security definition type '${definition.type}' not supported`) - } - }) - return req -} - -function processResponse(req, response, attempt, options) { - const format = response.ok ? formatResponse : formatServiceError - const contentType = response.headers.get('content-type') || '' - - let parse - if (response.status === 204) { - parse = Promise.resolve() - } else if (~contentType.indexOf('json')) { - parse = response.json() - } else if (~contentType.indexOf('octet-stream')) { - parse = response.blob() - } else if (~contentType.indexOf('text')) { - parse = response.text() - } else { - parse = Promise.resolve() - } - - return parse - .then(data => format(response, data, options)) - .then(res => { - if (options.processResponse) return options.processResponse(req, res, attempt) - else return Promise.resolve({ res }) - }) -} - -function formatResponse(response, data, options) { - return { raw: response, data } -} - -function formatServiceError(response, data, options) { - if (options.formatServiceError) { - data = options.formatServiceError(response, data) - } else { - const serviceError = new ServiceError() - if (data) { - if (typeof data === 'string') serviceError.message = data - else { - if (data.message) serviceError.message = data.message - if (data.body) serviceError.body = data.body - else serviceError.body = data - } - - if (data.code) serviceError.code = data.code - } else { - serviceError.message = response.statusText - } - serviceError.status = response.status - data = serviceError - } - return { raw: response, data, error: true } -} - -function processError(req, error) { - const { processError } = options - const res = { res: { raw: {}, data: error, error: true } } - - return Promise.resolve(processError ? processError(req, res) : res) -} - -const COLLECTION_DELIM = { csv: ',', multi: '&', pipes: '|', ssv: ' ', tsv: '\t' } - -export function formatArrayParam(array, format, name) { - if (!array) return - if (format === 'multi') return array.map(value => createQueryParam(name, value)) - const delim = COLLECTION_DELIM[format] - if (!delim) throw new Error(`Invalid collection format '${format}'`) - return array.map(formatParam).join(delim) -} - -export function formatDate(date, format) { - if (!date) return - const str = date.toISOString() - return (format === 'date') ? str.split('T')[0] : str -} diff --git a/ui/api/gateway/spec.js b/ui/api/gateway/spec.js deleted file mode 100644 index 1e69e5a3..00000000 --- a/ui/api/gateway/spec.js +++ /dev/null @@ -1,23 +0,0 @@ - -// Auto-generated, edits will be overwritten -const spec = { - 'host': 'localhost', - 'schemes': [ - 'http' - ], - 'basePath': '/api/v1', - 'contentTypes': [ - 'application/zrok.v1+json' - ], - 'accepts': [ - 'application/zrok.v1+json' - ], - 'securityDefinitions': { - 'key': { - 'type': 'apiKey', - 'in': 'header', - 'name': 'x-token' - } - } -} -export default spec diff --git a/ui/api/identity.js b/ui/api/identity.js deleted file mode 100644 index a13c4b3f..00000000 --- a/ui/api/identity.js +++ /dev/null @@ -1,40 +0,0 @@ -/** @module identity */ -// Auto-generated, edits will be overwritten -import * as gateway from './gateway' - -/** - * @param {object} options Optional options - * @param {module:types.accountRequest} [options.body] - * @return {Promise} account created - */ -export function createAccount(options) { - if (!options) options = {} - const parameters = { - body: { - body: options.body - } - } - return gateway.request(createAccountOperation, parameters) -} - -/** - */ -export function enable() { - return gateway.request(enableOperation) -} - -const createAccountOperation = { - path: '/account', - contentTypes: ['application/zrok.v1+json'], - method: 'post' -} - -const enableOperation = { - path: '/enable', - method: 'post', - security: [ - { - id: 'key' - } - ] -} diff --git a/ui/api/metadata.js b/ui/api/metadata.js deleted file mode 100644 index b782394b..00000000 --- a/ui/api/metadata.js +++ /dev/null @@ -1,14 +0,0 @@ -/** @module metadata */ -// Auto-generated, edits will be overwritten -import * as gateway from './gateway' - -/** - */ -export function version() { - return gateway.request(versionOperation) -} - -const versionOperation = { - path: '/version', - method: 'get' -} diff --git a/ui/api/tunnel.js b/ui/api/tunnel.js deleted file mode 100644 index 3451aa6a..00000000 --- a/ui/api/tunnel.js +++ /dev/null @@ -1,55 +0,0 @@ -/** @module tunnel */ -// Auto-generated, edits will be overwritten -import * as gateway from './gateway' - -/** - * @param {object} options Optional options - * @param {module:types.tunnelRequest} [options.body] - * @return {Promise} tunnel created - */ -export function tunnel(options) { - if (!options) options = {} - const parameters = { - body: { - body: options.body - } - } - return gateway.request(tunnelOperation, parameters) -} - -/** - * @param {object} options Optional options - * @param {module:types.untunnelRequest} [options.body] - * @return {Promise} tunnel removed - */ -export function untunnel(options) { - if (!options) options = {} - const parameters = { - body: { - body: options.body - } - } - return gateway.request(untunnelOperation, parameters) -} - -const tunnelOperation = { - path: '/tunnel', - contentTypes: ['application/zrok.v1+json'], - method: 'post', - security: [ - { - id: 'key' - } - ] -} - -const untunnelOperation = { - path: '/untunnel', - contentTypes: ['application/zrok.v1+json'], - method: 'delete', - security: [ - { - id: 'key' - } - ] -} diff --git a/ui/api/types.js b/ui/api/types.js deleted file mode 100644 index 6c2b49a7..00000000 --- a/ui/api/types.js +++ /dev/null @@ -1,56 +0,0 @@ -/** @module types */ -// Auto-generated, edits will be overwritten - -/** - * @typedef accountRequest - * @memberof module:types - * - * @property {string} username - * @property {string} password - */ - -/** - * @typedef accountResponse - * @memberof module:types - * - * @property {string} token - */ - -/** - * @typedef enableResponse - * @memberof module:types - * - * @property {string} identity - * @property {string} cfg - */ - -/** - * @typedef principal - * @memberof module:types - * - * @property {number} id - * @property {string} username - * @property {string} token - */ - -/** - * @typedef tunnelRequest - * @memberof module:types - * - * @property {string} identity - * @property {string} endpoint - */ - -/** - * @typedef tunnelResponse - * @memberof module:types - * - * @property {string} service - */ - -/** - * @typedef untunnelRequest - * @memberof module:types - * - * @property {string} service - */ diff --git a/ui/src/Login.js b/ui/src/Login.js index b05a2cf1..cc2bb838 100644 --- a/ui/src/Login.js +++ b/ui/src/Login.js @@ -1,4 +1,5 @@ import {useState} from "react"; +import * as identity from './api/identity'; const Login = (props) => { const [email, setEmail] = useState(''); @@ -6,9 +7,21 @@ const Login = (props) => { const handleSubmit = async e => { e.preventDefault() - props.loginSuccess({ - email: email, - }) + identity.login({body: {"email": email, "password": password}}) + .then(resp => { + if(!resp.error) { + props.loginSuccess({ + email: email, + token: resp.token + }) + console.log('login succeeded', resp) + } else { + console.log('login failed') + } + }) + .catch((resp) => { + console.log('login failed', resp) + }); }; return ( @@ -17,7 +30,7 @@ const Login = (props) => { setEmail(target.value)}/>
- setPassword(target.value)}/> + setPassword(target.value)}/>
diff --git a/ui/src/api/identity.js b/ui/src/api/identity.js index a13c4b3f..fa4bce45 100644 --- a/ui/src/api/identity.js +++ b/ui/src/api/identity.js @@ -23,6 +23,21 @@ export function enable() { return gateway.request(enableOperation) } +/** + * @param {{password: string, email: string}} options Optional options + * @param {module:types.loginRequest} [options.body] + * @return {Promise} login successful + */ +export function login(options) { + if (!options) options = {} + const parameters = { + body: { + body: options.body + } + } + return gateway.request(loginOperation, parameters) +} + const createAccountOperation = { path: '/account', contentTypes: ['application/zrok.v1+json'], @@ -38,3 +53,9 @@ const enableOperation = { } ] } + +const loginOperation = { + path: '/login', + contentTypes: ['application/zrok.v1+json'], + method: 'post' +} diff --git a/ui/src/api/types.js b/ui/src/api/types.js index 6c2b49a7..6f34bece 100644 --- a/ui/src/api/types.js +++ b/ui/src/api/types.js @@ -24,6 +24,14 @@ * @property {string} cfg */ +/** + * @typedef loginRequest + * @memberof module:types + * + * @property {string} email + * @property {string} password + */ + /** * @typedef principal * @memberof module:types