add user authentication
This commit is contained in:
@@ -3,6 +3,10 @@ module archive.local
|
||||
go 1.25.4
|
||||
|
||||
require (
|
||||
github.com/jmoiron/sqlx v1.4.0 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.32 // indirect
|
||||
github.com/jmoiron/sqlx v1.4.0
|
||||
github.com/joho/godotenv v1.5.1
|
||||
github.com/mattn/go-sqlite3 v1.14.32
|
||||
golang.org/x/crypto v0.46.0
|
||||
)
|
||||
|
||||
require github.com/golang-jwt/jwt/v5 v5.3.0
|
||||
|
||||
@@ -1,8 +1,17 @@
|
||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
|
||||
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||
github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
|
||||
github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY=
|
||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs=
|
||||
github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
|
||||
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
|
||||
|
||||
168
api/main.go
168
api/main.go
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user