add user authentication

This commit is contained in:
2025-12-16 23:08:09 +03:00
parent cb0b14799e
commit b72974ef62
19 changed files with 543 additions and 10 deletions

View File

@@ -1,12 +1,18 @@
package main
import (
"encoding/json"
"flag"
"fmt"
"log"
"net/http"
"os"
"time"
"github.com/golang-jwt/jwt/v5"
"github.com/jmoiron/sqlx"
"github.com/joho/godotenv"
"golang.org/x/crypto/bcrypt"
_ "github.com/mattn/go-sqlite3"
)
@@ -15,6 +21,76 @@ var (
addr = flag.String("addr", ":5000", "http server address")
)
type User struct {
ID int64 `json:"id" db:"id"`
Email string `json:"email" db:"email"`
Password string `json:"-" db:"password"`
CreatedAt string `json:"created_at" db:"created_at"`
UpdatedAt string `json:"updated_at" db:"updated_at"`
}
func readJSON(r *http.Request, s any) error {
return json.NewDecoder(r.Body).Decode(s)
}
func sendJSON(w http.ResponseWriter, data any, status int) error {
w.WriteHeader(status)
return json.NewEncoder(w).Encode(data)
}
func sendApiError(w http.ResponseWriter, msg string, err error, status int) {
var e struct {
Message string `json:"message"`
Error string `json:"error,omitempty"`
}
e.Message = msg
if err != nil {
e.Error = err.Error()
}
sendJSON(w, e, status)
}
var jwtSecretKey []byte
func init() {
if err := godotenv.Load(); err != nil {
panic("couldn't parse env")
}
s := os.Getenv("JWT_SECRET")
if s == "" {
panic("JWT_SECRET env doesn't exist")
}
jwtSecretKey = []byte(s)
}
func generateAccessToken(userId int64) (string, error) {
exp := time.Now().Add(time.Hour * 24 * 7)
claims := jwt.MapClaims{
"id": userId,
"exp": exp.Unix(),
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString(jwtSecretKey)
}
type AuthResponse struct {
AccessToken string `json:"access_token"`
}
func handler(mux *http.ServeMux) http.HandlerFunc {
fmt.Printf("starting http server at %s\n", *addr)
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Credentials", "true")
w.Header().Set("Access-Control-Allow-Origin", "http://localhost:3000")
w.Header().Set("Access-Control-Allow-Headers", "Authorization,Content-Type,Content-Length")
w.Header().Set("Access-Control-Allow-Methods", "GET,POST,PATCH,DELETE,OPTIONS")
mux.ServeHTTP(w, r)
}
}
func main() {
flag.Parse()
@@ -30,8 +106,96 @@ func main() {
mux := http.NewServeMux()
fmt.Printf("starting http server at %s\n", *addr)
if err := http.ListenAndServe(*addr, mux); err != nil {
mux.HandleFunc("OPTIONS /", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(200)
fmt.Fprintf(w, "ok")
})
mux.HandleFunc("POST /auth/register", func(w http.ResponseWriter, r *http.Request) {
var body struct {
Email string `json:"email"`
Password string `json:"password"`
}
if err := readJSON(r, &body); err != nil {
sendApiError(w, "couldn't parse body", err, 500)
return
}
if body.Email == "" || body.Password == "" {
sendApiError(w, "invalid request", nil, 400)
return
}
p, err := bcrypt.GenerateFromPassword([]byte(body.Password), 10)
if err != nil {
sendApiError(w, "couldn't hash password", err, 500)
return
}
res, err := db.Exec("INSERT INTO users (email, password) VALUES (?, ?)", body.Email, string(p))
if err != nil {
sendApiError(w, "couldn't create user", err, 500)
return
}
_ = res
id, err := res.LastInsertId()
if err != nil {
sendApiError(w, "couldn't get user id", err, 500)
return
}
token, err := generateAccessToken(id)
if err != nil {
sendApiError(w, "couldn't generate token", err, 500)
return
}
sendJSON(w, AuthResponse{token}, 201)
})
mux.HandleFunc("POST /auth/login", func(w http.ResponseWriter, r *http.Request) {
var body struct {
Email string `json:"email"`
Password string `json:"password"`
}
if err := readJSON(r, &body); err != nil {
sendApiError(w, "couldn't parse body", err, 500)
return
}
if body.Email == "" || body.Password == "" {
sendApiError(w, "invalid request", nil, 400)
return
}
row := db.QueryRowx("SELECT * FROM users WHERE email = ?", body.Email)
if row.Err() != nil {
sendApiError(w, "couldn't find user", err, 404)
return
}
var user User
if err := row.StructScan(&user); err != nil {
sendApiError(w, "couldn't find user", err, 404)
return
}
if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(body.Password)); err != nil {
sendApiError(w, "invalid password", nil, 400)
return
}
token, err := generateAccessToken(user.ID)
if err != nil {
sendApiError(w, "couldn't generate token", err, 500)
return
}
sendJSON(w, AuthResponse{token}, 200)
})
if err := http.ListenAndServe(*addr, handler(mux)); err != nil {
log.Fatalf("failed to start http server: %v\n", err)
}
}