mirror of
https://github.com/renorris/openfsd
synced 2026-03-22 23:05:36 +08:00
Changes:
- Implement bootstrapping library for managing several concurrent internal services
- Refactor concurrency model for connections/logical clients and their associated I/O
- Refactor server context singleton
- Refactor error handling
- Most errors are now gracefully sent to the FSD client directly encoded as an $ER packet,
enhancing visibility and debugging
- Most errors are now rightfully treated as non-fatal
- Refactor package/dependency graph
- Refactor calling conventions/interfaces for many packages
- Refactor database package
- Refactor post office
Features:
- Add VATSIM-esque HTTP/JSON "data feed"
- Add ephemeral in-memory database option
- Add user management REST API
- Add improved web interface
- Add MySQL support (drop SQLite support)
194 lines
5.5 KiB
Go
194 lines
5.5 KiB
Go
package database
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"errors"
|
|
"github.com/renorris/openfsd/protocol"
|
|
"golang.org/x/crypto/bcrypt"
|
|
"time"
|
|
)
|
|
|
|
type FSDUserRecord struct {
|
|
CID int `json:"cid"`
|
|
Email string `json:"email"`
|
|
FirstName string `json:"first_name"`
|
|
LastName string `json:"last_name"`
|
|
Password string `json:"password,omitempty"`
|
|
FSDPassword string `json:"fsd_password,omitempty"`
|
|
NetworkRating protocol.NetworkRating `json:"network_rating"`
|
|
PilotRating protocol.PilotRating `json:"pilot_rating"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
}
|
|
|
|
func noRowsChangedError() error { return errors.New("no rows changed") }
|
|
|
|
var NoRowsChangedError = noRowsChangedError()
|
|
|
|
// Update updates this record in the database `db`.
|
|
// CID is immutable: it must reference the account to update.
|
|
// All values must be set except for password and fsd_password, which are optional.
|
|
// Automatically hashes provided passwords, which must be in plaintext.
|
|
func (r *FSDUserRecord) Update(db *sql.DB) (err error) {
|
|
ctx, cancelCtx := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancelCtx()
|
|
|
|
var stmt *sql.Stmt
|
|
// Prepare the initial statement
|
|
if stmt, err = db.PrepareContext(ctx, "UPDATE users SET email=?, first_name=?, last_name=?, network_rating=?, pilot_rating=? WHERE cid=?"); err != nil {
|
|
return err
|
|
}
|
|
|
|
if _, err = stmt.ExecContext(ctx, r.Email, r.FirstName, r.LastName, int(r.NetworkRating), r.PilotRating, r.CID); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err = stmt.Close(); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Update passwords if necessary
|
|
if r.Password != "" {
|
|
var passwordHash []byte
|
|
if passwordHash, err = bcrypt.GenerateFromPassword([]byte(r.Password), bcrypt.MinCost); err != nil {
|
|
return err
|
|
}
|
|
|
|
if stmt, err = db.PrepareContext(ctx, "UPDATE users SET password=? WHERE cid=?"); err != nil {
|
|
return err
|
|
}
|
|
|
|
if _, err = stmt.ExecContext(ctx, string(passwordHash), r.CID); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err = stmt.Close(); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if r.FSDPassword != "" {
|
|
var fsdPasswordHash []byte
|
|
if fsdPasswordHash, err = bcrypt.GenerateFromPassword([]byte(r.FSDPassword), bcrypt.MinCost); err != nil {
|
|
return err
|
|
}
|
|
|
|
if stmt, err = db.PrepareContext(ctx, "UPDATE users SET fsd_password=? WHERE cid=?"); err != nil {
|
|
return err
|
|
}
|
|
|
|
if _, err = stmt.ExecContext(ctx, string(fsdPasswordHash), r.CID); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err = stmt.Close(); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// LoadByCID loads a user with the primary key `cid` from the database `db`
|
|
// Returns sql.ErrNoRows when no record matches the provided `cid`
|
|
func (r *FSDUserRecord) LoadByCID(db *sql.DB, cid int) error {
|
|
ctx, cancelCtx := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancelCtx()
|
|
|
|
var stmt *sql.Stmt
|
|
var err error
|
|
// Prepare the statement
|
|
if stmt, err = db.PrepareContext(ctx, "SELECT * FROM users WHERE cid=? LIMIT 1"); err != nil {
|
|
return err
|
|
}
|
|
|
|
var record FSDUserRecord
|
|
if err = stmt.QueryRowContext(ctx, cid).Scan(&record.CID, &record.Email, &record.FirstName,
|
|
&record.LastName, &record.Password, &record.FSDPassword, &record.NetworkRating,
|
|
&record.PilotRating, &record.CreatedAt, &record.UpdatedAt); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err = stmt.Close(); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Copy record into receiver
|
|
*r = record
|
|
|
|
return nil
|
|
}
|
|
|
|
// Insert inserts this user record into the database `db`
|
|
// Automatically hashes provided passwords.
|
|
// Received FSDUserRecord should contain the plaintext passwords to hash.
|
|
// Returns the automatically assigned CID
|
|
func (r *FSDUserRecord) Insert(db *sql.DB) (cid int, err error) {
|
|
ctx, cancelCtx := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancelCtx()
|
|
|
|
// hash the passwords
|
|
var passwordHash []byte
|
|
if passwordHash, err = bcrypt.GenerateFromPassword([]byte(r.Password), bcrypt.MinCost); err != nil {
|
|
return -1, err
|
|
}
|
|
|
|
var fsdPasswordHash []byte
|
|
if fsdPasswordHash, err = bcrypt.GenerateFromPassword([]byte(r.FSDPassword), bcrypt.MinCost); err != nil {
|
|
return -1, err
|
|
}
|
|
|
|
var stmt *sql.Stmt
|
|
// Prepare the statement
|
|
if stmt, err = db.PrepareContext(ctx, "INSERT INTO users (email, first_name, last_name, password, fsd_password, network_rating, pilot_rating) VALUES (?, ?, ?, ?, ?, ?, ?)"); err != nil {
|
|
return -1, err
|
|
}
|
|
|
|
var res sql.Result
|
|
if res, err = stmt.ExecContext(ctx, r.Email, r.FirstName, r.LastName, string(passwordHash), string(fsdPasswordHash), int(r.NetworkRating), int(r.PilotRating)); err != nil {
|
|
return -1, err
|
|
}
|
|
|
|
if err = stmt.Close(); err != nil {
|
|
return -1, err
|
|
}
|
|
|
|
var cid64 int64
|
|
if cid64, err = res.LastInsertId(); err != nil {
|
|
return -1, err
|
|
}
|
|
|
|
return int(cid64), nil
|
|
}
|
|
|
|
// Delete deletes this user record from the database `db`
|
|
// Returns NoRowsChangedError when nothing is deleted
|
|
func (r *FSDUserRecord) Delete(db *sql.DB, cid int) (err error) {
|
|
ctx, cancelCtx := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancelCtx()
|
|
|
|
var stmt *sql.Stmt
|
|
// Prepare the statement
|
|
if stmt, err = db.PrepareContext(ctx, "DELETE FROM users WHERE cid=?"); err != nil {
|
|
return err
|
|
}
|
|
|
|
var res sql.Result
|
|
if res, err = stmt.ExecContext(ctx, cid); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err = stmt.Close(); err != nil {
|
|
return err
|
|
}
|
|
|
|
if rows, err := res.RowsAffected(); err != nil {
|
|
return err
|
|
} else if rows != 1 {
|
|
return NoRowsChangedError
|
|
}
|
|
|
|
return nil
|
|
}
|