Update readme
This commit is contained in:
parent
dbba70b375
commit
47fe2de326
8 changed files with 412 additions and 2 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -1,2 +1,2 @@
|
|||
config.yaml
|
||||
gordio
|
||||
/gordio
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
# stillbox
|
||||
|
||||
A Rust scanner call server, with the Calls webapp.
|
||||
A Golang scanner call server, with the Calls webapp.
|
||||
|
|
108
pkg/gordio/admin/admin.go
Normal file
108
pkg/gordio/admin/admin.go
Normal file
|
@ -0,0 +1,108 @@
|
|||
package admin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"syscall"
|
||||
|
||||
"dynatron.me/x/stillbox/pkg/gordio/config"
|
||||
"dynatron.me/x/stillbox/pkg/gordio/database"
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"golang.org/x/term"
|
||||
)
|
||||
|
||||
const (
|
||||
PromptPassword = "Password: "
|
||||
PromptAgain = "Again: "
|
||||
)
|
||||
|
||||
var (
|
||||
ErrDontMatch = errors.New("passwords do not match")
|
||||
ErrInvalidArguments = errors.New("invalid arguments")
|
||||
)
|
||||
|
||||
func addUser(cfg *config.Config, username, email string, isAdmin bool) error {
|
||||
if username == "" || email == "" {
|
||||
return ErrInvalidArguments
|
||||
}
|
||||
|
||||
db, err := database.NewClient(cfg.DB)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
pw, err := readPassword(PromptPassword)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
pwAgain, err := readPassword(PromptAgain)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if pwAgain != pw {
|
||||
return ErrDontMatch
|
||||
}
|
||||
|
||||
if pw == "" {
|
||||
return ErrInvalidArguments
|
||||
}
|
||||
|
||||
hashpw, err := bcrypt.GenerateFromPassword([]byte(pw), bcrypt.DefaultCost)
|
||||
|
||||
_, err = database.New(db).CreateUser(context.Background(), database.CreateUserParams{
|
||||
Username: username,
|
||||
Password: string(hashpw),
|
||||
Email: email,
|
||||
IsAdmin: pgtype.Bool{Bool: isAdmin, Valid: true},
|
||||
})
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func readPassword(prompt string) (string, error) {
|
||||
fmt.Print(prompt)
|
||||
pw, err := term.ReadPassword(int(syscall.Stdin))
|
||||
fmt.Println()
|
||||
return string(pw), err
|
||||
}
|
||||
|
||||
func Command(cfg *config.Config) []*cobra.Command {
|
||||
userCmd := &cobra.Command{
|
||||
Use: "users",
|
||||
Aliases: []string{"u"},
|
||||
Short: "administers the server",
|
||||
}
|
||||
userCmd.AddCommand(addUserCommand(cfg))
|
||||
|
||||
return []*cobra.Command{userCmd}
|
||||
}
|
||||
|
||||
func addUserCommand(cfg *config.Config) *cobra.Command {
|
||||
c := &cobra.Command{
|
||||
Use: "add",
|
||||
Short: "adds a user",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
username := args[0]
|
||||
isAdmin, err := cmd.Flags().GetBool("admin")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
email, err := cmd.Flags().GetString("email")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return addUser(cfg, username, email, isAdmin)
|
||||
},
|
||||
Args: cobra.ExactArgs(1),
|
||||
}
|
||||
c.Flags().BoolP("admin", "a", false, "is admin")
|
||||
c.Flags().StringP("email", "m", "", "email address")
|
||||
|
||||
return c
|
||||
}
|
59
pkg/gordio/database/database.go
Normal file
59
pkg/gordio/database/database.go
Normal file
|
@ -0,0 +1,59 @@
|
|||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"dynatron.me/x/stillbox/pkg/gordio/config"
|
||||
sqlembed "dynatron.me/x/stillbox/sql"
|
||||
"github.com/golang-migrate/migrate/v4"
|
||||
_ "github.com/golang-migrate/migrate/v4/database/pgx/v5"
|
||||
"github.com/golang-migrate/migrate/v4/source/iofs"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
)
|
||||
|
||||
type Conn = *pgxpool.Pool
|
||||
|
||||
func NewClient(conf config.DB) (Conn, error) {
|
||||
dir, err := iofs.New(sqlembed.Migrations, "postgres/migrations")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
m, err := migrate.NewWithSourceInstance("iofs", dir, strings.Replace(conf.Connect, "postgres://", "pgx5://", 1)) // yech
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = m.Up()
|
||||
if err != nil && !errors.Is(err, migrate.ErrNoChange) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
m.Close()
|
||||
|
||||
db, err := pgxpool.New(context.Background(), conf.Connect)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return db, nil
|
||||
}
|
||||
|
||||
type DBCtxKey string
|
||||
|
||||
const DBCTXKeyValue DBCtxKey = "dbctx"
|
||||
|
||||
func FromCtx(ctx context.Context) Conn {
|
||||
c, ok := ctx.Value(DBCTXKeyValue).(Conn)
|
||||
if !ok {
|
||||
panic("no DB in context")
|
||||
}
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
func CtxWithDB(ctx context.Context, conn Conn) context.Context {
|
||||
return context.WithValue(ctx, DBCTXKeyValue, conn)
|
||||
}
|
32
pkg/gordio/database/db.go
Normal file
32
pkg/gordio/database/db.go
Normal file
|
@ -0,0 +1,32 @@
|
|||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.26.0
|
||||
|
||||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/jackc/pgx/v5/pgconn"
|
||||
)
|
||||
|
||||
type DBTX interface {
|
||||
Exec(context.Context, string, ...interface{}) (pgconn.CommandTag, error)
|
||||
Query(context.Context, string, ...interface{}) (pgx.Rows, error)
|
||||
QueryRow(context.Context, string, ...interface{}) pgx.Row
|
||||
}
|
||||
|
||||
func New(db DBTX) *Queries {
|
||||
return &Queries{db: db}
|
||||
}
|
||||
|
||||
type Queries struct {
|
||||
db DBTX
|
||||
}
|
||||
|
||||
func (q *Queries) WithTx(tx pgx.Tx) *Queries {
|
||||
return &Queries{
|
||||
db: tx,
|
||||
}
|
||||
}
|
28
pkg/gordio/database/models.go
Normal file
28
pkg/gordio/database/models.go
Normal file
|
@ -0,0 +1,28 @@
|
|||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.26.0
|
||||
|
||||
package database
|
||||
|
||||
import (
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
)
|
||||
|
||||
type Apikey struct {
|
||||
ID int32
|
||||
Owner pgtype.Int4
|
||||
Apikey string
|
||||
}
|
||||
|
||||
type Call struct {
|
||||
ID pgtype.UUID
|
||||
}
|
||||
|
||||
type User struct {
|
||||
ID int32
|
||||
Username string
|
||||
Password string
|
||||
Email string
|
||||
IsAdmin pgtype.Bool
|
||||
Prefs []byte
|
||||
}
|
117
pkg/gordio/database/query.sql.go
Normal file
117
pkg/gordio/database/query.sql.go
Normal file
|
@ -0,0 +1,117 @@
|
|||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.26.0
|
||||
// source: query.sql
|
||||
|
||||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
)
|
||||
|
||||
const createUser = `-- name: CreateUser :one
|
||||
INSERT INTO users (
|
||||
username,
|
||||
password,
|
||||
email,
|
||||
is_admin
|
||||
) VALUES ($1, $2, $3, $4)
|
||||
RETURNING id, username, password, email, is_admin, prefs
|
||||
`
|
||||
|
||||
type CreateUserParams struct {
|
||||
Username string
|
||||
Password string
|
||||
Email string
|
||||
IsAdmin pgtype.Bool
|
||||
}
|
||||
|
||||
func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (User, error) {
|
||||
row := q.db.QueryRow(ctx, createUser,
|
||||
arg.Username,
|
||||
arg.Password,
|
||||
arg.Email,
|
||||
arg.IsAdmin,
|
||||
)
|
||||
var i User
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.Username,
|
||||
&i.Password,
|
||||
&i.Email,
|
||||
&i.IsAdmin,
|
||||
&i.Prefs,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getUserByID = `-- name: GetUserByID :one
|
||||
SELECT id, username, password, email, is_admin, prefs FROM users
|
||||
WHERE id = $1 LIMIT 1
|
||||
`
|
||||
|
||||
func (q *Queries) GetUserByID(ctx context.Context, id int32) (User, error) {
|
||||
row := q.db.QueryRow(ctx, getUserByID, id)
|
||||
var i User
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.Username,
|
||||
&i.Password,
|
||||
&i.Email,
|
||||
&i.IsAdmin,
|
||||
&i.Prefs,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getUserByUsername = `-- name: GetUserByUsername :one
|
||||
SELECT id, username, password, email, is_admin, prefs FROM users
|
||||
WHERE username = $1 LIMIT 1
|
||||
`
|
||||
|
||||
func (q *Queries) GetUserByUsername(ctx context.Context, username string) (User, error) {
|
||||
row := q.db.QueryRow(ctx, getUserByUsername, username)
|
||||
var i User
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.Username,
|
||||
&i.Password,
|
||||
&i.Email,
|
||||
&i.IsAdmin,
|
||||
&i.Prefs,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getUsers = `-- name: GetUsers :many
|
||||
SELECT id, username, password, email, is_admin, prefs FROM users
|
||||
`
|
||||
|
||||
func (q *Queries) GetUsers(ctx context.Context) ([]User, error) {
|
||||
rows, err := q.db.Query(ctx, getUsers)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []User
|
||||
for rows.Next() {
|
||||
var i User
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.Username,
|
||||
&i.Password,
|
||||
&i.Email,
|
||||
&i.IsAdmin,
|
||||
&i.Prefs,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
66
pkg/gordio/server/auth.go
Normal file
66
pkg/gordio/server/auth.go
Normal file
|
@ -0,0 +1,66 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"dynatron.me/x/stillbox/pkg/gordio/database"
|
||||
|
||||
"github.com/go-chi/jwtauth/v5"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
type claims map[string]interface{}
|
||||
|
||||
func (s *Server) Authenticated(r *http.Request) (claims, bool) {
|
||||
// TODO: check IP against ACL, or conf.Public, and against map of routes
|
||||
tok, cl, err := jwtauth.FromContext(r.Context())
|
||||
return cl, err != nil && tok != nil
|
||||
}
|
||||
|
||||
var (
|
||||
ErrLoginFailed = errors.New("Login failed")
|
||||
)
|
||||
|
||||
func (s *Server) Login(ctx context.Context, username, password string) (token string, err error) {
|
||||
q := database.New(database.FromCtx(ctx))
|
||||
users, err := q.GetUsers(ctx)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("getUsers failed")
|
||||
return "", ErrLoginFailed
|
||||
}
|
||||
|
||||
var found *database.User
|
||||
|
||||
for _, u := range users {
|
||||
if u.Username == username {
|
||||
found = &u
|
||||
}
|
||||
}
|
||||
|
||||
if found == nil {
|
||||
return "", ErrLoginFailed
|
||||
}
|
||||
|
||||
err = bcrypt.CompareHashAndPassword([]byte(found.Password), []byte(password))
|
||||
if err != nil {
|
||||
return "", ErrLoginFailed
|
||||
}
|
||||
|
||||
return s.NewToken(found.ID), nil
|
||||
}
|
||||
|
||||
func (s *Server) NewToken(uid int32) string {
|
||||
claims := claims{
|
||||
"user_id": uid,
|
||||
}
|
||||
jwtauth.SetExpiryIn(claims, time.Hour*24*30) // one month
|
||||
_, tokenString, err := s.jwt.Encode(claims)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return tokenString
|
||||
}
|
Loading…
Reference in a new issue