From d9c40057e6c74e4d6092468f468826cb9f541423 Mon Sep 17 00:00:00 2001 From: Lakhan Samani Date: Thu, 30 Dec 2021 10:01:51 +0530 Subject: [PATCH] feat: add api for admin login --- dashboard/src/App.tsx | 11 +- dashboard/src/Router.tsx | 0 .../src/components/layouts/AuthLayout.tsx | 5 + .../src/components/layouts/DefaultLayout.tsx | 5 + dashboard/src/contexts/AuthContext.tsx | 0 server/__test__/admin_login_test.go | 27 ++ server/__test__/resolvers_test.go | 7 +- server/go.mod | 1 - server/graph/generated/generated.go | 353 ++++++++++++++++++ server/graph/model/models_gen.go | 10 + server/graph/schema.graphqls | 12 + server/graph/schema.resolvers.go | 9 + server/handlers/dashboard.go | 67 +--- server/resolvers/admin_login.go | 41 ++ server/utils/auth_token.go | 28 ++ server/utils/cookie.go | 16 + 16 files changed, 517 insertions(+), 75 deletions(-) create mode 100644 dashboard/src/Router.tsx create mode 100644 dashboard/src/components/layouts/AuthLayout.tsx create mode 100644 dashboard/src/components/layouts/DefaultLayout.tsx create mode 100644 dashboard/src/contexts/AuthContext.tsx create mode 100644 server/__test__/admin_login_test.go create mode 100644 server/resolvers/admin_login.go diff --git a/dashboard/src/App.tsx b/dashboard/src/App.tsx index d64be95..c05f223 100644 --- a/dashboard/src/App.tsx +++ b/dashboard/src/App.tsx @@ -1,19 +1,12 @@ import * as React from 'react'; import { Text, ChakraProvider } from '@chakra-ui/react'; import { MdStar } from 'react-icons/md'; +import { BrowserRouter } from 'react-router-dom'; export default function Example() { return ( - - Authorizer Dashboard - + ); } diff --git a/dashboard/src/Router.tsx b/dashboard/src/Router.tsx new file mode 100644 index 0000000..e69de29 diff --git a/dashboard/src/components/layouts/AuthLayout.tsx b/dashboard/src/components/layouts/AuthLayout.tsx new file mode 100644 index 0000000..0ba7800 --- /dev/null +++ b/dashboard/src/components/layouts/AuthLayout.tsx @@ -0,0 +1,5 @@ +import React from 'react'; + +export default function AuthLayout() { + return

Auth Layout

; +} diff --git a/dashboard/src/components/layouts/DefaultLayout.tsx b/dashboard/src/components/layouts/DefaultLayout.tsx new file mode 100644 index 0000000..505b6c5 --- /dev/null +++ b/dashboard/src/components/layouts/DefaultLayout.tsx @@ -0,0 +1,5 @@ +import React from 'react'; + +export default function DefaultLayout() { + return

Default Layout

; +} diff --git a/dashboard/src/contexts/AuthContext.tsx b/dashboard/src/contexts/AuthContext.tsx new file mode 100644 index 0000000..e69de29 diff --git a/server/__test__/admin_login_test.go b/server/__test__/admin_login_test.go new file mode 100644 index 0000000..4f19b33 --- /dev/null +++ b/server/__test__/admin_login_test.go @@ -0,0 +1,27 @@ +package test + +import ( + "testing" + + "github.com/authorizerdev/authorizer/server/graph/model" + "github.com/authorizerdev/authorizer/server/resolvers" + "github.com/stretchr/testify/assert" +) + +func aminLoginTests(s TestSetup, t *testing.T) { + t.Run(`should complete admin login`, func(t *testing.T) { + _, ctx := createContext(s) + _, err := resolvers.AdminLoginResolver(ctx, model.AdminLoginInput{ + AdminSecret: "admin_test", + }) + + assert.NotNil(t, err) + + res, err := resolvers.AdminLoginResolver(ctx, model.AdminLoginInput{ + AdminSecret: "admin", + }) + + assert.Nil(t, err) + assert.Greater(t, len(res.AccessToken), 0) + }) +} diff --git a/server/__test__/resolvers_test.go b/server/__test__/resolvers_test.go index 160265c..9fe05ec 100644 --- a/server/__test__/resolvers_test.go +++ b/server/__test__/resolvers_test.go @@ -10,9 +10,9 @@ import ( func TestResolvers(t *testing.T) { databases := map[string]string{ - enum.Sqlite.String(): "../../data.db", - enum.Arangodb.String(): "http://root:root@localhost:8529", - enum.Mongodb.String(): "mongodb://localhost:27017", + enum.Sqlite.String(): "../../data.db", + // enum.Arangodb.String(): "http://root:root@localhost:8529", + // enum.Mongodb.String(): "mongodb://localhost:27017", } for dbType, dbURL := range databases { @@ -42,6 +42,7 @@ func TestResolvers(t *testing.T) { usersTest(s, t) deleteUserTest(s, t) updateUserTest(s, t) + aminLoginTests(s, t) }) } } diff --git a/server/go.mod b/server/go.mod index 04b55a4..982e431 100644 --- a/server/go.mod +++ b/server/go.mod @@ -20,7 +20,6 @@ require ( github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.1 // indirect github.com/robertkrimen/otto v0.0.0-20211024170158-b87d35c0b86f - github.com/stretchr/testify v1.7.0 github.com/ugorji/go v1.2.6 // indirect github.com/vektah/gqlparser/v2 v2.2.0 go.mongodb.org/mongo-driver v1.8.1 diff --git a/server/graph/generated/generated.go b/server/graph/generated/generated.go index 300307a..a781e54 100644 --- a/server/graph/generated/generated.go +++ b/server/graph/generated/generated.go @@ -43,6 +43,12 @@ type DirectiveRoot struct { } type ComplexityRoot struct { + AdminLoginResponse struct { + AccessToken func(childComplexity int) int + ExpiresAt func(childComplexity int) int + Message func(childComplexity int) int + } + AuthResponse struct { AccessToken func(childComplexity int) int ExpiresAt func(childComplexity int) int @@ -66,6 +72,7 @@ type ComplexityRoot struct { } Mutation struct { + AdminLogin func(childComplexity int, params model.AdminLoginInput) int DeleteUser func(childComplexity int, params model.DeleteUserInput) int ForgotPassword func(childComplexity int, params model.ForgotPasswordInput) int Login func(childComplexity int, params model.LoginInput) int @@ -80,6 +87,7 @@ type ComplexityRoot struct { } Query struct { + AdminSession func(childComplexity int) int Meta func(childComplexity int) int Profile func(childComplexity int) int Session func(childComplexity int, roles []string) int @@ -134,6 +142,7 @@ type MutationResolver interface { ResetPassword(ctx context.Context, params model.ResetPasswordInput) (*model.Response, error) DeleteUser(ctx context.Context, params model.DeleteUserInput) (*model.Response, error) UpdateUser(ctx context.Context, params model.UpdateUserInput) (*model.User, error) + AdminLogin(ctx context.Context, params model.AdminLoginInput) (*model.AdminLoginResponse, error) } type QueryResolver interface { Meta(ctx context.Context) (*model.Meta, error) @@ -141,6 +150,7 @@ type QueryResolver interface { Profile(ctx context.Context) (*model.User, error) Users(ctx context.Context) ([]*model.User, error) VerificationRequests(ctx context.Context) ([]*model.VerificationRequest, error) + AdminSession(ctx context.Context) (*model.AdminLoginResponse, error) } type executableSchema struct { @@ -158,6 +168,27 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in _ = ec switch typeName + "." + field { + case "AdminLoginResponse.access_token": + if e.complexity.AdminLoginResponse.AccessToken == nil { + break + } + + return e.complexity.AdminLoginResponse.AccessToken(childComplexity), true + + case "AdminLoginResponse.expires_at": + if e.complexity.AdminLoginResponse.ExpiresAt == nil { + break + } + + return e.complexity.AdminLoginResponse.ExpiresAt(childComplexity), true + + case "AdminLoginResponse.message": + if e.complexity.AdminLoginResponse.Message == nil { + break + } + + return e.complexity.AdminLoginResponse.Message(childComplexity), true + case "AuthResponse.access_token": if e.complexity.AuthResponse.AccessToken == nil { break @@ -249,6 +280,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Meta.Version(childComplexity), true + case "Mutation._admin_login": + if e.complexity.Mutation.AdminLogin == nil { + break + } + + args, err := ec.field_Mutation__admin_login_args(context.TODO(), rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.Mutation.AdminLogin(childComplexity, args["params"].(model.AdminLoginInput)), true + case "Mutation._delete_user": if e.complexity.Mutation.DeleteUser == nil { break @@ -376,6 +419,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Mutation.VerifyEmail(childComplexity, args["params"].(model.VerifyEmailInput)), true + case "Query._admin_session": + if e.complexity.Query.AdminSession == nil { + break + } + + return e.complexity.Query.AdminSession(childComplexity), true + case "Query.meta": if e.complexity.Query.Meta == nil { break @@ -719,6 +769,16 @@ type Response { message: String! } +type AdminLoginResponse { + message: String! + access_token: String! + expires_at: Int64! +} + +input AdminLoginInput { + admin_secret: String! +} + input SignUpInput { email: String! given_name: String @@ -810,6 +870,7 @@ type Mutation { # admin only apis _delete_user(params: DeleteUserInput!): Response! _update_user(params: UpdateUserInput!): User! + _admin_login(params: AdminLoginInput!): AdminLoginResponse } type Query { @@ -819,6 +880,7 @@ type Query { # admin only apis _users: [User!]! _verification_requests: [VerificationRequest!]! + _admin_session: AdminLoginResponse } `, BuiltIn: false}, } @@ -828,6 +890,21 @@ var parsedSchema = gqlparser.MustLoadSchema(sources...) // region ***************************** args.gotpl ***************************** +func (ec *executionContext) field_Mutation__admin_login_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { + var err error + args := map[string]interface{}{} + var arg0 model.AdminLoginInput + if tmp, ok := rawArgs["params"]; ok { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("params")) + arg0, err = ec.unmarshalNAdminLoginInput2githubᚗcomᚋauthorizerdevᚋauthorizerᚋserverᚋgraphᚋmodelᚐAdminLoginInput(ctx, tmp) + if err != nil { + return nil, err + } + } + args["params"] = arg0 + return args, nil +} + func (ec *executionContext) field_Mutation__delete_user_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { var err error args := map[string]interface{}{} @@ -1046,6 +1123,111 @@ func (ec *executionContext) field___Type_fields_args(ctx context.Context, rawArg // region **************************** field.gotpl ***************************** +func (ec *executionContext) _AdminLoginResponse_message(ctx context.Context, field graphql.CollectedField, obj *model.AdminLoginResponse) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "AdminLoginResponse", + Field: field, + Args: nil, + IsMethod: false, + IsResolver: false, + } + + ctx = graphql.WithFieldContext(ctx, fc) + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.Message, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(string) + fc.Result = res + return ec.marshalNString2string(ctx, field.Selections, res) +} + +func (ec *executionContext) _AdminLoginResponse_access_token(ctx context.Context, field graphql.CollectedField, obj *model.AdminLoginResponse) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "AdminLoginResponse", + Field: field, + Args: nil, + IsMethod: false, + IsResolver: false, + } + + ctx = graphql.WithFieldContext(ctx, fc) + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.AccessToken, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(string) + fc.Result = res + return ec.marshalNString2string(ctx, field.Selections, res) +} + +func (ec *executionContext) _AdminLoginResponse_expires_at(ctx context.Context, field graphql.CollectedField, obj *model.AdminLoginResponse) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "AdminLoginResponse", + Field: field, + Args: nil, + IsMethod: false, + IsResolver: false, + } + + ctx = graphql.WithFieldContext(ctx, fc) + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.ExpiresAt, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(int64) + fc.Result = res + return ec.marshalNInt642int64(ctx, field.Selections, res) +} + func (ec *executionContext) _AuthResponse_message(ctx context.Context, field graphql.CollectedField, obj *model.AuthResponse) (ret graphql.Marshaler) { defer func() { if r := recover(); r != nil { @@ -1947,6 +2129,45 @@ func (ec *executionContext) _Mutation__update_user(ctx context.Context, field gr return ec.marshalNUser2ᚖgithubᚗcomᚋauthorizerdevᚋauthorizerᚋserverᚋgraphᚋmodelᚐUser(ctx, field.Selections, res) } +func (ec *executionContext) _Mutation__admin_login(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "Mutation", + Field: field, + Args: nil, + IsMethod: true, + IsResolver: true, + } + + ctx = graphql.WithFieldContext(ctx, fc) + rawArgs := field.ArgumentMap(ec.Variables) + args, err := ec.field_Mutation__admin_login_args(ctx, rawArgs) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + fc.Args = args + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Mutation().AdminLogin(rctx, args["params"].(model.AdminLoginInput)) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*model.AdminLoginResponse) + fc.Result = res + return ec.marshalOAdminLoginResponse2ᚖgithubᚗcomᚋauthorizerdevᚋauthorizerᚋserverᚋgraphᚋmodelᚐAdminLoginResponse(ctx, field.Selections, res) +} + func (ec *executionContext) _Query_meta(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { defer func() { if r := recover(); r != nil { @@ -2126,6 +2347,38 @@ func (ec *executionContext) _Query__verification_requests(ctx context.Context, f return ec.marshalNVerificationRequest2ᚕᚖgithubᚗcomᚋauthorizerdevᚋauthorizerᚋserverᚋgraphᚋmodelᚐVerificationRequestᚄ(ctx, field.Selections, res) } +func (ec *executionContext) _Query__admin_session(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "Query", + Field: field, + Args: nil, + IsMethod: true, + IsResolver: true, + } + + ctx = graphql.WithFieldContext(ctx, fc) + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Query().AdminSession(rctx) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*model.AdminLoginResponse) + fc.Result = res + return ec.marshalOAdminLoginResponse2ᚖgithubᚗcomᚋauthorizerdevᚋauthorizerᚋserverᚋgraphᚋmodelᚐAdminLoginResponse(ctx, field.Selections, res) +} + func (ec *executionContext) _Query___type(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { defer func() { if r := recover(); r != nil { @@ -4140,6 +4393,29 @@ func (ec *executionContext) ___Type_ofType(ctx context.Context, field graphql.Co // region **************************** input.gotpl ***************************** +func (ec *executionContext) unmarshalInputAdminLoginInput(ctx context.Context, obj interface{}) (model.AdminLoginInput, error) { + var it model.AdminLoginInput + asMap := map[string]interface{}{} + for k, v := range obj.(map[string]interface{}) { + asMap[k] = v + } + + for k, v := range asMap { + switch k { + case "admin_secret": + var err error + + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("admin_secret")) + it.AdminSecret, err = ec.unmarshalNString2string(ctx, v) + if err != nil { + return it, err + } + } + } + + return it, nil +} + func (ec *executionContext) unmarshalInputDeleteUserInput(ctx context.Context, obj interface{}) (model.DeleteUserInput, error) { var it model.DeleteUserInput asMap := map[string]interface{}{} @@ -4682,6 +4958,43 @@ func (ec *executionContext) unmarshalInputVerifyEmailInput(ctx context.Context, // region **************************** object.gotpl **************************** +var adminLoginResponseImplementors = []string{"AdminLoginResponse"} + +func (ec *executionContext) _AdminLoginResponse(ctx context.Context, sel ast.SelectionSet, obj *model.AdminLoginResponse) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, adminLoginResponseImplementors) + + out := graphql.NewFieldSet(fields) + var invalids uint32 + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("AdminLoginResponse") + case "message": + out.Values[i] = ec._AdminLoginResponse_message(ctx, field, obj) + if out.Values[i] == graphql.Null { + invalids++ + } + case "access_token": + out.Values[i] = ec._AdminLoginResponse_access_token(ctx, field, obj) + if out.Values[i] == graphql.Null { + invalids++ + } + case "expires_at": + out.Values[i] = ec._AdminLoginResponse_expires_at(ctx, field, obj) + if out.Values[i] == graphql.Null { + invalids++ + } + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch() + if invalids > 0 { + return graphql.Null + } + return out +} + var authResponseImplementors = []string{"AuthResponse"} func (ec *executionContext) _AuthResponse(ctx context.Context, sel ast.SelectionSet, obj *model.AuthResponse) graphql.Marshaler { @@ -4874,6 +5187,8 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet) if out.Values[i] == graphql.Null { invalids++ } + case "_admin_login": + out.Values[i] = ec._Mutation__admin_login(ctx, field) default: panic("unknown field " + strconv.Quote(field.Name)) } @@ -4967,6 +5282,17 @@ func (ec *executionContext) _Query(ctx context.Context, sel ast.SelectionSet) gr } return res }) + case "_admin_session": + field := field + out.Concurrently(i, func() (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._Query__admin_session(ctx, field) + return res + }) case "__type": out.Values[i] = ec._Query___type(ctx, field) case "__schema": @@ -5369,6 +5695,11 @@ func (ec *executionContext) ___Type(ctx context.Context, sel ast.SelectionSet, o // region ***************************** type.gotpl ***************************** +func (ec *executionContext) unmarshalNAdminLoginInput2githubᚗcomᚋauthorizerdevᚋauthorizerᚋserverᚋgraphᚋmodelᚐAdminLoginInput(ctx context.Context, v interface{}) (model.AdminLoginInput, error) { + res, err := ec.unmarshalInputAdminLoginInput(ctx, v) + return res, graphql.ErrorOnPath(ctx, err) +} + func (ec *executionContext) marshalNAuthResponse2githubᚗcomᚋauthorizerdevᚋauthorizerᚋserverᚋgraphᚋmodelᚐAuthResponse(ctx context.Context, sel ast.SelectionSet, v model.AuthResponse) graphql.Marshaler { return ec._AuthResponse(ctx, sel, &v) } @@ -5423,6 +5754,21 @@ func (ec *executionContext) marshalNID2string(ctx context.Context, sel ast.Selec return res } +func (ec *executionContext) unmarshalNInt642int64(ctx context.Context, v interface{}) (int64, error) { + res, err := graphql.UnmarshalInt64(v) + return res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) marshalNInt642int64(ctx context.Context, sel ast.SelectionSet, v int64) graphql.Marshaler { + res := graphql.MarshalInt64(v) + if res == graphql.Null { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + ec.Errorf(ctx, "must not be null") + } + } + return res +} + func (ec *executionContext) unmarshalNLoginInput2githubᚗcomᚋauthorizerdevᚋauthorizerᚋserverᚋgraphᚋmodelᚐLoginInput(ctx context.Context, v interface{}) (model.LoginInput, error) { res, err := ec.unmarshalInputLoginInput(ctx, v) return res, graphql.ErrorOnPath(ctx, err) @@ -5911,6 +6257,13 @@ func (ec *executionContext) marshalN__TypeKind2string(ctx context.Context, sel a return res } +func (ec *executionContext) marshalOAdminLoginResponse2ᚖgithubᚗcomᚋauthorizerdevᚋauthorizerᚋserverᚋgraphᚋmodelᚐAdminLoginResponse(ctx context.Context, sel ast.SelectionSet, v *model.AdminLoginResponse) graphql.Marshaler { + if v == nil { + return graphql.Null + } + return ec._AdminLoginResponse(ctx, sel, v) +} + func (ec *executionContext) marshalOAuthResponse2ᚖgithubᚗcomᚋauthorizerdevᚋauthorizerᚋserverᚋgraphᚋmodelᚐAuthResponse(ctx context.Context, sel ast.SelectionSet, v *model.AuthResponse) graphql.Marshaler { if v == nil { return graphql.Null diff --git a/server/graph/model/models_gen.go b/server/graph/model/models_gen.go index ffd76ea..17b294b 100644 --- a/server/graph/model/models_gen.go +++ b/server/graph/model/models_gen.go @@ -2,6 +2,16 @@ package model +type AdminLoginInput struct { + AdminSecret string `json:"admin_secret"` +} + +type AdminLoginResponse struct { + Message string `json:"message"` + AccessToken string `json:"access_token"` + ExpiresAt int64 `json:"expires_at"` +} + type AuthResponse struct { Message string `json:"message"` AccessToken *string `json:"access_token"` diff --git a/server/graph/schema.graphqls b/server/graph/schema.graphqls index 4353073..67fd957 100644 --- a/server/graph/schema.graphqls +++ b/server/graph/schema.graphqls @@ -62,6 +62,16 @@ type Response { message: String! } +type AdminLoginResponse { + message: String! + access_token: String! + expires_at: Int64! +} + +input AdminLoginInput { + admin_secret: String! +} + input SignUpInput { email: String! given_name: String @@ -153,6 +163,7 @@ type Mutation { # admin only apis _delete_user(params: DeleteUserInput!): Response! _update_user(params: UpdateUserInput!): User! + _admin_login(params: AdminLoginInput!): AdminLoginResponse } type Query { @@ -162,4 +173,5 @@ type Query { # admin only apis _users: [User!]! _verification_requests: [VerificationRequest!]! + _admin_session: AdminLoginResponse } diff --git a/server/graph/schema.resolvers.go b/server/graph/schema.resolvers.go index 9510800..24197d4 100644 --- a/server/graph/schema.resolvers.go +++ b/server/graph/schema.resolvers.go @@ -5,6 +5,7 @@ package graph import ( "context" + "fmt" "github.com/authorizerdev/authorizer/server/graph/generated" "github.com/authorizerdev/authorizer/server/graph/model" @@ -55,6 +56,10 @@ func (r *mutationResolver) UpdateUser(ctx context.Context, params model.UpdateUs return resolvers.UpdateUser(ctx, params) } +func (r *mutationResolver) AdminLogin(ctx context.Context, params model.AdminLoginInput) (*model.AdminLoginResponse, error) { + return resolvers.AdminLoginResolver(ctx, params) +} + func (r *queryResolver) Meta(ctx context.Context) (*model.Meta, error) { return resolvers.Meta(ctx) } @@ -75,6 +80,10 @@ func (r *queryResolver) VerificationRequests(ctx context.Context) ([]*model.Veri return resolvers.VerificationRequests(ctx) } +func (r *queryResolver) AdminSession(ctx context.Context) (*model.AdminLoginResponse, error) { + panic(fmt.Errorf("not implemented")) +} + // Mutation returns generated.MutationResolver implementation. func (r *Resolver) Mutation() generated.MutationResolver { return &mutationResolver{r} } diff --git a/server/handlers/dashboard.go b/server/handlers/dashboard.go index 88eb40b..9ad06ec 100644 --- a/server/handlers/dashboard.go +++ b/server/handlers/dashboard.go @@ -1,79 +1,22 @@ package handlers import ( - "encoding/base64" - "encoding/json" - "log" "net/http" - "strings" "github.com/authorizerdev/authorizer/server/constants" - "github.com/authorizerdev/authorizer/server/utils" "github.com/gin-gonic/gin" ) func DashboardHandler() gin.HandlerFunc { return func(c *gin.Context) { - state := c.Query("state") - - var stateObj State - - if state == "" { - // cookie, err := utils.GetAuthToken(c) - // if err != nil { - // c.JSON(400, gin.H{"error": "invalid state"}) - // return - // } - - stateObj.AuthorizerURL = constants.AUTHORIZER_URL - stateObj.RedirectURL = constants.AUTHORIZER_URL + "/app" - - } else { - decodedState, err := base64.StdEncoding.DecodeString(state) - if err != nil { - c.JSON(400, gin.H{"error": "[unable to decode state] invalid state"}) - return - } - - err = json.Unmarshal(decodedState, &stateObj) - if err != nil { - c.JSON(400, gin.H{"error": "[unable to parse state] invalid state"}) - return - } - stateObj.AuthorizerURL = strings.TrimSuffix(stateObj.AuthorizerURL, "/") - stateObj.RedirectURL = strings.TrimSuffix(stateObj.RedirectURL, "/") - - // validate redirect url with allowed origins - if !utils.IsValidOrigin(stateObj.RedirectURL) { - c.JSON(400, gin.H{"error": "invalid redirect url"}) - return - } - - if stateObj.AuthorizerURL == "" { - c.JSON(400, gin.H{"error": "invalid authorizer url"}) - return - } - - // validate host and domain of authorizer url - if strings.TrimSuffix(stateObj.AuthorizerURL, "/") != constants.AUTHORIZER_URL { - c.JSON(400, gin.H{"error": "invalid host url"}) - return - } + isOnboardingCompleted := false + if constants.ADMIN_SECRET != "" && constants.DATABASE_TYPE != "" && constants.DATABASE_URL != "" { + isOnboardingCompleted = true } - // debug the request state - if pusher := c.Writer.Pusher(); pusher != nil { - // use pusher.Push() to do server push - if err := pusher.Push("/app/build/bundle.js", nil); err != nil { - log.Printf("Failed to push: %v", err) - } - } c.HTML(http.StatusOK, "dashboard.tmpl", gin.H{ - "data": map[string]string{ - "authorizerURL": stateObj.AuthorizerURL, - "redirectURL": stateObj.RedirectURL, - "organizationName": constants.ORGANIZATION_NAME, - "organizationLogo": constants.ORGANIZATION_LOGO, + "data": map[string]interface{}{ + "isOnboardingCompleted": isOnboardingCompleted, }, }) } diff --git a/server/resolvers/admin_login.go b/server/resolvers/admin_login.go new file mode 100644 index 0000000..e61bae7 --- /dev/null +++ b/server/resolvers/admin_login.go @@ -0,0 +1,41 @@ +package resolvers + +import ( + "context" + "fmt" + "log" + "time" + + "github.com/authorizerdev/authorizer/server/constants" + "github.com/authorizerdev/authorizer/server/enum" + "github.com/authorizerdev/authorizer/server/graph/model" + "github.com/authorizerdev/authorizer/server/session" + "github.com/authorizerdev/authorizer/server/utils" +) + +func AdminLoginResolver(ctx context.Context, params model.AdminLoginInput) (*model.AdminLoginResponse, error) { + gc, err := utils.GinContextFromContext(ctx) + var res *model.AdminLoginResponse + if err != nil { + log.Println("=> error:", err) + return res, err + } + if params.AdminSecret != constants.ADMIN_SECRET { + return nil, fmt.Errorf(`invalid admin secret`) + } + + refreshToken, _, _ := utils.CreateAdminAuthToken(enum.RefreshToken, gc) + accessToken, expiresAt, _ := utils.CreateAdminAuthToken(enum.AccessToken, gc) + + currentTime := time.Now().Unix() + tokenId := fmt.Sprintf("authorizer_admin_%d", currentTime) + session.SetToken(tokenId, accessToken, refreshToken) + utils.SetAdminCookie(gc, accessToken) + + res = &model.AdminLoginResponse{ + AccessToken: accessToken, + ExpiresAt: expiresAt, + Message: "admin logged in successfully", + } + return res, nil +} diff --git a/server/utils/auth_token.go b/server/utils/auth_token.go index 23995de..7d4a3ad 100644 --- a/server/utils/auth_token.go +++ b/server/utils/auth_token.go @@ -124,3 +124,31 @@ func VerifyAuthToken(token string) (map[string]interface{}, error) { return res, nil } + +func CreateAdminAuthToken(tokenType enum.TokenType, c *gin.Context) (string, int64, error) { + t := jwt.New(jwt.GetSigningMethod(constants.JWT_TYPE)) + expiryBound := time.Hour + if tokenType == enum.RefreshToken { + // expires in 1 year + expiryBound = time.Hour * 8760 + } + + expiresAt := time.Now().Add(expiryBound).Unix() + + customClaims := jwt.MapClaims{ + "exp": expiresAt, + "iat": time.Now().Unix(), + "user_agent": GetUserAgent(c.Request), + "ip": GetIP(c.Request), + "role": "authorizer_admin", + "created_at": time.Now().Unix(), + } + + t.Claims = customClaims + + token, err := t.SignedString([]byte(constants.JWT_SECRET)) + if err != nil { + return "", 0, err + } + return token, expiresAt, nil +} diff --git a/server/utils/cookie.go b/server/utils/cookie.go index 38cf327..c9323f7 100644 --- a/server/utils/cookie.go +++ b/server/utils/cookie.go @@ -47,3 +47,19 @@ func DeleteCookie(gc *gin.Context) { gc.SetCookie(constants.COOKIE_NAME, "", -1, "/", host, secure, httpOnly) gc.SetCookie(constants.COOKIE_NAME+"-client", "", -1, "/", domain, secure, httpOnly) } + +func SetAdminCookie(gc *gin.Context, token string) { + secure := true + httpOnly := true + host, _ := GetHostParts(constants.AUTHORIZER_URL) + + gc.SetCookie("authorizer-admin", token, 3600, "/", host, secure, httpOnly) +} + +func DeleteAdminCookie(gc *gin.Context, token string) { + secure := true + httpOnly := true + host, _ := GetHostParts(constants.AUTHORIZER_URL) + + gc.SetCookie("authorizer-admin", "", -1, "/", host, secure, httpOnly) +}