Skip to content

Commit 4006fcb

Browse files
committed
/auth/login implemented
1 parent 81e7b3e commit 4006fcb

6 files changed

Lines changed: 164 additions & 0 deletions

File tree

server/controllers/auth.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,39 @@ func HandleRegister(w http.ResponseWriter, r *http.Request) {
4949
}
5050

5151
w.WriteHeader(http.StatusCreated)
52+
_ = json.NewEncoder(w).Encode(models.JSONResponse{
53+
Success: true,
54+
Data: map[string]interface{}{
55+
"user": user,
56+
"tokens": tokens,
57+
},
58+
})
59+
}
60+
61+
62+
type LoginReq struct {
63+
Username string `json:"username"`
64+
Password string `json:"password"`
65+
}
66+
67+
func HandleLogin(w http.ResponseWriter, r *http.Request) {
68+
w.Header().Set("Content-Type", "application/json")
69+
70+
var req LoginReq
71+
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
72+
w.WriteHeader(http.StatusBadRequest)
73+
_ = json.NewEncoder(w).Encode(models.JSONResponse{Success: false, Error: "Invalid json payload structure"})
74+
return
75+
}
76+
77+
svc := services.NewAuthService()
78+
user, tokens, err := svc.Login(r.Context(), req.Username, req.Password)
79+
if err != nil {
80+
w.WriteHeader(http.StatusUnauthorized)
81+
_ = json.NewEncoder(w).Encode(models.JSONResponse{Success: false, Error: err.Error()})
82+
return
83+
}
84+
5285
_ = json.NewEncoder(w).Encode(models.JSONResponse{
5386
Success: true,
5487
Data: map[string]interface{}{

server/middlewares/auth.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,48 @@
11
package middlewares
2+
3+
import (
4+
"context"
5+
"net/http"
6+
"strings"
7+
8+
"github.com/commandlinecoding/elephant/server/models"
9+
"github.com/commandlinecoding/elephant/server/services"
10+
"encoding/json"
11+
)
12+
13+
type contextKey string
14+
const UserIDKey contextKey = "userId"
15+
16+
func AuthGuard(next http.Handler) http.Handler {
17+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
18+
authHeader := r.Header.Get("Authorization")
19+
if authHeader == "" {
20+
respondUnauthorized(w)
21+
return
22+
}
23+
24+
parts := strings.Split(authHeader, " ")
25+
if len(parts) != 2 || strings.ToLower(parts[0]) != "bearer" {
26+
respondUnauthorized(w)
27+
return
28+
}
29+
30+
uid, err := services.VerifyAccessToken(parts[1])
31+
if err != nil {
32+
respondUnauthorized(w)
33+
return
34+
}
35+
36+
ctx := context.WithValue(r.Context(), UserIDKey, uid)
37+
next.ServeHTTP(w, r.WithContext(ctx))
38+
})
39+
}
40+
41+
func respondUnauthorized(w http.ResponseWriter) {
42+
w.Header().Set("Content-Type", "application/json")
43+
w.WriteHeader(http.StatusUnauthorized)
44+
_ = json.NewEncoder(w).Encode(models.JSONResponse{
45+
Success: false,
46+
Error: "Access token missing, expired or malformed",
47+
})
48+
}

server/repository/user.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,3 +49,23 @@ func (r *UserRepository) UsernameExists(ctx context.Context, username string) (b
4949

5050
return exists, nil
5151
}
52+
53+
func (r *UserRepository) FindByUsername(ctx context.Context, username string) (*models.User, error) {
54+
query := `
55+
SELECT id, username, display_name, password_hash, created_at
56+
FROM users
57+
WHERE username = $1;
58+
`
59+
var user models.User
60+
err := config.DB.QueryRow(ctx, query, username).Scan(
61+
&user.ID,
62+
&user.Username,
63+
&user.DisplayName,
64+
&user.PasswordHash,
65+
&user.CreatedAt,
66+
)
67+
if err != nil {
68+
return nil, err
69+
}
70+
return &user, nil
71+
}

server/routes/auth.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,5 @@ import (
77

88
func AuthRoute(router chi.Router) {
99
router.Post("/register", controllers.HandleRegister)
10+
router.Post("/login", controllers.HandleLogin)
1011
}

server/services/auth.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,39 @@
11
package services
2+
3+
import (
4+
"context"
5+
"errors"
6+
"strings"
7+
8+
"github.com/commandlinecoding/elephant/server/models"
9+
"github.com/commandlinecoding/elephant/server/repository"
10+
)
11+
12+
type AuthService struct {
13+
repo *repository.UserRepository
14+
}
15+
16+
func NewAuthService() *AuthService {
17+
return &AuthService{repo: repository.NewUserRepository()}
18+
}
19+
20+
func (s *AuthService) Login(ctx context.Context, username, password string) (*models.User, *TokenPair, error) {
21+
username = strings.ToLower(strings.TrimSpace(username))
22+
23+
user, err := s.repo.FindByUsername(ctx, username)
24+
if err != nil {
25+
return nil, nil, errors.New("invalid username or password credentials")
26+
}
27+
28+
match, err := VerifyPassword(password, user.PasswordHash)
29+
if err != nil || !match {
30+
return nil, nil, errors.New("invalid username or password credentials")
31+
}
32+
33+
tokens, err := GenerateTokenPair(user.ID)
34+
if err != nil {
35+
return nil, nil, errors.New("failed to issue tokens")
36+
}
37+
38+
return user, tokens, nil
39+
}

server/services/jwt.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package services
22

33
import (
4+
"fmt"
45
"time"
56

67
"github.com/commandlinecoding/elephant/server/env"
@@ -42,3 +43,27 @@ func GenerateTokenPair(uid string) (*TokenPair, error) {
4243
RefreshToken: rtkStr,
4344
}, nil
4445
}
46+
47+
// VerifyAccessToken
48+
func VerifyAccessToken(tokenStr string) (string, error) {
49+
token, err := jwt.Parse(tokenStr, func(token *jwt.Token) (interface{}, error) {
50+
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
51+
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
52+
}
53+
return []byte(env.JWT_SECRET), nil
54+
})
55+
56+
if err != nil {
57+
return "", err
58+
}
59+
60+
if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
61+
uid, ok := claims["sub"].(string)
62+
if !ok {
63+
return "", fmt.Errorf("invalid token claim structure")
64+
}
65+
return uid, nil
66+
}
67+
68+
return "", fmt.Errorf("invalid token status")
69+
}

0 commit comments

Comments
 (0)