Skip to content

Commit 3c63e73

Browse files
committed
Created repository
1 parent 8bb2d0a commit 3c63e73

13 files changed

Lines changed: 487 additions & 18 deletions

File tree

api/pkg/entities/auth_user.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package entities
2+
3+
// AuthUser is the user gotten from an auth request
4+
type AuthUser struct {
5+
ID UserID `json:"id"`
6+
Email string `json:"email"`
7+
}
8+
9+
// IsNoop checks if a user is empty
10+
func (user AuthUser) IsNoop() bool {
11+
return user.ID == "" || user.Email == ""
12+
}

api/pkg/entities/contact.go

Lines changed: 0 additions & 17 deletions
This file was deleted.

api/pkg/entities/user.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package entities
2+
3+
import (
4+
"time"
5+
6+
"github.com/google/uuid"
7+
)
8+
9+
// UserID is the ID of a user
10+
type UserID string
11+
12+
// User stores information about a user
13+
type User struct {
14+
ID UserID `json:"id" gorm:"primaryKey;type:string;" example:"WB7DRDWrJZRGbYrv2CKGkqbzvqdC"`
15+
Email string `json:"email" gorm:"uniqueIndex" example:"name@email.com"`
16+
APIKey string `json:"api_key" gorm:"uniqueIndex" example:"xyz"`
17+
ActivePhoneID *uuid.UUID `json:"active_phone_id" gorm:"type:uuid;" example:"32343a19-da5e-4b1b-a767-3298a73703cb"`
18+
CreatedAt time.Time `json:"created_at" example:"2022-06-05T14:26:02.302718+03:00"`
19+
UpdatedAt time.Time `json:"updated_at" example:"2022-06-05T14:26:10.303278+03:00"`
20+
}

api/pkg/handlers/handler.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ package handlers
33
import (
44
"net/url"
55

6+
"github.com/NdoleStudio/http-sms-manager/pkg/entities"
7+
"github.com/NdoleStudio/http-sms-manager/pkg/middlewares"
8+
69
"github.com/gofiber/fiber/v2"
710
)
811

@@ -24,6 +27,14 @@ func (h *handler) responseInternalServerError(c *fiber.Ctx) error {
2427
})
2528
}
2629

30+
func (h *handler) responseUnauthorized(c *fiber.Ctx) error {
31+
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
32+
"status": "error",
33+
"message": "You are not authorized to carry out this request.",
34+
"data": "Make sure your API key is set in the [X-API-Key] header in the request",
35+
})
36+
}
37+
2738
func (h *handler) responseForbidden(c *fiber.Ctx) error {
2839
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{
2940
"status": "error",
@@ -60,3 +71,14 @@ func (h *handler) pluralize(value string, count int) string {
6071
}
6172
return value + "s"
6273
}
74+
75+
func (h *handler) userFromContext(c *fiber.Ctx) entities.AuthUser {
76+
if tokenUser, ok := c.Locals(middlewares.ContextKeyAuthUserID).(entities.AuthUser); ok && !tokenUser.IsNoop() {
77+
return tokenUser
78+
}
79+
panic("user does not exist in context.")
80+
}
81+
82+
func (h *handler) userIDFomContext(c *fiber.Ctx) entities.UserID {
83+
return h.userFromContext(c).ID
84+
}

api/pkg/handlers/user_handler.go

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package handlers
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/NdoleStudio/http-sms-manager/pkg/services"
7+
"github.com/NdoleStudio/http-sms-manager/pkg/telemetry"
8+
"github.com/gofiber/fiber/v2"
9+
"github.com/palantir/stacktrace"
10+
)
11+
12+
// UserHandler handles user http requests.
13+
type UserHandler struct {
14+
handler
15+
logger telemetry.Logger
16+
tracer telemetry.Tracer
17+
service *services.UserService
18+
}
19+
20+
// NewUserHandler creates a new UserHandler
21+
func NewUserHandler(
22+
logger telemetry.Logger,
23+
tracer telemetry.Tracer,
24+
service *services.UserService,
25+
) (h *UserHandler) {
26+
return &UserHandler{
27+
logger: logger.WithService(fmt.Sprintf("%T", h)),
28+
tracer: tracer,
29+
service: service,
30+
}
31+
}
32+
33+
// RegisterRoutes registers the routes for the MessageHandler
34+
func (h *UserHandler) RegisterRoutes(router fiber.Router) {
35+
router.Get("/users/me", h.Show)
36+
}
37+
38+
// Show returns an entities.User
39+
// @Summary Get current user
40+
// @Description Get details of the currently authenticated user
41+
// @Security ApiKeyAuth
42+
// @Tags Messages
43+
// @Accept json
44+
// @Produce json
45+
// @Success 200 {object} responses.MessagesResponse
46+
// @Failure 400 {object} responses.BadRequest
47+
// @Failure 403 {object} responses.Unauthorized
48+
// @Failure 422 {object} responses.UnprocessableEntity
49+
// @Failure 500 {object} responses.InternalServerError
50+
// @Router /users/me [get]
51+
func (h *UserHandler) Show(c *fiber.Ctx) error {
52+
ctx, span := h.tracer.StartFromFiberCtx(c)
53+
defer span.End()
54+
55+
ctxLogger := h.tracer.CtxLogger(h.logger, span)
56+
57+
authUser := h.userFromContext(c)
58+
59+
user, err := h.service.Get(ctx, authUser)
60+
if err != nil {
61+
msg := fmt.Sprintf("cannot get user with ID [%s]", authUser.ID)
62+
ctxLogger.Error(stacktrace.Propagate(err, msg))
63+
return h.responseInternalServerError(c)
64+
}
65+
66+
return h.responseOK(c, "user fetched successfully", user)
67+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package middlewares
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/NdoleStudio/http-sms-manager/pkg/repositories"
7+
"github.com/NdoleStudio/http-sms-manager/pkg/telemetry"
8+
"github.com/gofiber/fiber/v2"
9+
"github.com/palantir/stacktrace"
10+
)
11+
12+
// APIKeyAuth authenticates a user from the X-API-Key header
13+
func APIKeyAuth(logger telemetry.Logger, tracer telemetry.Tracer, userRepository repositories.UserRepository) fiber.Handler {
14+
return func(c *fiber.Ctx) error {
15+
ctx, span := tracer.StartFromFiberCtx(c, "middlewares.APIKeyAuth")
16+
defer span.End()
17+
18+
ctxLogger := tracer.CtxLogger(logger, span)
19+
20+
apiKey := c.Get(authHeaderAPIKey)
21+
if len(apiKey) > 0 {
22+
span.AddEvent(fmt.Sprintf("the request header has no [%s] api key", authHeaderAPIKey))
23+
return c.Next()
24+
}
25+
26+
authUser, err := userRepository.LoadAuthUser(ctx, apiKey)
27+
if err != nil {
28+
ctxLogger.Error(stacktrace.Propagate(err, fmt.Sprintf("cannot load user with api key [%s]", apiKey)))
29+
return c.Next()
30+
}
31+
32+
c.Locals(ContextKeyAuthUserID, authUser)
33+
34+
ctxLogger.Info(fmt.Sprintf("[%s] set successfully for user with ID [%s]", authUser, authUser.ID))
35+
36+
return c.Next()
37+
}
38+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package middlewares
2+
3+
import (
4+
"github.com/NdoleStudio/http-sms-manager/pkg/entities"
5+
"github.com/NdoleStudio/http-sms-manager/pkg/telemetry"
6+
"github.com/gofiber/fiber/v2"
7+
)
8+
9+
const (
10+
authHeaderBearer = "Authorization"
11+
authHeaderAPIKey = "X-API-Key"
12+
bearerScheme = "Bearer"
13+
)
14+
15+
const (
16+
// ContextKeyAuthUserID is the context key used to store the ID of an authenticated user
17+
ContextKeyAuthUserID = "auth.user.id"
18+
)
19+
20+
// Authenticated checks if the request is authenticated
21+
func Authenticated(tracer telemetry.Tracer) fiber.Handler {
22+
return func(c *fiber.Ctx) error {
23+
_, span := tracer.StartFromFiberCtx(c, "middlewares.Authenticated")
24+
defer span.End()
25+
26+
if tokenUser, ok := c.Locals(ContextKeyAuthUserID).(entities.AuthUser); !ok || tokenUser.IsNoop() {
27+
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
28+
"status": "error",
29+
"message": "You are not authorized to carry out this request.",
30+
"data": "Make sure your API key is set in the [X-API-Key] header in the request",
31+
})
32+
}
33+
34+
return c.Next()
35+
}
36+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package middlewares
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"strings"
7+
8+
"firebase.google.com/go/auth"
9+
"github.com/NdoleStudio/http-sms-manager/pkg/entities"
10+
"github.com/NdoleStudio/http-sms-manager/pkg/telemetry"
11+
"github.com/gofiber/fiber/v2"
12+
"github.com/palantir/stacktrace"
13+
)
14+
15+
// BearerAuth authenticates a user based on the bearer token
16+
func BearerAuth(logger telemetry.Logger, tracer telemetry.Tracer, authClient *auth.Client) fiber.Handler {
17+
return func(c *fiber.Ctx) error {
18+
_, span := tracer.StartFromFiberCtx(c, "middlewares.BearerAuth")
19+
defer span.End()
20+
21+
authToken := c.Get(authHeaderBearer)
22+
if !strings.HasPrefix(authToken, bearerScheme) {
23+
span.AddEvent(fmt.Sprintf("The request header has no [%s] token", bearerScheme))
24+
return c.Next()
25+
}
26+
27+
if len(authToken) > len(bearerScheme)+1 {
28+
authToken = authToken[len(bearerScheme)+1:]
29+
}
30+
31+
ctxLogger := tracer.CtxLogger(logger, span)
32+
33+
token, err := authClient.VerifyIDToken(context.Background(), authToken)
34+
if err != nil {
35+
msg := fmt.Sprintf("invalid firebase id token %s", authToken)
36+
ctxLogger.Error(tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg)))
37+
return c.Next()
38+
}
39+
40+
span.AddEvent(fmt.Sprintf("[%s] token is valid", bearerScheme))
41+
42+
authUser := entities.AuthUser{
43+
ID: entities.UserID(token.Claims["user_id"].(string)),
44+
}
45+
46+
c.Locals(ContextKeyAuthUserID, authUser)
47+
48+
ctxLogger.Info(fmt.Sprintf("[%s] set successfully for user with ID [%s]", authUser, authUser.ID))
49+
return c.Next()
50+
}
51+
}

0 commit comments

Comments
 (0)