Skip to content

Commit 235a9a2

Browse files
feat: added background job
1 parent c0cb5ea commit 235a9a2

File tree

16 files changed

+254
-62
lines changed

16 files changed

+254
-62
lines changed

cmd/api/api.go

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import (
1010
"khel/internal/mailer"
1111
"khel/internal/ratelimiter"
1212
"khel/internal/store"
13-
"log"
1413

1514
"net/http"
1615
"os"
@@ -82,14 +81,6 @@ type dbConfig struct {
8281
func (app *application) mount() http.Handler {
8382
r := chi.NewRouter()
8483

85-
//TODO:remove later
86-
r.Use(func(next http.Handler) http.Handler {
87-
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
88-
log.Printf("Incoming request: %s %s\n", r.Method, r.URL.Path)
89-
next.ServeHTTP(w, r)
90-
})
91-
})
92-
9384
r.Use(middleware.RequestID)
9485
r.Use(middleware.StripSlashes)
9586
r.Use(middleware.RealIP)
@@ -99,7 +90,7 @@ func (app *application) mount() http.Handler {
9990

10091
r.Use(cors.Handler(cors.Options{
10192
AllowedOrigins: []string{"https://*", "http://*"},
102-
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
93+
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"},
10394
AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token"},
10495
ExposedHeaders: []string{"Link"},
10596
AllowCredentials: false,
@@ -112,8 +103,8 @@ func (app *application) mount() http.Handler {
112103
r.Route("/v1", func(r chi.Router) {
113104
r.Get("/venue/{id}", app.getVenueDetailHandler)
114105
r.Get("/health", app.healthCheckHandler)
115-
docsURL := fmt.Sprintf("%s/swagger/doc.json", app.config.addr)
116-
r.Get("/swagger/*", httpSwagger.Handler(httpSwagger.URL(docsURL)))
106+
docsURL := fmt.Sprintf("%s/v1/swagger/doc.json", app.config.addr)
107+
r.With(app.BasicAuthMiddleware()).Get("/swagger/*", httpSwagger.Handler(httpSwagger.URL(docsURL)))
117108

118109
r.With(app.BasicAuthMiddleware()).Get("/debug/vars", expvar.Handler().ServeHTTP)
119110

cmd/api/auth.go

Lines changed: 36 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,26 @@ import (
1515
"github.com/google/uuid"
1616
)
1717

18+
// ErrorBadRequestResponse represents the standard error format for bad request API responses.
19+
//
20+
// @name ErrorBadRequestResponse
21+
// @description Standard error response format returned by all bad request API endpoints
22+
type ErrorBadRequestResponse struct {
23+
Success bool `json:"success" example:"false"`
24+
Message string `json:"message" example:"It show error from err.Error()"`
25+
Status int `json:"status" example:"400"`
26+
}
27+
28+
// ErrorInternalServerResponse represents the standard error format for internal server API responses.
29+
//
30+
// @name ErrorInternalServerResponse
31+
// @description Standard error response format returned by all internal server error API endpoints
32+
type ErrorInternalServerResponse struct {
33+
Success bool `json:"success" example:"false"`
34+
Message string `json:"message" example:"the server encountered a problem"`
35+
Status int `json:"status" example:"500"`
36+
}
37+
1838
type RegisterUserPayload struct {
1939
FirstName string `json:"first_name" validate:"required,max=50"`
2040
LastName string `json:"last_name" validate:"required,max=50"`
@@ -32,14 +52,16 @@ type UserWithToken struct {
3252
// registerUserHandler godoc
3353
//
3454
// @Summary Registers a user
35-
// @Description Registers a user
55+
// @Description Registers a user via Mobile App, Server will send activation url on email and need to click there to verify its your email
3656
// @Tags authentication
3757
// @Accept json
3858
// @Produce json
39-
// @Param payload body RegisterUserPayload true "User credentials"
40-
// @Success 201 {object} UserWithToken "User registered"
41-
// @Failure 400 {object} error
42-
// @Failure 500 {object} error
59+
// @Param payload body RegisterUserPayload true "User credentials"
60+
// @Success 201 {object} UserWithToken "User registered"
61+
//
62+
// @Failure 400 {object} ErrorBadRequestResponse "Bad request"
63+
// @Failure 500 {object} ErrorInternalServerResponse "Internal Server Error"
64+
//
4365
// @Router /authentication/user [post]
4466
func (app *application) registerUserHandler(w http.ResponseWriter, r *http.Request) {
4567
var payload RegisterUserPayload
@@ -89,7 +111,7 @@ func (app *application) registerUserHandler(w http.ResponseWriter, r *http.Reque
89111
Token: plainToken,
90112
}
91113

92-
activationURL := fmt.Sprintf("%s/confirm/%s", app.config.frontendURL, plainToken)
114+
activationURL := fmt.Sprintf("%s/confirm?token=%s", app.config.frontendURL, plainToken)
93115

94116
isProdEnv := app.config.env == "production"
95117
vars := struct {
@@ -396,7 +418,7 @@ func (app *application) requestResetPasswordHandler(w http.ResponseWriter, r *ht
396418
}
397419

398420
// Send reset email
399-
resetURL := fmt.Sprintf("%s/reset-password/%s", app.config.frontendURL, resetToken)
421+
resetURL := fmt.Sprintf("%s/reset-password/?token=%s", app.config.frontendURL, resetToken)
400422

401423
vars := struct {
402424
Username string
@@ -416,7 +438,10 @@ func (app *application) requestResetPasswordHandler(w http.ResponseWriter, r *ht
416438

417439
app.logger.Infow("Reset password email sent", "status code", status)
418440

419-
if err := app.jsonResponse(w, http.StatusOK, map[string]string{"message": "Reset token sent"}); err != nil {
441+
if err := app.jsonResponse(w, http.StatusOK, map[string]string{
442+
"message": "Reset token sent",
443+
"resetToken": resetToken, // unhashed token
444+
}); err != nil {
420445
app.internalServerError(w, r, err)
421446
}
422447
}
@@ -455,6 +480,7 @@ func (app *application) resetPasswordHandler(w http.ResponseWriter, r *http.Requ
455480
// Hash the token to compare with the stored hash
456481
hash := sha256.Sum256([]byte(payload.Token))
457482
hashToken := hex.EncodeToString(hash[:])
483+
fmt.Printf("this is hashToken: %s", hashToken)
458484

459485
// Get user by reset token
460486
user, err := app.store.Users.GetByResetToken(ctx, hashToken)
@@ -464,6 +490,7 @@ func (app *application) resetPasswordHandler(w http.ResponseWriter, r *http.Requ
464490
return
465491
}
466492
app.internalServerError(w, r, err)
493+
fmt.Println(err)
467494
return
468495
}
469496

@@ -490,6 +517,7 @@ func (app *application) resetPasswordHandler(w http.ResponseWriter, r *http.Requ
490517
// Save the updated user
491518
if err := app.store.Users.Update(ctx, user); err != nil {
492519
app.internalServerError(w, r, err)
520+
fmt.Println(err)
493521
return
494522
}
495523

cmd/api/background.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package main
2+
3+
import (
4+
"time"
5+
)
6+
7+
func (app *application) markCompletedGamesEvery30Mins() {
8+
go func() {
9+
ticker := time.NewTicker(30 * time.Minute)
10+
defer ticker.Stop()
11+
12+
// Run once immediately
13+
err := app.store.Games.MarkCompletedGames()
14+
if err != nil {
15+
app.logger.Errorf("Error marking games as completed: %v", err)
16+
} else {
17+
app.logger.Infof("Successfully marked games as completed at %s", time.Now().Format(time.RFC1123))
18+
}
19+
20+
// Then run every 30 minutes
21+
for range ticker.C {
22+
err := app.store.Games.MarkCompletedGames()
23+
if err != nil {
24+
app.logger.Errorf("Error marking games as completed: %v", err)
25+
} else {
26+
app.logger.Infof("Successfully marked games as completed at %s", time.Now().Format(time.RFC1123))
27+
}
28+
}
29+
}()
30+
}

cmd/api/errors.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,11 @@ func (app *application) unauthorizedErrorResponse(w http.ResponseWriter, r *http
3333
writeJSONError(w, http.StatusUnauthorized, err.Error())
3434
}
3535

36+
func (app *application) forbiddenErrorResponse(w http.ResponseWriter, r *http.Request, err error) {
37+
app.logger.Warnf("forbidden error", "method", r.Method, "path", r.URL.Path, "error", err.Error())
38+
writeJSONError(w, http.StatusForbidden, err.Error())
39+
}
40+
3641
func (app *application) unauthorizedBasicErrorResponse(w http.ResponseWriter, r *http.Request, err error) {
3742
app.logger.Warnf("unauthorized basic error", "method", r.Method, "path", r.URL.Path, "error", err.Error())
3843

cmd/api/games.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -435,10 +435,11 @@ func (app *application) AssignAssistantHandler(w http.ResponseWriter, r *http.Re
435435
func (app *application) getGamesHandler(w http.ResponseWriter, r *http.Request) {
436436
// Set default filter values.
437437
fq := store.GameFilterQuery{
438-
Limit: 10,
439-
Offset: 0,
440-
Sort: "asc",
441-
Radius: 0, // 0 means no location-based filtering.
438+
Limit: 10,
439+
Offset: 0,
440+
Sort: "asc",
441+
Radius: 0, // 0 means no location-based filtering.
442+
StartAfter: time.Now(),
442443
}
443444

444445
// Parse query parameters from the request (overriding defaults if provided).
@@ -456,7 +457,6 @@ func (app *application) getGamesHandler(w http.ResponseWriter, r *http.Request)
456457

457458
// Get the currently logged in user.
458459
user := getUserFromContext(r)
459-
fmt.Printf("the getGame userID is %d", user.ID)
460460

461461
// Query the database for matching games.
462462
games, err := app.store.Games.GetGames(r.Context(), fq)

cmd/api/main.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -82,9 +82,9 @@ var version = "1.2.0"
8282
// @title Khel API
8383
// @description API for Khel, a complete sport application.
8484

85-
// @contact.name API Support
86-
// @contact.url http://www.swagger.io/support
87-
// @contact.email support@swagger.io
85+
// @contact.name fullstacksherpa
86+
// @contact.url https://www.fullstacksherpa.tech/
87+
// @contact.email Ongchen10sherpa@gmail.com
8888

8989
// @license.name Apache 2.0
9090
// @license.url http://www.apache.org/licenses/LICENSE-2.0.html
@@ -213,6 +213,8 @@ func main() {
213213
return runtime.NumGoroutine()
214214
}))
215215

216+
app.markCompletedGamesEvery30Mins()
217+
216218
mux := app.mount()
217219

218220
logger.Fatal(app.run(mux))

docker-compose.yml

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,14 @@ services:
1515
ports:
1616
- "80:80"
1717
- "443:443"
18-
- "8080:8080"
1918
labels:
2019
- "traefik.enable=true"
2120
- "traefik.http.routers.traefik.rule=Host(`traefik.gocloudnepal.com`)"
2221
- "traefik.http.routers.traefik.entrypoints=websecure"
2322
- "traefik.http.routers.traefik.tls.certresolver=myresolver"
24-
- "traefik.http.services.traefik.loadbalancer.server.port=8080"
23+
- "traefik.http.routers.traefik.service=api@internal"
24+
- "traefik.http.routers.traefik.middlewares=dashboard-auth"
25+
- "traefik.http.middlewares.dashboard-auth.basicauth.users=admin:$2y$05$2hQXRr9Rpo8aJs0ERvDMauAwYEHPh7eSvTY.CTzLyBYKr.mkGInKO"
2526
volumes:
2627
- letsencrypt:/letsencrypt
2728
- /var/run/docker.sock:/var/run/docker.sock
@@ -31,8 +32,6 @@ services:
3132
build:
3233
context: .
3334
dockerfile: Dockerfile
34-
ports:
35-
- "8080:8080"
3635
labels:
3736
- "traefik.enable=true"
3837
- "traefik.http.routers.khel.rule=Host(`api.gocloudnepal.com`)"
@@ -57,6 +56,5 @@ services:
5756
- "--rolling-restart"
5857
volumes:
5958
- /var/run/docker.sock:/var/run/docker.sock
60-
6159
volumes:
6260
letsencrypt:

dockerfile

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ FROM debian:bookworm-slim
2828

2929
WORKDIR /app
3030

31+
# Install CA certificates (💥 this fixes TLS errors like x509 unknown authority)
32+
RUN apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/*
33+
34+
3135
# Create a non-root user and group
3236
RUN groupadd -r appuser && useradd -r -g appuser appuser
3337

docs/docs.go

Lines changed: 47 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@ const docTemplate = `{
1010
"description": "{{escape .Description}}",
1111
"title": "{{.Title}}",
1212
"contact": {
13-
"name": "API Support",
14-
"url": "http://www.swagger.io/support",
15-
"email": "support@swagger.io"
13+
"name": "fullstacksherpa",
14+
"url": "https://www.fullstacksherpa.tech/",
15+
"email": "Ongchen10sherpa@gmail.com"
1616
},
1717
"license": {
1818
"name": "Apache 2.0",
@@ -289,7 +289,7 @@ const docTemplate = `{
289289
},
290290
"/authentication/user": {
291291
"post": {
292-
"description": "Registers a user",
292+
"description": "Registers a user via Mobile App, Server will send activation url on email and need to click there to verify its your email",
293293
"consumes": [
294294
"application/json"
295295
],
@@ -319,12 +319,16 @@ const docTemplate = `{
319319
}
320320
},
321321
"400": {
322-
"description": "Bad Request",
323-
"schema": {}
322+
"description": "Bad request",
323+
"schema": {
324+
"$ref": "#/definitions/main.ErrorBadRequestResponse"
325+
}
324326
},
325327
"500": {
326328
"description": "Internal Server Error",
327-
"schema": {}
329+
"schema": {
330+
"$ref": "#/definitions/main.ErrorInternalServerResponse"
331+
}
328332
}
329333
}
330334
}
@@ -3483,6 +3487,42 @@ const docTemplate = `{
34833487
}
34843488
}
34853489
},
3490+
"main.ErrorBadRequestResponse": {
3491+
"description": "Standard error response format returned by all bad request API endpoints",
3492+
"type": "object",
3493+
"properties": {
3494+
"message": {
3495+
"type": "string",
3496+
"example": "It show error from err.Error()"
3497+
},
3498+
"status": {
3499+
"type": "integer",
3500+
"example": 400
3501+
},
3502+
"success": {
3503+
"type": "boolean",
3504+
"example": false
3505+
}
3506+
}
3507+
},
3508+
"main.ErrorInternalServerResponse": {
3509+
"description": "Standard error response format returned by all internal server error API endpoints",
3510+
"type": "object",
3511+
"properties": {
3512+
"message": {
3513+
"type": "string",
3514+
"example": "the server encountered a problem"
3515+
},
3516+
"status": {
3517+
"type": "integer",
3518+
"example": 500
3519+
},
3520+
"success": {
3521+
"type": "boolean",
3522+
"example": false
3523+
}
3524+
}
3525+
},
34863526
"main.HourlySlot": {
34873527
"type": "object",
34883528
"properties": {

0 commit comments

Comments
 (0)