Skip to content

Commit

Permalink
feat(coldmfa): Move codes between groups
Browse files Browse the repository at this point in the history
  • Loading branch information
ThetaSinner committed Oct 13, 2024
1 parent 1fec8b4 commit d9554f3
Show file tree
Hide file tree
Showing 6 changed files with 210 additions and 3 deletions.
55 changes: 55 additions & 0 deletions coldmfa/app/src/components/CodeSummaryLine.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ describe('CodeSummaryLine', () => {
let client: AxiosInstance
let pinia: Pinia
let groupId: string
let otherGroupId: string
let codeId: string

const Host = {
Expand Down Expand Up @@ -67,6 +68,15 @@ describe('CodeSummaryLine', () => {
const groupsStore = useGroupsStore()
groupsStore.insertGroup(resp.data)

const otherGroupResp: AxiosResponse<CodeGroup> = await client.post(
'http://127.0.0.1:3000/coldmfa/api/groups',
{
name: 'Other Test Group'
}
)
otherGroupId = otherGroupResp.data.groupId
groupsStore.insertGroup(otherGroupResp.data)

const codeResp: AxiosResponse<CodeSummary> = await client.post(
`http://127.0.0.1:3000/coldmfa/api/groups/${groupId}/codes`,
{
Expand Down Expand Up @@ -232,4 +242,49 @@ describe('CodeSummaryLine', () => {
const code = groupsStore.codeById(groupId, codeId)
expect(code?.deleted).toBe(true)
})

it('move between groups', async () => {
const wrapper = mount(Host, {
props: {
groupId,
codeId
},
global: {
provide: {
client
}
}
})

expect(wrapper.html()).toContain('EphyraSoftware:test-a')

await wrapper.get('button[data-test-id="move"]').trigger('click')

const otherGroups = wrapper.findAll('li')
expect(otherGroups.length).toBe(1)
expect(otherGroups[0].html()).toContain('Other Test Group')
expect(otherGroups[0].isVisible()).toBe(true)

// Select the group to switch to and click it
// That triggers a move request to the backend and should update the store
await otherGroups[0].trigger('click')
await flushPromises()

expect(wrapper.html()).not.toContain('EphyraSoftware:test-a')

// Switch to the other group, where the code should have moved to
await wrapper.setProps({
groupId: otherGroupId
})

// Now it's showing again
expect(wrapper.html()).toContain('EphyraSoftware:test-a')

// Check the state was correctly updated in the store
const groupsStore = useGroupsStore()
const removedCode = groupsStore.codeById(groupId, codeId)
expect(removedCode).toBe(undefined)
const code = groupsStore.codeById(otherGroupId, codeId)
expect(code?.codeId).toBe(codeId)
})
})
59 changes: 59 additions & 0 deletions coldmfa/app/src/components/CodeSummaryLine.vue
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,12 @@ defineEmits<{
const client = inject<AxiosInstance>('client') as AxiosInstance
const groupsStore = useGroupsStore()
const fetchedCode = ref<PasscodeResponse | undefined>()
const moveCodeModalOpen = ref(false)
const codeName = useTemplateRef<HTMLParagraphElement>('codeName')
const code = computed(() => groupsStore.codeById(props.groupId, props.codeId))
const otherGroups = computed(() => groupsStore.groups.filter((g) => g.groupId !== props.groupId))
const deleteCounter = ref(5)
Expand Down Expand Up @@ -110,6 +112,35 @@ const tryDelete = async () => {
}
}
}
const showMoveCode = () => {
moveCodeModalOpen.value = true
}
const moveCode = async (groupId: string) => {
if (!code.value || !groupId) {
return
}
try {
await client.post(
`api/groups/${props.groupId}/codes/${code.value.codeId}/move`,
{
toGroupId: groupId
},
{
validateStatus: (status) => status === 204
}
)
groupsStore.addCodeToGroup(groupId, code.value)
groupsStore.removeCodeFromGroup(props.groupId, code.value.codeId)
} catch (e) {
console.error(e)
}
moveCodeModalOpen.value = false
}
</script>

<template>
Expand Down Expand Up @@ -155,6 +186,9 @@ const tryDelete = async () => {
>
Export
</button>
<button class="btn btn-secondary join-item" data-test-id="move" @click="showMoveCode">
Move
</button>
<button
class="btn btn-primary join-item"
@click="getCode"
Expand All @@ -174,6 +208,31 @@ const tryDelete = async () => {
</div>
</div>
</div>

<dialog class="modal" :open="moveCodeModalOpen">
<div class="modal-box">
<h3 class="text-lg font-bold">Pick another group</h3>
<div class="ms-5 mt-3">
<ul class="list-disc">
<li
v-for="group in otherGroups"
:key="group.groupId"
class="cursor-pointer"
@click="moveCode(group.groupId)"
>
{{ group.name }}
</li>
</ul>
</div>

<div class="modal-action">
<form method="dialog">
<!-- if there is a button in a form, it will close the modal -->
<button class="btn">Cancel</button>
</form>
</div>
</div>
</dialog>
</template>

<style scoped></style>
13 changes: 12 additions & 1 deletion coldmfa/app/src/stores/groups.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,16 @@ export const useGroupsStore = defineStore('groups', () => {
return group && group.codes
}

const removeCodeFromGroup = (groupId: string, codeId: string) => {
const group = groups.value.find((g) => g.groupId === groupId)
if (group && group.codes) {
const codeIndex = group.codes.findIndex((c) => c.codeId === codeId)
if (codeIndex !== -1) {
group.codes.splice(codeIndex, 1)
}
}
}

return {
groups,
insertGroup,
Expand All @@ -71,7 +81,8 @@ export const useGroupsStore = defineStore('groups', () => {
groupHasCodes,
groupById,
codeById,
groupCodes
groupCodes,
removeCodeFromGroup
}
})

Expand Down
33 changes: 33 additions & 0 deletions coldmfa/app/src/support/httpMock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,39 @@ const handlers = [
}
),

http.post(
'http://127.0.0.1:3000/coldmfa/api/groups/:groupId/codes/:codeId/move',
async ({ request, params }) => {
if (nextIsHttpErr) {
nextIsHttpErr = false
return HttpResponse.json({ error: 'A test error' }, { status: 500 })
}

const groupId = params['groupId'] as string
const codeId = params['codeId'] as string

const req = (await request.json()) as Record<string, string>
const toGroupId = req['toGroupId']

if (!data[groupId] || !data[toGroupId]) {
return HttpResponse.json({ error: 'Group not found' }, { status: 404 })
}

if (!data[groupId].codes) {
data[groupId].codes = []
}

if (!data[toGroupId].codes) {
data[toGroupId].codes = []
}

data[groupId].codes = data[groupId].codes!.filter((code) => code.codeId !== codeId)
data[toGroupId].codes!.push(data[groupId].codes!.find((code) => code.codeId === codeId)!)

return new HttpResponse(null, { status: 204 })
}
),

http.delete(
'http://127.0.0.1:3000/coldmfa/api/groups/:groupId/codes/:codeId',
async ({ params }) => {
Expand Down
49 changes: 47 additions & 2 deletions coldmfa/coldmfa.go
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,51 @@ func (a *App) Prepare() {
return c.SendStatus(http.StatusNoContent)
})

api.Post("/groups/:groupId/codes/:codeId/move", func(c *fiber.Ctx) error {
sessionId := auth.SessionId(c)
if sessionId == "" {
return c.SendStatus(http.StatusUnauthorized)
}

currentGroupId := c.Params("groupId")
if currentGroupId == "" {
return c.Status(http.StatusBadRequest).JSON(ApiError{Error: "missing groupId"})
}

codeId := c.Params("codeId")
if codeId == "" {
return c.Status(http.StatusBadRequest).JSON(ApiError{Error: "missing codeId"})
}

moveCodeRequest := new(MoveCodeRequest)
if err := c.BodyParser(moveCodeRequest); err != nil {
return c.Status(http.StatusBadRequest).JSON(ApiError{Error: "invalid request"})
}

if moveCodeRequest.ToGroupId == "" {
return c.Status(http.StatusBadRequest).JSON(ApiError{Error: "missing toGroupId"})
}

result, err := db.ExecContext(c.Context(), "update code set code_group_id = (select id from code_group where owner_id = $1 and group_id = $2) where deleted = false and code_group_id = (select id from code_group where owner_id = $1 and group_id = $3) and code_id = $4", sessionId, moveCodeRequest.ToGroupId, currentGroupId, codeId)

if err != nil {
log.Errorf("failed to move code: %s", err.Error())
return c.Status(http.StatusInternalServerError).JSON(ApiError{Error: "database error"})
}

rowsAffected, err := result.RowsAffected()
if err != nil {
log.Errorf("failed to update code: %s", err.Error())
return c.Status(http.StatusInternalServerError).JSON(ApiError{Error: "database error"})
}

if rowsAffected == 0 {
return c.Status(http.StatusNotFound).JSON(ApiError{Error: "code or target group not found"})
}

return c.SendStatus(http.StatusNoContent)
})

api.Delete("/groups/:groupId/codes/:codeId", func(c *fiber.Ctx) error {
sessionId := auth.SessionId(c)
if sessionId == "" {
Expand Down Expand Up @@ -712,8 +757,8 @@ func extractOtpAuthUrl(raw string) (*OtpConfig, error) {
if err != nil || num < 0 {
return nil, fmt.Errorf("invalid period")
}
unsigned_num := uint(num)
out.Period = &unsigned_num
unsignedNum := uint(num)
out.Period = &unsignedNum
}

return &out, nil
Expand Down
4 changes: 4 additions & 0 deletions coldmfa/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,7 @@ type BackupWarning struct {
LastBackupAt *time.Time `json:"lastBackupAt"`
NumberNotBackedUp int `json:"numberNotBackedUp"`
}

type MoveCodeRequest struct {
ToGroupId string `json:"toGroupId"`
}

0 comments on commit d9554f3

Please sign in to comment.