diff --git a/Gopkg.lock b/Gopkg.lock index ef14900..ca734b1 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -1,6 +1,18 @@ # This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. +[[projects]] + name = "github.com/gorilla/context" + packages = ["."] + revision = "08b5f424b9271eedf6f9f0ce86cb9396ed337a42" + version = "v1.1.1" + +[[projects]] + name = "github.com/gorilla/mux" + packages = ["."] + revision = "e3702bed27f0d39777b0b37b664b6280e8ef8fbf" + version = "v1.6.2" + [[projects]] name = "github.com/jmoiron/sqlx" packages = [ @@ -25,9 +37,15 @@ ] revision = "ff983b9c42bc9fbf91556e191cc8efb585c16908" +[[projects]] + name = "gopkg.in/yaml.v2" + packages = ["."] + revision = "51d6538a90f86fe93ac480b35f37b2be17fef232" + version = "v2.2.2" + [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "8237f2aa046d402c0ec32f98a3e48419917aeab2979b0b394ce6f7873bc989c2" + inputs-digest = "52d5ff5f5eabb740093e6799fdc91d2d453a3cfd576031a15a0d1a1b7d5109b8" solver-name = "gps-cdcl" solver-version = 1 diff --git a/cmd/lucifer-server/main.go b/cmd/lucifer-server/main.go index b96bd4b..8f8e244 100644 --- a/cmd/lucifer-server/main.go +++ b/cmd/lucifer-server/main.go @@ -2,7 +2,11 @@ package main import ( "log" + "net/http" + "github.com/gorilla/mux" + + "git.aiterp.net/lucifer/lucifer/controllers" "git.aiterp.net/lucifer/lucifer/database/sqlite" "git.aiterp.net/lucifer/lucifer/internal/config" ) @@ -17,4 +21,12 @@ func main() { if err != nil { log.Fatalln("Failed to set up database:", err) } + + userController := controllers.NewUserController(sqlite.UserRepository) + + router := mux.NewRouter() + + userController.Mount(router, "/api/user/") + + http.ListenAndServe(conf.Server.Address, router) } diff --git a/controllers/user-controller.go b/controllers/user-controller.go new file mode 100644 index 0000000..045a119 --- /dev/null +++ b/controllers/user-controller.go @@ -0,0 +1,70 @@ +package controllers + +import ( + "encoding/json" + "net/http" + + "git.aiterp.net/lucifer/lucifer/internal/respond" + "git.aiterp.net/lucifer/lucifer/models" + "github.com/gorilla/mux" +) + +// The UserController is a controller for all user inports. +type UserController struct { + users models.UserRepository +} + +// getUsers (`GET /`): List users +func (c *UserController) getUsers(w http.ResponseWriter, r *http.Request) { + // TODO: Check session + + users, err := c.users.List(r.Context()) + if err != nil { + respond.Error(w, 500, "db_error", err.Error()) + return + } + + respond.JSON(w, 200, users) +} + +// login (`POST /login`): Log in as user +func (c *UserController) login(w http.ResponseWriter, r *http.Request) { + loginData := struct { + Username string `json:"username"` + Password string `json:"password"` + }{} + + err := json.NewDecoder(r.Body).Decode(&loginData) + if err != nil { + respond.Error(w, 400, "invalid_json", "Input is not valid JSON.") + return + } + + user, err := c.users.FindByName(r.Context(), loginData.Username) + if err != nil { + respond.Error(w, http.StatusUnauthorized, "login_failed", "Login failed.") + return + } + + if err := user.CheckPassword(loginData.Password); err != nil { + respond.Error(w, http.StatusUnauthorized, "login_failed", "Login failed.") + return + } + + // TODO: Open session + + respond.JSON(w, 200, user) +} + +// Mount mounts the controller +func (c *UserController) Mount(router *mux.Router, prefix string) { + sub := router.PathPrefix(prefix).Subrouter() + + sub.Handle("/", http.HandlerFunc(c.getUsers)).Methods("GET") + sub.Handle("/login", http.HandlerFunc(c.login)).Methods("POST") +} + +// NewUserController creates a new UserController. +func NewUserController(users models.UserRepository) *UserController { + return &UserController{users: users} +} diff --git a/database/sqlite/user-repository.go b/database/sqlite/user-repository.go new file mode 100644 index 0000000..e1b9364 --- /dev/null +++ b/database/sqlite/user-repository.go @@ -0,0 +1,94 @@ +package sqlite + +import ( + "context" + + "git.aiterp.net/lucifer/lucifer/models" +) + +// UserRepository is a sqlite database. +var UserRepository = &userRepository{} + +type userRepository struct{} + +func (repo *userRepository) FindByID(ctx context.Context, id int) (models.User, error) { + row := db.QueryRowxContext(ctx, "SELECT * FROM user WHERE id=?", id) + if err := row.Err(); err != nil { + return models.User{}, err + } + + user := models.User{} + if err := row.StructScan(&user); err != nil { + return models.User{}, err + } + + return user, nil +} + +func (repo *userRepository) FindByName(ctx context.Context, name string) (models.User, error) { + row := db.QueryRowxContext(ctx, "SELECT * FROM user WHERE name=?", name) + if err := row.Err(); err != nil { + return models.User{}, err + } + + user := models.User{} + if err := row.StructScan(&user); err != nil { + return models.User{}, err + } + + return user, nil +} + +func (repo *userRepository) List(ctx context.Context) ([]models.User, error) { + res, err := db.QueryxContext(ctx, "SELECT * FROM user") + if err != nil { + return nil, err + } else if err := res.Err(); err != nil { + return nil, err + } + + users := make([]models.User, 0, 64) + for res.Next() { + user := models.User{} + if err := res.StructScan(&user); err != nil { + return nil, err + } + + users = append(users, user) + } + + return users, nil +} + +func (repo *userRepository) Insert(ctx context.Context, user models.User) (models.User, error) { + res, err := db.NamedExecContext(ctx, "INSERT INTO user (name, hash) VALUES(:name, :hash)", user) + if err != nil { + return models.User{}, err + } + + id, err := res.LastInsertId() + if err != nil { + return models.User{}, err + } + + user.ID = int(id) + return user, nil +} + +func (repo *userRepository) Update(ctx context.Context, user models.User) error { + _, err := db.NamedExecContext(ctx, "UPDATE user SET name=:name AND hash=:hash WHERE id=:id", user) + if err != nil { + return err + } + + return nil +} + +func (repo *userRepository) Remove(ctx context.Context, user models.User) error { + _, err := db.ExecContext(ctx, "REMOVE FROM user WHERE id=?", user.ID) + if err != nil { + return err + } + + return err +} diff --git a/internal/config/config.go b/internal/config/config.go index 7d49179..8094973 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -11,6 +11,10 @@ type Config struct { DB struct { FileName string `yaml:"file_name"` } `yaml:"db"` + + Server struct { + Address string `yaml:"address"` + } `yaml:"server"` } // Load loads the first valid config file from the list of file paths. diff --git a/internal/respond/error.go b/internal/respond/error.go new file mode 100644 index 0000000..82dd829 --- /dev/null +++ b/internal/respond/error.go @@ -0,0 +1,18 @@ +package respond + +import "net/http" + +// Error responds with a standardized error object. +func Error(w http.ResponseWriter, code int, kind string, message string) { + type errorContent struct { + Code int `json:"code"` + Kind string `json:"kind"` + Message string `json:"message"` + } + + JSON(w, code, &errorContent{ + Code: code, + Kind: kind, + Message: message, + }) +} diff --git a/internal/respond/json.go b/internal/respond/json.go new file mode 100644 index 0000000..abcef45 --- /dev/null +++ b/internal/respond/json.go @@ -0,0 +1,14 @@ +package respond + +import ( + "encoding/json" + "net/http" +) + +// JSON gets the json value. +func JSON(w http.ResponseWriter, code int, data interface{}) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(code) + + json.NewEncoder(w).Encode(data) +} diff --git a/models/user.go b/models/user.go index 47e73e0..b58a918 100644 --- a/models/user.go +++ b/models/user.go @@ -1,14 +1,16 @@ package models import ( + "context" + "golang.org/x/crypto/bcrypt" ) // The User model represents a registered user. type User struct { - ID int - Name string - PassHash []byte + ID int `db:"id" json:"id"` + Name string `db:"name" json:"name"` + PassHash []byte `db:"hash" json:"-"` } // SetPassword sets the user's password @@ -35,10 +37,10 @@ func (user *User) CheckPassword(attempt string) error { // UserRepository is an interface for all database operations // the user model makes. type UserRepository interface { - FindUserByID(id int) (User, error) - FindUserByName(name string) (User, error) - ListUsers() ([]User, error) - InsertUser(user User) (int, error) - UpdateUser(user User) error - RemoveUser(user User) error + FindByID(ctx context.Context, id int) (User, error) + FindByName(ctx context.Context, name string) (User, error) + List(ctx context.Context) ([]User, error) + Insert(ctx context.Context, user User) (User, error) + Update(ctx context.Context, user User) error + Remove(ctx context.Context, user User) error }