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) {
|
func (h *SpaceSettingsHandler) JoinSpace(w http.ResponseWriter, r *http.Request) {
|
||||||
token := r.PathValue("token")
|
token := r.PathValue("token")
|
||||||
user := ctxkeys.User(r.Context())
|
user := ctxkeys.User(r.Context())
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ type SpaceRepository interface {
|
||||||
GetMembers(spaceID string) ([]*model.SpaceMemberWithProfile, error)
|
GetMembers(spaceID string) ([]*model.SpaceMemberWithProfile, error)
|
||||||
UpdateName(spaceID, name string) error
|
UpdateName(spaceID, name string) error
|
||||||
UpdateTimezone(spaceID, timezone string) error
|
UpdateTimezone(spaceID, timezone string) error
|
||||||
|
Delete(spaceID string) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type spaceRepository struct {
|
type spaceRepository struct {
|
||||||
|
|
@ -135,3 +136,9 @@ func (r *spaceRepository) UpdateTimezone(spaceID, timezone string) error {
|
||||||
_, err := r.db.Exec(query, timezone, time.Now(), spaceID)
|
_, err := r.db.Exec(query, timezone, time.Now(), spaceID)
|
||||||
return err
|
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)
|
spaceRouteLimited(mux, sa, cl, "DELETE /app/spaces/{spaceID}/invites/{token}", spaceSettings.CancelInvite)
|
||||||
spaceRoute(mux, sa, "GET /app/spaces/{spaceID}/settings/invites", spaceSettings.GetPendingInvites)
|
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, "POST /app/spaces/{spaceID}/invites", spaceSettings.CreateInvite)
|
||||||
|
spaceRouteLimited(mux, sa, cl, "DELETE /app/spaces/{spaceID}", spaceSettings.DeleteSpace)
|
||||||
|
|
||||||
// Loans
|
// Loans
|
||||||
spaceRoute(mux, sa, "GET /app/spaces/{spaceID}/loans", space.LoansPage)
|
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)
|
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>
|
</div>
|
||||||
@dialog.Script()
|
@dialog.Script()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue