Skip to content

Commit be50df4

Browse files
feat: add user account deletion
1 parent 3dcab91 commit be50df4

File tree

9 files changed

+184
-23
lines changed

9 files changed

+184
-23
lines changed

.air.toml

Lines changed: 19 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,29 @@
1+
# Root directory for Air to watch
12
root = "."
23
testdata_dir = "testdata"
34
tmp_dir = "bin"
45

56
[build]
6-
args_bin = []
7+
# Output binary location
78
bin = "./bin/main"
9+
# Build command for your API
810
cmd = "go build -o ./bin/main ./cmd/api"
11+
# Delay after file change before rebuilding (ms)
912
delay = 1000
13+
14+
# Directories and files to ignore
1015
exclude_dir = ["assets", "bin", "vendor", "testdata", "web", "docs", "scripts"]
11-
exclude_file = []
12-
exclude_regex = ["_test.go"]
16+
exclude_regex = ["_test.go"] # ignore test files
1317
exclude_unchanged = false
14-
follow_symlink = false
15-
full_bin = ""
16-
include_dir = []
18+
19+
# Extensions to watch
1720
include_ext = ["go", "tpl", "tmpl", "html"]
18-
include_file = []
19-
kill_delay = "0s"
20-
log = "build-errors.log"
21-
poll = false
22-
poll_interval = 0
23-
post_cmd = []
21+
22+
# Run before build (auto‑generate docs)
2423
pre_cmd = ["make gen-docs"]
25-
rerun = false
26-
rerun_delay = 500
27-
send_interrupt = false
28-
stop_on_error = false
24+
25+
# Log build errors here
26+
log = "build-errors.log"
2927

3028
[color]
3129
app = ""
@@ -43,4 +41,8 @@ tmp_dir = "bin"
4341

4442
[screen]
4543
clear_on_rebuild = false
46-
keep_scroll = true
44+
keep_scroll = true
45+
46+
[env]
47+
# Default environment for local dev
48+
APP_ENV = "development"

Makefile

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
# Environment Configuration
33
# -------------------------------
44
MIGRATIONS_PATH = ./cmd/migrate/migrations
5-
ENV ?= staging # Default to staging if not specified
5+
ENV ?= development # Default to development if not specified
66

77
# Debug output showing which environment is being loaded
88
$(info Loading $(ENV) environment...)
@@ -11,15 +11,18 @@ $(info Loading $(ENV) environment...)
1111
ifeq ($(ENV),prod)
1212
include .env.prod
1313
$(info Using production database configuration)
14-
else
14+
else ifeq ($(ENV),staging)
1515
include .env.staging
1616
$(info Using staging database configuration)
17+
else
18+
include .env.development
19+
$(info Using development database configuration)
1720
endif
1821

1922
# -------------------------------
2023
# Migration Targets
2124
# -------------------------------
22-
.PHONY: migrate-create
25+
.PHONY: migration
2326
migration:
2427
@migrate create -seq -ext sql -dir $(MIGRATIONS_PATH) $(filter-out $@,$(MAKECMDGOALS))
2528

@@ -52,6 +55,14 @@ staging-up:
5255
staging-down:
5356
@$(MAKE) migrate-down ENV=staging
5457

58+
.PHONY: dev-up
59+
dev-up:
60+
@$(MAKE) migrate-up ENV=development
61+
62+
.PHONY: dev-down
63+
dev-down:
64+
@$(MAKE) migrate-down ENV=development
65+
5566
# -------------------------------
5667
# Other Commands
5768
# -------------------------------
@@ -75,4 +86,4 @@ gen-docs:
7586
show-env:
7687
@echo "Current Environment: $(ENV)"
7788
@echo "DB Connection: $(DB_ADDR_NO_POOL)"
78-
@echo "Migrations Path: $(MIGRATIONS_PATH)"
89+
@echo "Migrations Path: $(MIGRATIONS_PATH)"

cmd/api/api.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,7 @@ func (app *application) mount() http.Handler {
160160
r.Use(app.AuthTokenMiddleware)
161161
r.Get("/bookings", app.getBookingsByUserHandler)
162162
r.Get("/me", app.getCurrentUserHandler)
163+
r.Delete("/me", app.deleteUserAccountHandler)
163164
r.Patch("/update-profile", app.editProfileHandler)
164165
r.Put("/", app.updateUserHandler)
165166
r.Post("/profile-picture", app.uploadProfilePictureHandler)

cmd/api/main.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,9 +96,13 @@ var version = "1.2.0"
9696
// @name Authorization
9797
// @description
9898
func main() {
99-
if err := godotenv.Load(); err != nil {
100-
fmt.Println("No .env file found, relying on environment variables from Docker")
99+
env := os.Getenv("APP_ENV")
100+
if env == "" {
101+
env = "development"
101102
}
103+
envFile := fmt.Sprintf(".env.%s", env)
104+
godotenv.Load(envFile)
105+
fmt.Println("Running in", env, "mode")
102106
// Retrieve and convert maxOpenConns
103107
maxOpenConnsStr := os.Getenv("DB_MAX_OPEN_CONNS")
104108
maxOpenConns, err := strconv.Atoi(maxOpenConnsStr)

cmd/api/users.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -458,3 +458,44 @@ func (app *application) getCurrentUserHandler(w http.ResponseWriter, r *http.Req
458458
app.internalServerError(w, r, err)
459459
}
460460
}
461+
462+
// deleteUserAccountHandler godoc
463+
//
464+
// @Summary Delete current user account
465+
// @Description Deletes the logged-in user's account and Cloudinary profile photo
466+
// @Tags users
467+
// @Produce json
468+
// @Success 204 {string} string "User deleted"
469+
// @Failure 401 {object} error "Unauthorized"
470+
// @Failure 500 {object} error "Internal server error"
471+
// @Security ApiKeyAuth
472+
// @Router /users/me [delete]
473+
func (app *application) deleteUserAccountHandler(w http.ResponseWriter, r *http.Request) {
474+
user := getUserFromContext(r)
475+
if user == nil {
476+
http.Error(w, "unauthorized", http.StatusUnauthorized)
477+
return
478+
}
479+
480+
// Delete profile picture if one exists
481+
if user.ProfilePictureURL.Valid {
482+
err := app.deletePhotoFromCloudinary(user.ProfilePictureURL.String)
483+
if err != nil {
484+
app.logger.Error(err, nil) // Log failure, don't block deletion
485+
} else {
486+
app.logger.Info("Successfully deleted profile picture from Cloudinary", map[string]any{
487+
"user_id": user.ID,
488+
"url": user.ProfilePictureURL.String,
489+
})
490+
}
491+
}
492+
493+
// Delete user from DB
494+
err := app.store.Users.Delete(r.Context(), user.ID)
495+
if err != nil {
496+
app.internalServerError(w, r, err)
497+
return
498+
}
499+
500+
w.WriteHeader(http.StatusNoContent)
501+
}

docs/docs.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1799,6 +1799,37 @@ const docTemplate = `{
17991799
"schema": {}
18001800
}
18011801
}
1802+
},
1803+
"delete": {
1804+
"security": [
1805+
{
1806+
"ApiKeyAuth": []
1807+
}
1808+
],
1809+
"description": "Deletes the logged-in user's account and Cloudinary profile photo",
1810+
"produces": [
1811+
"application/json"
1812+
],
1813+
"tags": [
1814+
"users"
1815+
],
1816+
"summary": "Delete current user account",
1817+
"responses": {
1818+
"204": {
1819+
"description": "User deleted",
1820+
"schema": {
1821+
"type": "string"
1822+
}
1823+
},
1824+
"401": {
1825+
"description": "Unauthorized",
1826+
"schema": {}
1827+
},
1828+
"500": {
1829+
"description": "Internal server error",
1830+
"schema": {}
1831+
}
1832+
}
18021833
}
18031834
},
18041835
"/users/profile-picture": {

docs/swagger.json

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1791,6 +1791,37 @@
17911791
"schema": {}
17921792
}
17931793
}
1794+
},
1795+
"delete": {
1796+
"security": [
1797+
{
1798+
"ApiKeyAuth": []
1799+
}
1800+
],
1801+
"description": "Deletes the logged-in user's account and Cloudinary profile photo",
1802+
"produces": [
1803+
"application/json"
1804+
],
1805+
"tags": [
1806+
"users"
1807+
],
1808+
"summary": "Delete current user account",
1809+
"responses": {
1810+
"204": {
1811+
"description": "User deleted",
1812+
"schema": {
1813+
"type": "string"
1814+
}
1815+
},
1816+
"401": {
1817+
"description": "Unauthorized",
1818+
"schema": {}
1819+
},
1820+
"500": {
1821+
"description": "Internal server error",
1822+
"schema": {}
1823+
}
1824+
}
17941825
}
17951826
},
17961827
"/users/profile-picture": {

docs/swagger.yaml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2156,6 +2156,26 @@ paths:
21562156
tags:
21572157
- authentication
21582158
/users/me:
2159+
delete:
2160+
description: Deletes the logged-in user's account and Cloudinary profile photo
2161+
produces:
2162+
- application/json
2163+
responses:
2164+
"204":
2165+
description: User deleted
2166+
schema:
2167+
type: string
2168+
"401":
2169+
description: Unauthorized
2170+
schema: {}
2171+
"500":
2172+
description: Internal server error
2173+
schema: {}
2174+
security:
2175+
- ApiKeyAuth: []
2176+
summary: Delete current user account
2177+
tags:
2178+
- users
21592179
get:
21602180
consumes:
21612181
- application/json

test.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
6+
"golang.org/x/crypto/bcrypt"
7+
)
8+
9+
func main() {
10+
11+
storedHash := "\x2432612431302436327a5969635470397872534f37473354737567374f596641436c6479573071336c4c474b736f443854536466497a30596b6c696d"
12+
plain := "khel"
13+
14+
err := bcrypt.CompareHashAndPassword([]byte(storedHash), []byte(plain))
15+
if err != nil {
16+
fmt.Println("FAIL:", err)
17+
} else {
18+
fmt.Println("SUCCESS")
19+
}
20+
}

0 commit comments

Comments
 (0)