Skip to content

Commit

Permalink
UI and API for an admin to update a user
Browse files Browse the repository at this point in the history
  • Loading branch information
briskt committed Dec 28, 2023
1 parent de34108 commit 952c9fe
Show file tree
Hide file tree
Showing 7 changed files with 155 additions and 1 deletion.
7 changes: 6 additions & 1 deletion assets/data/api/users.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type {User} from 'data/types/user'
import type {User, UserUpdateInput} from 'data/types/user'
import api from '../api'

export const getUser = async (id: string): Promise<User> => {
Expand All @@ -10,3 +10,8 @@ export const listUsers = async (): Promise<User[]> => {
const response = await api.get('/api/users')
return response.json()
}

export const updateUser = async (id: string, input: UserUpdateInput): Promise<User> => {
const response = await api.put('/api/users/' + encodeURIComponent(id), input)
return response.json()
}
6 changes: 6 additions & 0 deletions assets/data/types/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ export type User = {
TenantID: string
}

export type UserUpdateInput = {
Email: string
FirstName: string
LastName: string
}

export const isAdmin = (user: User) => user.Role == Admin

const Admin = 'Admin'
60 changes: 60 additions & 0 deletions assets/pages/admin/users/[id].svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<script lang="ts">
import {getUser, updateUser} from 'data/api/users'
import type {User, UserUpdateInput} from 'data/types/user'
import {goto} from '@roxi/routify'
import {Button, Form, TextField} from '@silintl/ui-components'
import {onMount} from 'svelte'
export let id: string
let user = {} as User
const formData = {} as UserUpdateInput
onMount(async () => {
user = await getUser(id)
formData.FirstName = user.FirstName
formData.LastName = user.LastName
formData.Email = user.Email
})
const onSubmit = (event: any) => {
updateUser(id, formData)
$goto('/admin/users')
}
const onCancel = (event: Event) => {
event.preventDefault()
$goto('/admin/users')
}
</script>

<h2>Edit User</h2>

<Form on:submit={onSubmit}>
<div class="my-1">
<p>
<TextField required label="First Name" bind:value={formData.FirstName} />
</p>
<p>
<TextField required label="Last Name" bind:value={formData.LastName} />
</p>
<p>
<TextField required label="Email" bind:value={formData.Email} />
</p>
</div>
<div class="form-button">
<Button raised>Save</Button>
</div>
<div class="form-button">
<Button on:click={onCancel}>Cancel</Button>
</div>
</Form>


<style>
.form-button {
float: right;
margin: 0.5rem;
}
</style>
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@

<table>
<tr>
<th>Edit</th>
<th>Role</th>
<th>Tenant</th>
<th>First Name</th>
Expand All @@ -36,6 +37,7 @@
</tr>
{#each users as user (user.ID)}
<tr>
<td><a href="/admin/users/{user.ID}">Edit</a></td>
<td>{user.Role}</td>
<td>{getTenantNameFromID(user.TenantID)}</td>
<td>{user.FirstName}</td>
Expand Down
1 change: 1 addition & 0 deletions server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,4 +84,5 @@ func (s *Server) registerAPIRoutes() {

api.GET("/users", s.usersListHandler)
api.GET("/users/:id", s.userHandler)
api.PUT("/users/:id", s.usersUpdateHandler)
}
27 changes: 27 additions & 0 deletions server/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,30 @@ func (s *Server) userHandler(c echo.Context) error {

return c.JSON(http.StatusOK, user)
}

func (s *Server) usersUpdateHandler(c echo.Context) error {
var input app.UserUpdateInput
err := (&echo.DefaultBinder{}).BindBody(c, &input)
if err != nil {
// TODO: improve error response here (and probably everywhere else too)
return echo.NewHTTPError(http.StatusBadRequest, "bad request")
}

id := c.Param("id")
actor := app.CurrentUser(c)
if id != actor.ID && actor.Role != app.UserRoleAdmin {
return echo.NewHTTPError(http.StatusNotFound, AuthError{Error: "not found"})
}

updatedUser, err := db.UpdateUser(c, id, input)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, err)
}

user, err := db.ConvertUser(c, updatedUser)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, err)
}

return c.JSON(http.StatusOK, user)
}
53 changes: 53 additions & 0 deletions server/user_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,3 +113,56 @@ func (ts *TestSuite) Test_GetUserList() {
})
}
}

func (ts *TestSuite) Test_usersUpdateHandler() {
user := ts.createUserFixture(app.UserRoleBasic)
admin := ts.createUserFixture(app.UserRoleAdmin)

tests := []struct {
name string
actor db.User
userID string
wantStatus int
}{
{
name: "not a valid user",
userID: "xyz",
wantStatus: http.StatusUnauthorized,
},
{
name: "a user cannot update a user",
actor: user,
userID: admin.ID,
wantStatus: http.StatusNotFound,
},
{
name: "admin can update a user",
actor: admin,
userID: user.ID,
wantStatus: http.StatusOK,
},
}

for _, tt := range tests {
ts.T().Run(tt.name, func(t *testing.T) {
email := "[email protected]"
input := app.UserUpdateInput{Email: &email}
body, status := ts.request(http.MethodPut, "/api/users/"+tt.userID, tt.actor.Email, input)

// Assertions
ts.Equal(tt.wantStatus, status, "incorrect http status, body: \n%s", body)

if tt.wantStatus != http.StatusOK {
return
}

var gotUser app.User
ts.NoError(json.Unmarshal(body, &gotUser))
ts.Equal(*input.Email, gotUser.Email, "incorrect User Email, body: \n%s", body)

dbUser, err := db.FindUserByID(ts.ctx, gotUser.ID)
ts.NoError(err)
ts.Equal(*input.Email, dbUser.Email, "incorrect User Name in db")
})
}
}

0 comments on commit 952c9fe

Please sign in to comment.