momento

commit 954f5a1c94428d083a53efb2af0dc6b15c15dd49

Author: Pedro Lucas Porcellis <porcellis@eletrotupi.com>

api: introduce a *very* basic wip auth and api handler

 api/api.go | 91 +++++++++++++++++++++++++++++++++++++++++++++++++++++
 api/auth.go | 94 +++++++++++++++++++++++++++++++++++++++++++++++++++++++


diff --git a/api/api.go b/api/api.go
new file mode 100644
index 0000000000000000000000000000000000000000..6bef5106b2f2c8356a592c2c281b76f93e3e5b73
--- /dev/null
+++ b/api/api.go
@@ -0,0 +1,91 @@
+package api
+
+import (
+	"database/sql"
+	"encoding/json"
+	"net/http"
+	"strings"
+
+	"golang.org/x/crypto/bcrypt"
+
+	"git.eletrotupi.com/momento/database"
+)
+
+type Account struct {
+	Email string
+	Password string
+	Bio string
+	Url string
+}
+
+func New() http.Handler {
+	mux := http.NewServeMux()
+	mux.HandleFunc("/api/profile", handleProfile)
+	mux.HandleFunc("/api/register", handleRegister)
+
+	return WithAuth(mux)
+}
+
+func handleProfile(w http.ResponseWriter, r *http.Request) {
+	user := Auth(r.Context())
+	encoder := json.NewEncoder(w)
+
+	err := encoder.Encode(user)
+	if err != nil {
+		panic(err)
+	}
+}
+
+func handleRegister(w http.ResponseWriter, r *http.Request) {
+	if r.Method != "POST" {
+		http.Error(w, "Method not Allowed", http.StatusMethodNotAllowed)
+
+		return
+	}
+
+	var acc Account
+	ctx := r.Context()
+
+	// TODO: Add some security measures and deal gracefully with problems like
+	// malformed body, etc
+	err := json.NewDecoder(r.Body).Decode(&acc)
+	if err != nil {
+		http.Error(w, err.Error(), http.StatusBadRequest)
+		return
+	}
+
+	if acc.Email == "" || acc.Password == "" {
+		http.Error(w, "Missing required fields", http.StatusUnprocessableEntity)
+		return
+	}
+
+	if !strings.ContainsRune(acc.Email, '@') {
+		http.Error(w, "Invalid Email Address", http.StatusUnprocessableEntity)
+		return
+	}
+
+	pwhash, err := bcrypt.GenerateFromPassword([]byte(acc.Password), bcrypt.DefaultCost)
+
+	if err != nil {
+		http.Error(w, "Problem when hashing", http.StatusInternalServerError)
+		return
+	}
+
+	var userID int
+	if err := database.WithTx(ctx, nil, func(tx *sql.Tx) error {
+		row := tx.QueryRowContext(ctx, `
+		INSERT INTO users (
+			created_at, email, password, bio, url
+		) VALUES (
+			NOW() at time zone 'utc',
+			$1, $2, $3, $4
+		)
+		RETURNING id;
+		`, acc.Email, string(pwhash), acc.Bio, acc.Url)
+		// TODO: Detect duplicate users
+		return row.Scan(&userID)
+	}); err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
+}




diff --git a/api/auth.go b/api/auth.go
new file mode 100644
index 0000000000000000000000000000000000000000..423f3bc7a294ae94ee6f58c9938fef8d2477f422
--- /dev/null
+++ b/api/auth.go
@@ -0,0 +1,94 @@
+package api
+
+import (
+	"context"
+	"database/sql"
+	"errors"
+	"net/http"
+	"time"
+
+	"golang.org/x/crypto/bcrypt"
+
+	"git.eletrotupi.com/momento/database"
+)
+
+type User struct {
+	ID			int       `json:"id"`
+	CreatedAt	time.Time `json:"created_at"`
+	Email		string    `json:"email"`
+}
+
+var userCtxKey = &contextKey{"user"}
+
+type contextKey struct {
+	name string
+}
+
+func Context(ctx context.Context, user *User) context.Context {
+	return context.WithValue(ctx, userCtxKey, user)
+}
+
+func Auth(ctx context.Context) *User {
+	user, ok := ctx.Value(userCtxKey).(*User)
+
+	if !ok {
+		panic(errors.New("Invalid authentication context"))
+	}
+
+	return user
+}
+
+func WithAuth(h http.Handler) http.Handler {
+    middleware := func(w http.ResponseWriter, r *http.Request) {
+		if r.URL.Path == "/api/register" {
+			h.ServeHTTP(w, r)
+			return
+		}
+
+		// TODO: Replace this with the final auth mechanism
+		email, password, ok := r.BasicAuth()
+		if !ok {
+			http.Error(w, "Authorization required", http.StatusUnauthorized)
+			return
+		}
+
+		var (
+			user         User
+			pwhash       string
+		)
+
+		if err := database.WithTx(r.Context(), &sql.TxOptions{
+			Isolation: 0,
+			ReadOnly:  true,
+		}, func(tx *sql.Tx) error {
+			row := tx.QueryRowContext(r.Context(), `
+				SELECT
+					id, created_at, email, password
+				FROM users
+				WHERE email = $1;
+			`, email)
+			return row.Scan(&user.ID, &user.CreatedAt, &user.Email, &pwhash)
+		}); err != nil {
+			if err == sql.ErrNoRows {
+				http.Error(w, "Invalid username or password", http.StatusUnauthorized)
+				return
+			}
+
+			panic(err)
+		}
+
+		err := bcrypt.CompareHashAndPassword([]byte(pwhash), []byte(password))
+
+		if err != nil {
+			http.Error(w, "Invalid username or password", http.StatusUnauthorized)
+			return
+		}
+
+		ctx := Context(r.Context(), &user)
+		r = r.WithContext(ctx)
+        h.ServeHTTP(w, r)
+	}
+
+	return http.HandlerFunc(middleware)
+}
+