Compare commits
9 Commits
cb91a10192
...
eba6253ff6
Author | SHA1 | Date | |
---|---|---|---|
eba6253ff6
|
|||
ef7e1a80d9
|
|||
854e1b44a9
|
|||
607174110c
|
|||
c283998403
|
|||
986ca8e353
|
|||
20d9947642
|
|||
97e15e1b1e
|
|||
d50bd6c4f5
|
@ -7,6 +7,8 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"gitea.local/admin/hspguard/internal/auth"
|
||||
imiddleware "gitea.local/admin/hspguard/internal/middleware"
|
||||
"gitea.local/admin/hspguard/internal/repository"
|
||||
"gitea.local/admin/hspguard/internal/user"
|
||||
"github.com/go-chi/chi/v5"
|
||||
@ -34,8 +36,13 @@ func (s *APIServer) Run() error {
|
||||
FileServer(router, "/static", staticDir)
|
||||
|
||||
router.Route("/api/v1", func(r chi.Router) {
|
||||
r.Use(imiddleware.WithSkipper(imiddleware.AuthMiddleware, "/api/v1/login", "/api/v1/register"))
|
||||
|
||||
userHandler := user.NewUserHandler(s.repo)
|
||||
userHandler.RegisterRoutes(router, r)
|
||||
|
||||
authHandler := auth.NewAuthHandler(s.repo)
|
||||
authHandler.RegisterRoutes(router, r)
|
||||
})
|
||||
|
||||
// Handle unknown routes
|
||||
@ -48,7 +55,7 @@ func (s *APIServer) Run() error {
|
||||
router.MethodNotAllowed(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||
fmt.Fprint(w, `{"error": "405 - method not allowed"}`)
|
||||
_, _ = fmt.Fprint(w, `{"error": "405 - method not allowed"}`)
|
||||
})
|
||||
|
||||
log.Println("Listening on", s.addr)
|
||||
|
@ -7,6 +7,7 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"gitea.local/admin/hspguard/internal/types"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
)
|
||||
|
||||
@ -63,12 +64,13 @@ func SignJwtToken(claims jwt.Claims) (string, error) {
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func VerifyToken(token string, claims jwt.Claims) (*jwt.Token, error) {
|
||||
func VerifyToken(token string) (*jwt.Token, *types.UserClaims, error) {
|
||||
publicKey, err := parseBase64PublicKey("JWT_PUBLIC_KEY")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
claims := &types.UserClaims{}
|
||||
parsed, err := jwt.ParseWithClaims(token, claims, func(t *jwt.Token) (any, error) {
|
||||
if _, ok := t.Method.(*jwt.SigningMethodECDSA); !ok {
|
||||
return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"])
|
||||
@ -77,13 +79,13 @@ func VerifyToken(token string, claims jwt.Claims) (*jwt.Token, error) {
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid token: %w", err)
|
||||
return nil, nil, fmt.Errorf("invalid token: %w", err)
|
||||
}
|
||||
|
||||
if !parsed.Valid {
|
||||
return nil, fmt.Errorf("token is not valid")
|
||||
return nil, nil, fmt.Errorf("token is not valid")
|
||||
}
|
||||
|
||||
return parsed, nil
|
||||
return parsed, claims, nil
|
||||
}
|
||||
|
||||
|
@ -1,8 +1,18 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"gitea.local/admin/hspguard/internal/repository"
|
||||
"gitea.local/admin/hspguard/internal/types"
|
||||
"gitea.local/admin/hspguard/internal/util"
|
||||
"gitea.local/admin/hspguard/internal/web"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type AuthHandler struct {
|
||||
@ -16,6 +26,87 @@ func NewAuthHandler(repo *repository.Queries) *AuthHandler {
|
||||
}
|
||||
|
||||
func (h *AuthHandler) RegisterRoutes(router chi.Router, api chi.Router) {
|
||||
|
||||
api.Get("/profile", h.getProfile)
|
||||
api.Post("/login", h.login)
|
||||
}
|
||||
|
||||
func (h *AuthHandler) getProfile(w http.ResponseWriter, r *http.Request) {
|
||||
userId, ok := util.GetRequestUserId(r.Context())
|
||||
if !ok {
|
||||
web.Error(w, "failed to get user id from auth session", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
user, err := h.repo.FindUserId(r.Context(), uuid.MustParse(userId))
|
||||
if err != nil {
|
||||
web.Error(w, "user with provided id does not exist", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
if err := json.NewEncoder(w).Encode(map[string]any{
|
||||
"full_name": user.FullName,
|
||||
"email": user.Email,
|
||||
"phoneNumber": user.PhoneNumber,
|
||||
"isAdmin": user.IsAdmin,
|
||||
"last_login": user.LastLogin,
|
||||
"updated_at": user.UpdatedAt,
|
||||
"created_at": user.CreatedAt,
|
||||
}); err != nil {
|
||||
web.Error(w, "failed to encode user profile", http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
type LoginParams struct {
|
||||
Email string `json:"email"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
func (h *AuthHandler) login(w http.ResponseWriter, r *http.Request) {
|
||||
var params LoginParams
|
||||
|
||||
decoder := json.NewDecoder(r.Body)
|
||||
if err := decoder.Decode(¶ms); err != nil {
|
||||
web.Error(w, "failed to parse request body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if params.Email == "" || params.Password == "" {
|
||||
web.Error(w, "missing required fields", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
user, err := h.repo.FindUserEmail(r.Context(), params.Email)
|
||||
if err != nil {
|
||||
web.Error(w, "user with provided email does not exists", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
claims := types.UserClaims{
|
||||
UserID: user.ID.String(),
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
Issuer: "hspguard",
|
||||
Subject: user.Email,
|
||||
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour)),
|
||||
},
|
||||
}
|
||||
|
||||
token, err := SignJwtToken(claims)
|
||||
if err != nil {
|
||||
web.Error(w, fmt.Sprintf("failed to generate access token: %v", err), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
encoder := json.NewEncoder(w)
|
||||
|
||||
type Response struct {
|
||||
Token string `json:"token"`
|
||||
}
|
||||
|
||||
if err := encoder.Encode(Response{
|
||||
Token: token,
|
||||
}); err != nil {
|
||||
web.Error(w, "failed to encode response", http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
|
53
internal/middleware/auth.go
Normal file
53
internal/middleware/auth.go
Normal file
@ -0,0 +1,53 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"gitea.local/admin/hspguard/internal/auth"
|
||||
"gitea.local/admin/hspguard/internal/types"
|
||||
"gitea.local/admin/hspguard/internal/web"
|
||||
)
|
||||
|
||||
func AuthMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
authHeader := r.Header.Get("Authorization")
|
||||
if authHeader == "" {
|
||||
web.Error(w, "unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
parts := strings.Split(authHeader, "Bearer ")
|
||||
if len(parts) != 2 {
|
||||
web.Error(w, "invalid auth header format", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
tokenStr := parts[1]
|
||||
token, userClaims, err := auth.VerifyToken(tokenStr)
|
||||
if err != nil || !token.Valid {
|
||||
http.Error(w, fmt.Sprintf("invalid token: %v", err), http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
ctx := context.WithValue(r.Context(), types.UserIdKey, userClaims.UserID)
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
|
||||
func WithSkipper(mw func(http.Handler) http.Handler, excludedPaths ...string) func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
for _, path := range excludedPaths {
|
||||
if strings.HasPrefix(r.URL.Path, path) {
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
}
|
||||
mw(next).ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -66,6 +66,27 @@ func (q *Queries) FindUserEmail(ctx context.Context, email string) (User, error)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const findUserId = `-- name: FindUserId :one
|
||||
SELECT id, email, full_name, password_hash, is_admin, created_at, updated_at, last_login, phone_number FROM users WHERE id = $1 LIMIT 1
|
||||
`
|
||||
|
||||
func (q *Queries) FindUserId(ctx context.Context, id uuid.UUID) (User, error) {
|
||||
row := q.db.QueryRow(ctx, findUserId, id)
|
||||
var i User
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.Email,
|
||||
&i.FullName,
|
||||
&i.PasswordHash,
|
||||
&i.IsAdmin,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.LastLogin,
|
||||
&i.PhoneNumber,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const insertUser = `-- name: InsertUser :one
|
||||
INSERT INTO users (
|
||||
email, full_name, password_hash, is_admin
|
||||
|
10
internal/types/claims.go
Normal file
10
internal/types/claims.go
Normal file
@ -0,0 +1,10 @@
|
||||
package types
|
||||
|
||||
import "github.com/golang-jwt/jwt/v5"
|
||||
|
||||
type UserClaims struct {
|
||||
UserID string `json:"user_id"`
|
||||
// Role
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
6
internal/types/middleware.go
Normal file
6
internal/types/middleware.go
Normal file
@ -0,0 +1,6 @@
|
||||
package types
|
||||
|
||||
type contextKey string
|
||||
|
||||
const UserIdKey contextKey = "userID"
|
||||
|
@ -3,15 +3,11 @@ package user
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"gitea.local/admin/hspguard/internal/auth"
|
||||
"gitea.local/admin/hspguard/internal/repository"
|
||||
"gitea.local/admin/hspguard/internal/web"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
)
|
||||
|
||||
type UserHandler struct {
|
||||
@ -28,7 +24,6 @@ func (h *UserHandler) RegisterRoutes(router chi.Router, api chi.Router) {
|
||||
router.Get("/login", h.loginPage)
|
||||
router.Get("/register", h.registerPage)
|
||||
api.Post("/register", h.register)
|
||||
api.Post("/login", h.login)
|
||||
}
|
||||
|
||||
func (h *UserHandler) loginPage(w http.ResponseWriter, r *http.Request) {
|
||||
@ -98,62 +93,3 @@ func (h *UserHandler) register(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
type LoginParams struct {
|
||||
Email string `json:"email"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
type UserClaims struct {
|
||||
UserID string `json:"user_id"`
|
||||
// Role
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
func (h *UserHandler) login(w http.ResponseWriter, r *http.Request) {
|
||||
var params LoginParams
|
||||
|
||||
decoder := json.NewDecoder(r.Body)
|
||||
if err := decoder.Decode(¶ms); err != nil {
|
||||
web.Error(w, "failed to parse request body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if params.Email == "" || params.Password == "" {
|
||||
web.Error(w, "missing required fields", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
user, err := h.repo.FindUserEmail(context.Background(), params.Email)
|
||||
if err != nil {
|
||||
web.Error(w, "user with provided email does not exists", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
claims := UserClaims{
|
||||
UserID: user.ID.String(),
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
Issuer: "hspguard",
|
||||
Subject: user.Email,
|
||||
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour)),
|
||||
},
|
||||
}
|
||||
|
||||
token, err := auth.SignJwtToken(claims)
|
||||
if err != nil {
|
||||
web.Error(w, fmt.Sprintf("failed to generate access token: %v", err), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
encoder := json.NewEncoder(w)
|
||||
|
||||
type Response struct {
|
||||
Token string `json:"token"`
|
||||
}
|
||||
|
||||
if err := encoder.Encode(Response{
|
||||
Token: token,
|
||||
}); err != nil {
|
||||
web.Error(w, "failed to encode response", http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
13
internal/util/request.go
Normal file
13
internal/util/request.go
Normal file
@ -0,0 +1,13 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"gitea.local/admin/hspguard/internal/types"
|
||||
)
|
||||
|
||||
func GetRequestUserId(ctx context.Context) (string, bool) {
|
||||
userId, ok := ctx.Value(types.UserIdKey).(string)
|
||||
return userId, ok
|
||||
}
|
||||
|
@ -11,3 +11,6 @@ RETURNING id;
|
||||
|
||||
-- name: FindUserEmail :one
|
||||
SELECT * FROM users WHERE email = $1 LIMIT 1;
|
||||
|
||||
-- name: FindUserId :one
|
||||
SELECT * FROM users WHERE id = $1 LIMIT 1;
|
||||
|
Reference in New Issue
Block a user