From 304b9998dc6d9fdcdba51e4215bb9254dbfdfb52 Mon Sep 17 00:00:00 2001 From: BradHacker Date: Thu, 25 Apr 2024 12:55:48 -0400 Subject: [PATCH 1/2] Add password to GraphQL user create --- backend/go.mod | 2 +- backend/go.sum | 4 ++-- backend/graph/generated/generated.go | 10 +++++++++- backend/graph/model/models_gen.go | 9 +++++---- backend/graph/schema.graphqls | 1 + backend/graph/schema.resolvers.go | 13 +++++++++++++ frontend/src/lib/api/generated/graphql.schema.json | 12 ++++++++++++ frontend/src/lib/api/generated/index.tsx | 1 + 8 files changed, 44 insertions(+), 8 deletions(-) diff --git a/backend/go.mod b/backend/go.mod index 1c479c9..4fa5890 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -5,7 +5,7 @@ go 1.22.2 require ( entgo.io/ent v0.12.5 github.com/99designs/gqlgen v0.17.44 - github.com/cble-platform/cble-provider-grpc v0.2.1 + github.com/cble-platform/cble-provider-grpc v0.2.2 github.com/docker/docker v25.0.3+incompatible github.com/fatih/color v1.16.0 github.com/gin-contrib/cors v1.4.0 diff --git a/backend/go.sum b/backend/go.sum index f2df96a..5c4f290 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -43,8 +43,8 @@ github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1 github.com/bytedance/sonic v1.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM= github.com/bytedance/sonic v1.10.2 h1:GQebETVBxYB7JGWJtLBi07OVzWwt+8dWA00gEVW2ZFE= github.com/bytedance/sonic v1.10.2/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4= -github.com/cble-platform/cble-provider-grpc v0.2.1 h1:ZxL+YPhD+B8jRe4YnIaOC6lhjpWS0iAM/h+tg/Ahfy4= -github.com/cble-platform/cble-provider-grpc v0.2.1/go.mod h1:udlx6gdnTEX2dDU1PvPEOwDnAJKUogbtHqRe3Fg3RZI= +github.com/cble-platform/cble-provider-grpc v0.2.2 h1:Xvl4cnu9vdzx6zbbZ1jJkb72Wtu/9cDp1nu9OGa4X7c= +github.com/cble-platform/cble-provider-grpc v0.2.2/go.mod h1:udlx6gdnTEX2dDU1PvPEOwDnAJKUogbtHqRe3Fg3RZI= github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= diff --git a/backend/graph/generated/generated.go b/backend/graph/generated/generated.go index dc84d61..421bc92 100644 --- a/backend/graph/generated/generated.go +++ b/backend/graph/generated/generated.go @@ -2505,6 +2505,7 @@ input ProviderInput { input UserInput { username: String! + password: String email: String! firstName: String! lastName: String! @@ -15489,7 +15490,7 @@ func (ec *executionContext) unmarshalInputUserInput(ctx context.Context, obj int asMap[k] = v } - fieldsInOrder := [...]string{"username", "email", "firstName", "lastName"} + fieldsInOrder := [...]string{"username", "password", "email", "firstName", "lastName"} for _, k := range fieldsInOrder { v, ok := asMap[k] if !ok { @@ -15503,6 +15504,13 @@ func (ec *executionContext) unmarshalInputUserInput(ctx context.Context, obj int return it, err } it.Username = data + case "password": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("password")) + data, err := ec.unmarshalOString2áš–string(ctx, v) + if err != nil { + return it, err + } + it.Password = data case "email": ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("email")) data, err := ec.unmarshalNString2string(ctx, v) diff --git a/backend/graph/model/models_gen.go b/backend/graph/model/models_gen.go index 38f0b58..c68fa39 100644 --- a/backend/graph/model/models_gen.go +++ b/backend/graph/model/models_gen.go @@ -93,10 +93,11 @@ type ProviderPage struct { } type UserInput struct { - Username string `json:"username"` - Email string `json:"email"` - FirstName string `json:"firstName"` - LastName string `json:"lastName"` + Username string `json:"username"` + Password *string `json:"password,omitempty"` + Email string `json:"email"` + FirstName string `json:"firstName"` + LastName string `json:"lastName"` } type UserPage struct { diff --git a/backend/graph/schema.graphqls b/backend/graph/schema.graphqls index 245ae1a..365ee12 100644 --- a/backend/graph/schema.graphqls +++ b/backend/graph/schema.graphqls @@ -422,6 +422,7 @@ input ProviderInput { input UserInput { username: String! + password: String email: String! firstName: String! lastName: String! diff --git a/backend/graph/schema.resolvers.go b/backend/graph/schema.resolvers.go index dcb82b2..d86feb2 100644 --- a/backend/graph/schema.resolvers.go +++ b/backend/graph/schema.resolvers.go @@ -33,6 +33,7 @@ import ( "github.com/cble-platform/cble/backend/permission/actions" "github.com/google/uuid" "github.com/vektah/gqlparser/v2/gqlerror" + "golang.org/x/crypto/bcrypt" yaml "gopkg.in/yaml.v3" ) @@ -167,12 +168,24 @@ func (r *mutationResolver) CreateUser(ctx context.Context, input model.UserInput return nil, auth.PERMISSION_DENIED_GQL_ERROR } + // Make sure password is included + if input.Password == nil { + return nil, gqlerror.Errorf("must provide password") + } + + // Hash the password + hashedPassword, err := bcrypt.GenerateFromPassword([]byte(*input.Password), 8) + if err != nil { + return nil, gqlerror.Errorf("failed to hash user password: %v", err) + } + // Create the user entUser, err := r.ent.User.Create(). SetEmail(input.Email). SetFirstName(input.FirstName). SetLastName(input.LastName). SetUsername(input.Username). + SetPassword(string(hashedPassword)). Save(ctx) if err != nil { return nil, gqlerror.Errorf("failed to create user: %v", err) diff --git a/frontend/src/lib/api/generated/graphql.schema.json b/frontend/src/lib/api/generated/graphql.schema.json index 980f941..d49021f 100644 --- a/frontend/src/lib/api/generated/graphql.schema.json +++ b/frontend/src/lib/api/generated/graphql.schema.json @@ -5475,6 +5475,18 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "password", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "username", "description": null, diff --git a/frontend/src/lib/api/generated/index.tsx b/frontend/src/lib/api/generated/index.tsx index 37e845c..76417d5 100644 --- a/frontend/src/lib/api/generated/index.tsx +++ b/frontend/src/lib/api/generated/index.tsx @@ -752,6 +752,7 @@ export type UserInput = { email: Scalars['String']['input']; firstName: Scalars['String']['input']; lastName: Scalars['String']['input']; + password?: InputMaybe; username: Scalars['String']['input']; }; From 54567332be7c8480310036e2993db630f502dc6d Mon Sep 17 00:00:00 2001 From: BradHacker Date: Thu, 25 Apr 2024 13:29:59 -0400 Subject: [PATCH 2/2] Add users page --- frontend/src/components/navbar.tsx | 13 ++ frontend/src/lib/api/generated/index.tsx | 14 +- frontend/src/lib/api/graphql/permission.gql | 5 + frontend/src/lib/api/graphql/user.gql | 4 +- frontend/src/main.tsx | 9 +- frontend/src/{ => routes}/error-page.tsx | 5 + frontend/src/routes/users/index.tsx | 168 ++++++++++++++++++++ 7 files changed, 211 insertions(+), 7 deletions(-) rename frontend/src/{ => routes}/error-page.tsx (74%) create mode 100644 frontend/src/routes/users/index.tsx diff --git a/frontend/src/components/navbar.tsx b/frontend/src/components/navbar.tsx index 5b4af88..f321ca6 100644 --- a/frontend/src/components/navbar.tsx +++ b/frontend/src/components/navbar.tsx @@ -132,6 +132,19 @@ export default function Navbar({ Permissions )} + {navPermissions?.listUsers && ( + + )} ; -export type NavPermissionsQuery = { __typename?: 'Query', listProviders: boolean, listPermissions: boolean }; +export type NavPermissionsQuery = { __typename?: 'Query', listProviders: boolean, listPermissions: boolean, listUsers: boolean }; export type GrantPermissionMutationVariables = Exact<{ subjectType: SubjectType; @@ -1019,7 +1019,10 @@ export type MeQueryVariables = Exact<{ [key: string]: never; }>; export type MeQuery = { __typename?: 'Query', me: { __typename?: 'User', id: string, createdAt: any, updatedAt: any, username: string, email: string, firstName: string, lastName: string } }; -export type ListUsersQueryVariables = Exact<{ [key: string]: never; }>; +export type ListUsersQueryVariables = Exact<{ + count?: Scalars['Int']['input']; + offset?: InputMaybe; +}>; export type ListUsersQuery = { __typename?: 'Query', users: { __typename?: 'UserPage', total: number, users: Array<{ __typename?: 'User', id: string, createdAt: any, updatedAt: any, username: string, email: string, firstName: string, lastName: string }> } }; @@ -1763,6 +1766,7 @@ export const NavPermissionsDocument = gql` objectID: null action: permission_list ) + listUsers: meHasPermission(objectType: user, objectID: null, action: user_list) } `; @@ -2432,8 +2436,8 @@ export type MeLazyQueryHookResult = ReturnType; export type MeSuspenseQueryHookResult = ReturnType; export type MeQueryResult = Apollo.QueryResult; export const ListUsersDocument = gql` - query ListUsers { - users { + query ListUsers($count: Int! = 10, $offset: Int) { + users(count: $count, offset: $offset) { users { ...UserFragment } @@ -2454,6 +2458,8 @@ export const ListUsersDocument = gql` * @example * const { data, loading, error } = useListUsersQuery({ * variables: { + * count: // value for 'count' + * offset: // value for 'offset' * }, * }); */ diff --git a/frontend/src/lib/api/graphql/permission.gql b/frontend/src/lib/api/graphql/permission.gql index b3b8421..3aab51f 100644 --- a/frontend/src/lib/api/graphql/permission.gql +++ b/frontend/src/lib/api/graphql/permission.gql @@ -30,6 +30,11 @@ query NavPermissions { objectID: null action: permission_list ) + listUsers: meHasPermission( + objectType: user + objectID: null + action: user_list + ) } mutation GrantPermission( diff --git a/frontend/src/lib/api/graphql/user.gql b/frontend/src/lib/api/graphql/user.gql index b74efd9..49ab0f9 100644 --- a/frontend/src/lib/api/graphql/user.gql +++ b/frontend/src/lib/api/graphql/user.gql @@ -14,8 +14,8 @@ query Me { } } -query ListUsers { - users { +query ListUsers($count: Int! = 10, $offset: Int) { + users(count: $count, offset: $offset) { users { ...UserFragment } diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index a347fbe..9182c63 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -17,10 +17,11 @@ import { SnackbarProvider } from 'notistack' import { ReactFlowProvider } from 'reactflow' import { Box, CircularProgress } from '@mui/material' import Logo from './components/logo' +import Users from './routes/users' // Pages const Root = lazy(() => import('./routes/root')) -const ErrorPage = lazy(() => import('./error-page')) +const ErrorPage = lazy(() => import('./routes/error-page')) const Login = lazy(() => import('./routes/auth/login')) const Blueprints = lazy(() => import('./routes/blueprints')) const RequestBlueprint = lazy(() => import('./routes/blueprints/request')) @@ -168,6 +169,12 @@ const router = createBrowserRouter([ { index: true, element: } /> }, ], }, + { + path: 'users', + children: [ + { index: true, element: } /> }, + ], + }, ], errorElement: } />, }, diff --git a/frontend/src/error-page.tsx b/frontend/src/routes/error-page.tsx similarity index 74% rename from frontend/src/error-page.tsx rename to frontend/src/routes/error-page.tsx index 17d6499..704ce1e 100644 --- a/frontend/src/error-page.tsx +++ b/frontend/src/routes/error-page.tsx @@ -1,7 +1,11 @@ import { Container, Typography } from '@mui/material' import { useRouteError } from 'react-router-dom' +import Navbar from '../components/navbar' +import { ThemeContext } from '../theme' +import { useContext } from 'react' export default function ErrorPage() { + const { themePreference, setThemePreference } = useContext(ThemeContext) const error = useRouteError() as { statusText?: string error?: Error @@ -18,6 +22,7 @@ export default function ErrorPage() { minHeight: '100dvh', }} > + Oops! Sorry, an unexpected error has occurred. diff --git a/frontend/src/routes/users/index.tsx b/frontend/src/routes/users/index.tsx new file mode 100644 index 0000000..6761f40 --- /dev/null +++ b/frontend/src/routes/users/index.tsx @@ -0,0 +1,168 @@ +import { + Box, + Button, + ButtonGroup, + Container, + Divider, + Paper, + Table, + TableBody, + TableCell, + TableContainer, + TableFooter, + TableHead, + TablePagination, + TableRow, + Typography, +} from '@mui/material' +import { useSnackbar } from 'notistack' +import { + Action, + ListProvidersQuery, + ObjectType, + useListUsersQuery, + useMeHasPermissionQuery, +} from '../../lib/api/generated' +import { useEffect, useMemo, useState } from 'react' +import { Add, Delete, Edit } from '@mui/icons-material' +import { useNavigate } from 'react-router-dom' + +export default function Users() { + const { enqueueSnackbar } = useSnackbar() + const navigate = useNavigate() + const { data: createUsersData } = useMeHasPermissionQuery({ + variables: { + objectID: null, + objectType: ObjectType.User, + action: Action.UserCreate, + }, + }) + const { + data: listUsersData, + error: listUsersError, + loading: listUsersLoading, + refetch: refetchListUsers, + } = useListUsersQuery() + const [moreMenuEl, setMoreMenuEl] = useState(null) + const [moreMenuProvider, setMoreMenuProvider] = + useState() + const [page, setPage] = useState(0) + const [rowsPerPage, setRowsPerPage] = useState(10) + + const emptyRows = useMemo( + () => + page > 0 + ? Math.max(0, rowsPerPage - (listUsersData?.users.users.length ?? 0)) + : 0, + [page, rowsPerPage, listUsersData] + ) + + useEffect(() => { + refetchListUsers({ + count: rowsPerPage, + offset: rowsPerPage * page, + }) + }, [page, rowsPerPage]) + + return ( + + + Users + {createUsersData?.meHasPermission && ( + + )} + + + + + + + + Username + First Name + Last Name + Email + + + + + {listUsersData?.users.users.map((row) => ( + + + {row.username} + + {row.firstName} + {row.lastName} + {row.email} + + + + + + + + ))} + {emptyRows > 0 && ( + + + + )} + + + + { + setPage(0) + setRowsPerPage(parseInt(e.target.value)) + }} + page={page} + onPageChange={(_e, value) => setPage(value)} + count={listUsersData?.users.total ?? 0} + disabled={listUsersLoading} + /> + + +
+
+
+ ) +}