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
|
config.yaml
|
||||||
gordio
|
/gordio
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
# stillbox
|
# 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