Skip to content

Commit d0d87aa

Browse files
Add auth endpoints and user migrations
1 parent adbe8ab commit d0d87aa

File tree

11 files changed

+229
-0
lines changed

11 files changed

+229
-0
lines changed

cmd/internal/queries/models.go

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

cmd/internal/queries/query.sql.go

Lines changed: 40 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

cmd/internal/routes/routes.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ func RegisterRoutes(mux *http.ServeMux, logger logging.Logger) {
3030
mux.Handle("DELETE /api/v1/recitals/{id}", deleteRecitalHandler(logger))
3131
mux.Handle("GET /api/v1/recitals/{id}/listen", mainpageHandler(logger))
3232
mux.Handle("GET /api/v1/recitals/{id}/audio", streamRecitalAudioHandler(logger))
33+
mux.Handle("POST /api/v1/users", createUserHandler(logger))
34+
mux.Handle("GET /api/v1/auth", loginUserHandler(logger))
3335
}
3436

3537
func mainpageHandler(logger logging.Logger) http.Handler {

cmd/internal/routes/user_routes.go

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
package routes
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"net/http"
7+
"time"
8+
9+
"github.com/jackc/pgx/v5/pgtype"
10+
constants "github.com/simondanielsson/recite/cmd/internal"
11+
"github.com/simondanielsson/recite/cmd/internal/logging"
12+
"github.com/simondanielsson/recite/cmd/internal/queries"
13+
"github.com/simondanielsson/recite/cmd/internal/utils"
14+
)
15+
16+
func createUserHandler(logger logging.Logger) http.Handler {
17+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
18+
email := r.FormValue("email")
19+
password := r.FormValue("password")
20+
21+
ctx := r.Context()
22+
23+
passwordHash, err := utils.HashPassword(password)
24+
if err != nil {
25+
respondWithOpaqueMessage(ctx, w, r, err, logger)
26+
return
27+
}
28+
29+
repository, ok := ctx.Value(constants.RepositoryKey).(*queries.Queries)
30+
if !ok {
31+
respondWithOpaqueMessage(ctx, w, r, fmt.Errorf("failed loading repository"), logger)
32+
return
33+
}
34+
35+
userArgs := queries.CreateUserParams{
36+
Email: email,
37+
PasswordHash: passwordHash,
38+
CreatedAt: pgtype.Date{Time: time.Now(), InfinityModifier: pgtype.Finite, Valid: true},
39+
}
40+
_, err = repository.CreateUser(ctx, userArgs)
41+
if err != nil {
42+
repondWithErrorMessage(w, r, "Failed creating user", http.StatusInternalServerError, logger)
43+
return
44+
}
45+
46+
status := http.StatusOK
47+
w.WriteHeader(status)
48+
// A bit hacky way towrite override the existing context
49+
ctx = context.WithValue(ctx, constants.StatusCodeKey, status)
50+
*r = *(r.WithContext(ctx))
51+
})
52+
}
53+
54+
func loginUserHandler(logger logging.Logger) http.Handler {
55+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
56+
ctx := r.Context()
57+
repository, ok := ctx.Value(constants.RepositoryKey).(*queries.Queries)
58+
if !ok {
59+
respondWithOpaqueMessage(ctx, w, r, fmt.Errorf("failed loading repository"), logger)
60+
return
61+
}
62+
63+
email, password, ok := r.BasicAuth()
64+
if !ok {
65+
repondWithErrorMessage(w, r, "could not get credentials", http.StatusBadRequest, logger)
66+
return
67+
}
68+
69+
user, err := repository.GetUser(ctx, email)
70+
if err != nil {
71+
message := fmt.Sprintf("User with email %s not found", email)
72+
repondWithErrorMessage(w, r, message, http.StatusNotFound, logger)
73+
return
74+
}
75+
if err := utils.ValidatePassword(password, user.PasswordHash); err != nil {
76+
repondWithErrorMessage(w, r, "Incorrect password", http.StatusBadRequest, logger)
77+
return
78+
}
79+
80+
status := http.StatusOK
81+
w.WriteHeader(status)
82+
// A bit hacky way towrite override the existing context
83+
ctx = context.WithValue(ctx, constants.StatusCodeKey, status)
84+
*r = *(r.WithContext(ctx))
85+
})
86+
}

cmd/internal/server/server.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"net/http"
66

77
"github.com/jackc/pgx/v5/pgxpool"
8+
"github.com/rs/cors"
89
"github.com/simondanielsson/recite/cmd/internal/config"
910
"github.com/simondanielsson/recite/cmd/internal/db"
1011
"github.com/simondanielsson/recite/cmd/internal/logging"
@@ -23,6 +24,7 @@ func New(config config.Config, DB *pgxpool.Pool, logger logging.Logger) App {
2324
var handler http.Handler = mux
2425
handler = logging.AddLoggingMiddleware(handler, logger)
2526
handler = db.AddDatabaseMiddleware(handler, DB, logger)
27+
handler = cors.AllowAll().Handler(handler)
2628

2729
return App{
2830
Server: http.Server{

cmd/internal/utils/jwt.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package utils
2+
3+
import (
4+
"time"
5+
6+
"github.com/golang-jwt/jwt"
7+
)
8+
9+
type JWTClaims struct {
10+
UserID uint `json:"user_id"`
11+
jwt.StandardClaims
12+
}
13+
14+
var jwtKey []byte
15+
16+
func SetJWTKey(key string) {
17+
jwtKey = []byte(key)
18+
}
19+
20+
// CreateJWTToken creates a JWT token with the given user ID
21+
func CreateJWTToken(userID uint) (string, error) {
22+
expirationTime := time.Now().Add(1 * time.Hour)
23+
claims := &JWTClaims{
24+
UserID: userID,
25+
StandardClaims: jwt.StandardClaims{
26+
ExpiresAt: expirationTime.Unix(),
27+
},
28+
}
29+
30+
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
31+
token_string, err := token.SignedString(jwtKey)
32+
if err != nil {
33+
return "", err
34+
}
35+
36+
return token_string, nil
37+
}
38+
39+
// ParseJWTToken parses a JWT token and returns the claims
40+
func ParseJWTToken(token string) (*JWTClaims, error) {
41+
claims := &JWTClaims{}
42+
tkn, err := jwt.ParseWithClaims(token, claims, func(token *jwt.Token) (any, error) {
43+
return jwtKey, nil
44+
})
45+
if err != nil {
46+
return nil, err
47+
}
48+
if !tkn.Valid {
49+
return nil, err
50+
}
51+
52+
return claims, nil
53+
}

cmd/internal/utils/password.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package utils
2+
3+
import "golang.org/x/crypto/bcrypt"
4+
5+
func HashPassword(password string) (string, error) {
6+
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
7+
if err != nil {
8+
return "", err
9+
}
10+
return string(hashedPassword), nil
11+
}
12+
13+
func ValidatePassword(password, hashedPassword string) error {
14+
return bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password))
15+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
-- migrate:up
2+
CREATE TABLE users (
3+
id SERIAL PRIMARY KEY,
4+
email TEXT NOT NULL,
5+
password_hash TEXT NOT NULL,
6+
created_at DATE NOT NULL
7+
);
8+
9+
-- migrate:down
10+
DROP TABLE users;

db/query.sql

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,11 @@ RETURNING *;
2222
-- name: DeleteRecital :exec
2323
DELETE FROM recitals WHERE id = $1
2424
RETURNING *;
25+
26+
-- name: GetUser :one
27+
SELECT * FROM users WHERE email = $1;
28+
29+
-- name: CreateUser :one
30+
INSERT INTO users (email, password_hash, created_at)
31+
VALUES ($1, $2, $3)
32+
RETURNING *;

go.mod

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,15 @@ require (
1414
require (
1515
github.com/andybalholm/cascadia v1.3.3 // indirect
1616
github.com/ebitengine/purego v0.8.0 // indirect
17+
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
1718
github.com/goph/emperror v0.17.1 // indirect
1819
github.com/jackc/pgpassfile v1.0.0 // indirect
1920
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
2021
github.com/jackc/pgx/v5 v5.7.5 // indirect
2122
github.com/jackc/puddle/v2 v2.2.2 // indirect
2223
github.com/konsorten/go-windows-terminal-sequences v1.0.1 // indirect
2324
github.com/pkg/errors v0.8.1 // indirect
25+
github.com/rs/cors v1.11.1 // indirect
2426
github.com/sirupsen/logrus v1.3.0 // indirect
2527
github.com/tidwall/gjson v1.14.4 // indirect
2628
github.com/tidwall/match v1.1.1 // indirect

0 commit comments

Comments
 (0)