This commit is contained in:
parent
b40e693748
commit
6871b1f8c6
5 changed files with 134 additions and 0 deletions
|
|
@ -271,6 +271,42 @@ func (h *SpaceSettingsHandler) CreateInvite(w http.ResponseWriter, r *http.Reque
|
|||
}))
|
||||
}
|
||||
|
||||
func (h *SpaceSettingsHandler) DeleteSpace(w http.ResponseWriter, r *http.Request) {
|
||||
spaceID := r.PathValue("spaceID")
|
||||
user := ctxkeys.User(r.Context())
|
||||
|
||||
space, err := h.spaceService.GetSpace(spaceID)
|
||||
if err != nil {
|
||||
http.Error(w, "Space not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
if space.OwnerID != user.ID {
|
||||
http.Error(w, "Forbidden", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
if err := r.ParseForm(); err != nil {
|
||||
ui.RenderError(w, r, "Bad Request", http.StatusUnprocessableEntity)
|
||||
return
|
||||
}
|
||||
|
||||
confirmationName := r.FormValue("confirmation_name")
|
||||
if confirmationName != space.Name {
|
||||
ui.RenderError(w, r, "Space name does not match", http.StatusUnprocessableEntity)
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.spaceService.DeleteSpace(spaceID); err != nil {
|
||||
slog.Error("failed to delete space", "error", err, "space_id", spaceID)
|
||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("HX-Redirect", "/app/spaces")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
func (h *SpaceSettingsHandler) JoinSpace(w http.ResponseWriter, r *http.Request) {
|
||||
token := r.PathValue("token")
|
||||
user := ctxkeys.User(r.Context())
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ type SpaceRepository interface {
|
|||
GetMembers(spaceID string) ([]*model.SpaceMemberWithProfile, error)
|
||||
UpdateName(spaceID, name string) error
|
||||
UpdateTimezone(spaceID, timezone string) error
|
||||
Delete(spaceID string) error
|
||||
}
|
||||
|
||||
type spaceRepository struct {
|
||||
|
|
@ -135,3 +136,9 @@ func (r *spaceRepository) UpdateTimezone(spaceID, timezone string) error {
|
|||
_, err := r.db.Exec(query, timezone, time.Now(), spaceID)
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *spaceRepository) Delete(spaceID string) error {
|
||||
query := `DELETE FROM spaces WHERE id = $1;`
|
||||
_, err := r.db.Exec(query, spaceID)
|
||||
return err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -154,6 +154,7 @@ func SetupRoutes(a *app.App) http.Handler {
|
|||
spaceRouteLimited(mux, sa, cl, "DELETE /app/spaces/{spaceID}/invites/{token}", spaceSettings.CancelInvite)
|
||||
spaceRoute(mux, sa, "GET /app/spaces/{spaceID}/settings/invites", spaceSettings.GetPendingInvites)
|
||||
spaceRouteLimited(mux, sa, cl, "POST /app/spaces/{spaceID}/invites", spaceSettings.CreateInvite)
|
||||
spaceRouteLimited(mux, sa, cl, "DELETE /app/spaces/{spaceID}", spaceSettings.DeleteSpace)
|
||||
|
||||
// Loans
|
||||
spaceRoute(mux, sa, "GET /app/spaces/{spaceID}/loans", space.LoansPage)
|
||||
|
|
|
|||
|
|
@ -118,3 +118,8 @@ func (s *SpaceService) UpdateSpaceTimezone(spaceID, timezone string) error {
|
|||
}
|
||||
return s.spaceRepo.UpdateTimezone(spaceID, timezone)
|
||||
}
|
||||
|
||||
// DeleteSpace permanently deletes a space and all its associated data.
|
||||
func (s *SpaceService) DeleteSpace(spaceID string) error {
|
||||
return s.spaceRepo.Delete(spaceID)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -193,6 +193,91 @@ templ SpaceSettingsPage(space *model.Space, members []*model.SpaceMemberWithProf
|
|||
}
|
||||
}
|
||||
}
|
||||
// Danger Zone (owner only)
|
||||
if isOwner {
|
||||
@card.Card() {
|
||||
@card.Header() {
|
||||
@card.Title() {
|
||||
<div class="flex items-center gap-2 text-destructive">
|
||||
@icon.TriangleAlert(icon.Props{Class: "size-5"})
|
||||
Danger Zone
|
||||
</div>
|
||||
}
|
||||
@card.Description() {
|
||||
Irreversible and destructive actions.
|
||||
}
|
||||
}
|
||||
@card.Content() {
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-sm font-medium">Delete this space</p>
|
||||
<p class="text-sm text-muted-foreground">Once deleted, all data in this space will be permanently removed.</p>
|
||||
</div>
|
||||
{{ deleteDialogID := "delete-space-dialog-" + space.ID }}
|
||||
@dialog.Dialog(dialog.Props{ID: deleteDialogID, DisableClickAway: true}) {
|
||||
@dialog.Trigger() {
|
||||
@button.Button(button.Props{
|
||||
Variant: button.VariantDestructive,
|
||||
Type: button.TypeButton,
|
||||
}) {
|
||||
Delete Space
|
||||
}
|
||||
}
|
||||
@dialog.Content() {
|
||||
@dialog.Header() {
|
||||
@dialog.Title() {
|
||||
Delete space
|
||||
}
|
||||
@dialog.Description() {
|
||||
This action is permanent and cannot be undone. All data including expenses, budgets, shopping lists, and members will be permanently deleted.
|
||||
}
|
||||
}
|
||||
<form
|
||||
hx-delete={ "/app/spaces/" + space.ID }
|
||||
hx-swap="none"
|
||||
class="space-y-4 px-6 pb-6"
|
||||
>
|
||||
@csrf.Token()
|
||||
<div class="space-y-2">
|
||||
@label.Label(label.Props{For: "confirmation_name"}) {
|
||||
Type <span class="font-semibold">{ space.Name }</span> to confirm
|
||||
}
|
||||
@input.Input(input.Props{
|
||||
Name: "confirmation_name",
|
||||
Placeholder: space.Name,
|
||||
Attributes: templ.Attributes{
|
||||
"id": "confirmation_name",
|
||||
"autocomplete": "off",
|
||||
"_": "on input if my value equals '" + space.Name + "' remove @disabled from #delete-space-confirm else add @disabled to #delete-space-confirm",
|
||||
},
|
||||
})
|
||||
</div>
|
||||
<div class="flex justify-end gap-2">
|
||||
@dialog.Close() {
|
||||
@button.Button(button.Props{
|
||||
Variant: button.VariantOutline,
|
||||
Type: button.TypeButton,
|
||||
}) {
|
||||
Cancel
|
||||
}
|
||||
}
|
||||
@button.Button(button.Props{
|
||||
Variant: button.VariantDestructive,
|
||||
Attributes: templ.Attributes{
|
||||
"id": "delete-space-confirm",
|
||||
"disabled": true,
|
||||
},
|
||||
}) {
|
||||
Delete Space
|
||||
}
|
||||
</div>
|
||||
</form>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
}
|
||||
</div>
|
||||
@dialog.Script()
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue