feat(backend): Create Users and Get JWT
Introducing a unified signup and login API. You can post to the /user route with credentials (so far just name and password), and if the user doesn't exist, we create the user and return a token. If the user does exist, if the password inputted matches the database password, we return a token for that user, else we return an error. i've never wanted to kms more during a programming session
This commit is contained in:
parent
9157bfe8a1
commit
e08adc96ae
2
backend/.gitignore
vendored
Normal file
2
backend/.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
*.db
|
||||
.env
|
15
backend/README.md
Normal file
15
backend/README.md
Normal file
|
@ -0,0 +1,15 @@
|
|||
# Backend Usage
|
||||
|
||||
In order for this to work, you need to have [sqlite3](https://sqlite.org)
|
||||
installed on your system. Once you do, make a databse called `users.db`
|
||||
and initialize it with the `sql/init.sql` script:
|
||||
|
||||
```sh
|
||||
$ cat sql/init.sql | sqlite3 users.db
|
||||
```
|
||||
|
||||
You also need to have a `.env` file in this folder, with the following options specified
|
||||
|
||||
- JWT_SECRET: Used to encrypt tokens for user auth. Must be provided. Should
|
||||
be cryptographically secure
|
||||
- PORT: Optionally replace the default port of 7741
|
|
@ -1,3 +1,10 @@
|
|||
module github.com/devraza/ambition/backend
|
||||
|
||||
go 1.20
|
||||
|
||||
require (
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible
|
||||
github.com/joho/godotenv v1.5.1
|
||||
github.com/mattn/go-sqlite3 v1.14.17
|
||||
golang.org/x/crypto v0.11.0
|
||||
)
|
||||
|
|
8
backend/go.sum
Normal file
8
backend/go.sum
Normal file
|
@ -0,0 +1,8 @@
|
|||
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
|
||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM=
|
||||
github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||
golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA=
|
||||
golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
|
|
@ -2,37 +2,39 @@ package main
|
|||
|
||||
import (
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
)
|
||||
|
||||
const (
|
||||
serverAddress = "localhost:7764"
|
||||
)
|
||||
type App struct {
|
||||
UserHandler *UserHandler
|
||||
}
|
||||
|
||||
func (h *App) ServeHTTP(res http.ResponseWriter, req *http.Request) {
|
||||
var head string
|
||||
head, req.URL.Path = ShiftPath(req.URL.Path)
|
||||
switch head {
|
||||
case "user":
|
||||
h.UserHandler.Handle(res, req)
|
||||
default:
|
||||
http.Error(res, "Not Found", http.StatusNotFound)
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
log.Println("Ambition going strong at", serverAddress)
|
||||
|
||||
listener, err := net.Listen("tcp", serverAddress)
|
||||
user_handler, err := NewUserHandler()
|
||||
if err != nil {
|
||||
log.Fatalln("Failed to initialise TCP listener", err)
|
||||
}
|
||||
defer listener.close()
|
||||
|
||||
for {
|
||||
conn, err := listener.Accept()
|
||||
if err != nil {
|
||||
log.Println("Failed to accept connection:", err)
|
||||
continue
|
||||
log.Fatalln(err)
|
||||
}
|
||||
|
||||
// Concurrency FTW
|
||||
go handleConnection(conn)
|
||||
a := &App{
|
||||
UserHandler: user_handler,
|
||||
}
|
||||
}
|
||||
|
||||
func handleConnection(conn net.Conn) {
|
||||
defer conn.Close()
|
||||
|
||||
// TODO implement actual server. Waiting on frontend for this
|
||||
|
||||
port := os.Getenv("PORT")
|
||||
if port == "" {
|
||||
port = "7741"
|
||||
}
|
||||
log.Println("Ambition going strong at port 7741")
|
||||
http.ListenAndServe(":"+port, a)
|
||||
}
|
||||
|
|
4
backend/sql/init.sql
Normal file
4
backend/sql/init.sql
Normal file
|
@ -0,0 +1,4 @@
|
|||
CREATE TABLE IF NOT EXISTS users (
|
||||
name TEXT NOT NULL,
|
||||
pwdhash TEXT NOT NULL
|
||||
);
|
126
backend/user.go
Normal file
126
backend/user.go
Normal file
|
@ -0,0 +1,126 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/golang-jwt/jwt"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
|
||||
"database/sql"
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
)
|
||||
|
||||
type UserHandler struct {
|
||||
db *sql.DB
|
||||
jwt_secret *ecdsa.PrivateKey
|
||||
}
|
||||
|
||||
type UserRequest struct {
|
||||
Name string `json:"name"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
func NewUserHandler() (*UserHandler, error) {
|
||||
db, err := sql.Open("sqlite3", "users.db")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
jwt_secret, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &UserHandler{
|
||||
db: db,
|
||||
jwt_secret: jwt_secret,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (h *UserHandler) Handle(res http.ResponseWriter, req *http.Request) {
|
||||
switch req.Method {
|
||||
case "POST":
|
||||
h.createUser(res, req)
|
||||
case "PUT":
|
||||
h.updateUser(res, req)
|
||||
case "DELETE":
|
||||
h.deleteUser(res, req)
|
||||
default:
|
||||
http.Error(res, "Only POST, PUT, and DELETE are valid methods", http.StatusMethodNotAllowed)
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE(midnadimple): This function could be considered to do too much stuff, but
|
||||
// I think this is the best implementation
|
||||
func (h *UserHandler) createUser(res http.ResponseWriter, req *http.Request) {
|
||||
// Can't unmarshal the actual req.Body so must read first
|
||||
body, err := io.ReadAll(req.Body)
|
||||
if err != nil {
|
||||
http.Error(res, fmt.Sprintf("user: failed to read request (%s)", err), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
var user_request UserRequest
|
||||
if err := json.Unmarshal(body, &user_request); err != nil {
|
||||
http.Error(res, "user: json request body doesn't match schema", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
name := user_request.Name
|
||||
password := []byte(user_request.Password)
|
||||
|
||||
// Password checks
|
||||
// -------------------
|
||||
row := h.db.QueryRow("SELECT pwdhash FROM users WHERE name=?", name)
|
||||
var db_pwdhash string
|
||||
|
||||
if err = row.Scan(&db_pwdhash); err != nil {
|
||||
// If no user found with name, create the user
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
pwdhash_bytes, err := bcrypt.GenerateFromPassword(password, 12)
|
||||
if err != nil {
|
||||
http.Error(res, fmt.Sprintf("user: failed to generate password hash (%s)", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
pwdhash := string(pwdhash_bytes)
|
||||
|
||||
_, err = h.db.Exec("INSERT INTO users VALUES (?,?)", name, pwdhash)
|
||||
if err != nil {
|
||||
http.Error(res, fmt.Sprintf("db: failed to create user (%s)", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
http.Error(res, fmt.Sprintf("db: failed to query row (%s)", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
} else if bcrypt.CompareHashAndPassword([]byte(db_pwdhash), password) != nil {
|
||||
http.Error(res, "User exists, but invalid password", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
// JWT generation
|
||||
token := jwt.New(jwt.SigningMethodES256)
|
||||
claims := token.Claims.(jwt.MapClaims)
|
||||
claims["name"] = name
|
||||
claims["pwdhash"] = db_pwdhash
|
||||
|
||||
token_string, err := token.SignedString(h.jwt_secret)
|
||||
if err != nil {
|
||||
http.Error(res, fmt.Sprintf("jwt: failed to generate token (%s)", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Fprintf(res, "%s", token_string)
|
||||
}
|
||||
|
||||
// TODO(midnadimple): implement:
|
||||
func (h *UserHandler) updateUser(res http.ResponseWriter, req *http.Request) {}
|
||||
func (h *UserHandler) deleteUser(res http.ResponseWriter, req *http.Request) {}
|
16
backend/util.go
Normal file
16
backend/util.go
Normal file
|
@ -0,0 +1,16 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"path"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ShiftPath function taken from https://blog.merovius.de/posts/2017-06-18-how-not-to-use-an-http-router/
|
||||
func ShiftPath(p string) (head, tail string) {
|
||||
p = path.Clean("/" + p)
|
||||
i := strings.Index(p[1:], "/") + 1
|
||||
if i <= 0 {
|
||||
return p[1:], "/"
|
||||
}
|
||||
return p[1:i], p[i:]
|
||||
}
|
Reference in a new issue