Update readme

This commit is contained in:
Daniel 2024-07-15 10:12:53 -04:00
parent dbba70b375
commit 47fe2de326
8 changed files with 412 additions and 2 deletions

2
.gitignore vendored
View file

@ -1,2 +1,2 @@
config.yaml
gordio
/gordio

View file

@ -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
View 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
}

View 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
View 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,
}
}

View 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
}

View 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
View 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
}