Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions cmd/contacts.go
Original file line number Diff line number Diff line change
Expand Up @@ -286,3 +286,25 @@ func handleBlockContact(r *fastglue.Request) error {
}
return r.SendEnvelope(contact)
}


// handleDeleteContact soft deletes a contact.
func handleDeleteContact(r *fastglue.Request) error {
var (
app = r.Context.(*App)
contactID, _ = strconv.Atoi(r.RequestCtx.UserValue("id").(string))
auser = r.RequestCtx.UserValue("user").(amodels.User)
)

if contactID <= 0 {
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.Ts("globals.messages.invalid", "name", "`id`"), nil, envelope.InputError)
}

app.lo.Info("deleting contact", "contact_id", contactID, "actor_id", auser.ID)

if err := app.user.SoftDeleteContact(contactID); err != nil {
return sendErrorEnvelope(r, err)
}

return r.SendEnvelope(true)
}
1 change: 1 addition & 0 deletions cmd/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ func initHandlers(g *fastglue.Fastglue, hub *ws.Hub) {
g.GET("/api/v1/contacts/{id}", perm(handleGetContact, "contacts:read"))
g.PUT("/api/v1/contacts/{id}", perm(handleUpdateContact, "contacts:write"))
g.PUT("/api/v1/contacts/{id}/block", perm(handleBlockContact, "contacts:block"))
g.DELETE("/api/v1/contacts/{id}", perm(handleDeleteContact, "contacts:delete"))

// Contact notes.
g.GET("/api/v1/contacts/{id}/notes", perm(handleGetContactNotes, "contact_notes:read"))
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/api/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,7 @@ const updateRole = (id, data) =>
const deleteRole = (id) => http.delete(`/api/v1/roles/${id}`)
const getContacts = (params) => http.get('/api/v1/contacts', { params })
const getContact = (id) => http.get(`/api/v1/contacts/${id}`)
const deleteContact = (id) => http.delete(`/api/v1/contacts/${id}`)
const updateContact = (id, data) =>
http.put(`/api/v1/contacts/${id}`, data, {
headers: {
Expand Down Expand Up @@ -548,6 +549,7 @@ export default {
removeAssignee,
getContacts,
getContact,
deleteContact,
updateContact,
blockContact,
getCustomAttributes,
Expand Down
1 change: 1 addition & 0 deletions frontend/src/constants/permissions.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export const permissions = {
CONTACTS_READ: 'contacts:read',
CONTACTS_WRITE: 'contacts:write',
CONTACTS_BLOCK: 'contacts:block',
CONTACTS_DELETE: 'contacts:delete',
CONTACT_NOTES_READ: 'contact_notes:read',
CONTACT_NOTES_WRITE: 'contact_notes:write',
CONTACT_NOTES_DELETE: 'contact_notes:delete',
Expand Down
1 change: 1 addition & 0 deletions frontend/src/features/admin/roles/RoleForm.vue
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,7 @@ const permissions = ref([
{ name: perms.CONTACTS_READ, label: t('admin.role.contacts.read') },
{ name: perms.CONTACTS_WRITE, label: t('admin.role.contacts.write') },
{ name: perms.CONTACTS_BLOCK, label: t('admin.role.contacts.block') },
{ name: perms.CONTACTS_DELETE, label: t('admin.role.contacts.delete') },
{ name: perms.CONTACT_NOTES_READ, label: t('admin.role.contactNotes.read') },
{ name: perms.CONTACT_NOTES_WRITE, label: t('admin.role.contactNotes.write') },
{ name: perms.CONTACT_NOTES_DELETE, label: t('admin.role.contactNotes.delete') }
Expand Down
52 changes: 49 additions & 3 deletions frontend/src/views/contact/ContactDetailView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
{{ contact.created_at ? format(new Date(contact.created_at), 'PPP') : 'N/A' }}
</div>

<div class="w-30 pt-3">
<div class="flex gap-2 pt-3">
<Button
:variant="contact.enabled ? 'destructive' : 'outline'"
@click="showBlockConfirmation = true"
Expand All @@ -41,6 +41,15 @@
<ShieldCheckIcon v-else size="18" class="mr-2" />
{{ t(contact.enabled ? 'globals.messages.block' : 'globals.messages.unblock') }}
</Button>
<Button
v-if="userStore.can('contacts:delete')"
variant="destructive"
@click="showDeleteConfirmation = true"
size="sm"
>
<Trash2Icon size="18" class="mr-2" />
{{ t('globals.messages.delete') }}
</Button>
</div>
</div>

Expand Down Expand Up @@ -80,13 +89,34 @@
</div>
</DialogContent>
</Dialog>

<Dialog :open="showDeleteConfirmation" @update:open="showDeleteConfirmation = $event">
<DialogContent class="sm:max-w-md">
<DialogHeader>
<DialogTitle>
{{ t('globals.messages.delete', { name: t('globals.terms.contact') }) }}
</DialogTitle>
<DialogDescription>
Are you sure you want to delete this contact? This action cannot be undone.
</DialogDescription>
</DialogHeader>
<div class="flex justify-end space-x-2 pt-4">
<Button variant="outline" @click="showDeleteConfirmation = false">
{{ t('globals.messages.cancel') }}
</Button>
<Button variant="destructive" @click="confirmDelete">
{{ t('globals.messages.delete') }}
</Button>
</div>
</DialogContent>
</Dialog>
</div>
</ContactDetail>
</template>

<script setup>
import { ref, onMounted, computed } from 'vue'
import { useRoute } from 'vue-router'
import { useRoute, useRouter } from 'vue-router'
import { format } from 'date-fns'
import { useI18n } from 'vue-i18n'
import { useForm } from 'vee-validate'
Expand All @@ -101,7 +131,7 @@ import {
DialogDescription
} from '@/components/ui/dialog'
import { useUserStore } from '@/stores/user'
import { ShieldOffIcon, ShieldCheckIcon } from 'lucide-vue-next'
import { ShieldOffIcon, ShieldCheckIcon, Trash2Icon } from 'lucide-vue-next'
import ContactDetail from '@/layouts/contact/ContactDetail.vue'
import api from '@/api'
import ContactForm from '@/features/contact/ContactForm.vue'
Expand All @@ -116,9 +146,11 @@ import { Spinner } from '@/components/ui/spinner'
const { t } = useI18n()
const emitter = useEmitter()
const route = useRoute()
const router = useRouter()
const formLoading = ref(false)
const contact = ref(null)
const showBlockConfirmation = ref(false)
const showDeleteConfirmation = ref(false)
const userStore = useUserStore()

const form = useForm({
Expand Down Expand Up @@ -171,6 +203,20 @@ async function toggleBlock() {
}
}

async function confirmDelete() {
showDeleteConfirmation.value = false
try {
formLoading.value = true
await api.deleteContact(contact.value.id)
emitToast(t('globals.messages.deletedSuccessfully', { name: t('globals.terms.contact') }))
router.push('/contacts')
} catch (err) {
showError(err)
} finally {
formLoading.value = false
}
}

const onSubmit = form.handleSubmit(async (values) => {
try {
formLoading.value = true
Expand Down
1 change: 1 addition & 0 deletions i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -524,6 +524,7 @@
"admin.role.contacts.read": "View Contact Details",
"admin.role.contacts.write": "Edit Contact Details",
"admin.role.contacts.block": "Block Contacts",
"admin.role.contacts.delete": "Delete Contacts",
"admin.role.contactNotes.read": "View Contact Notes",
"admin.role.contactNotes.write": "Add Contact Notes",
"admin.role.contactNotes.delete": "Delete Contact Notes",
Expand Down
2 changes: 2 additions & 0 deletions internal/authz/models/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ const (
PermContactsRead = "contacts:read"
PermContactsWrite = "contacts:write"
PermContactsBlock = "contacts:block"
PermContactsDelete = "contacts:delete"

// Contact Notes
PermContactNotesRead = "contact_notes:read"
Expand Down Expand Up @@ -126,6 +127,7 @@ var validPermissions = map[string]struct{}{
PermContactsRead: {},
PermContactsWrite: {},
PermContactsBlock: {},
PermContactsDelete: {},
PermContactNotesRead: {},
PermContactNotesWrite: {},
PermContactNotesDelete: {},
Expand Down
9 changes: 9 additions & 0 deletions internal/user/contact.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,12 @@ func (u *Manager) GetContacts(page, pageSize int, order, orderBy string, filters
}
return u.GetAllUsers(page, pageSize, models.UserTypeContact, order, orderBy, filtersJSON)
}

// SoftDeleteContact soft deletes a contact by ID.
func (u *Manager) SoftDeleteContact(id int) error {
if _, err := u.q.SoftDeleteContact.Exec(id); err != nil {
u.lo.Error("error deleting contact", "error", err)
return envelope.NewError(envelope.GeneralError, u.i18n.Ts("globals.messages.errorDeleting", "name", "{globals.terms.contact}"), nil)
}
return nil
}
7 changes: 6 additions & 1 deletion internal/user/queries.sql
Original file line number Diff line number Diff line change
Expand Up @@ -267,4 +267,9 @@ WHERE id = $1;
-- name: update-api-key-last-used
UPDATE users
SET api_key_last_used_at = now()
WHERE id = $1;
WHERE id = $1;
-- name: soft-delete-contact
UPDATE users
SET deleted_at = now(), updated_at = now()
WHERE id = $1 AND type = 'contact'
RETURNING id;