initial commit
This commit is contained in:
55
internal/db/config.go
Normal file
55
internal/db/config.go
Normal file
@@ -0,0 +1,55 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"git.haelnorr.com/h/golib/env"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
User string // ENV DB_USER: Database user for authentication (required)
|
||||
Password string // ENV DB_PASSWORD: Database password for authentication (required)
|
||||
Host string // ENV DB_HOST: Database host address (required)
|
||||
Port uint16 // ENV DB_PORT: Database port (default: 5432)
|
||||
DB string // ENV DB_NAME: Database name to connect to (required)
|
||||
SSL string // ENV DB_SSL: SSL mode for connection (default: disable)
|
||||
}
|
||||
|
||||
func ConfigFromEnv() (any, error) {
|
||||
cfg := &Config{
|
||||
User: env.String("DB_USER", ""),
|
||||
Password: env.String("DB_PASSWORD", ""),
|
||||
Host: env.String("DB_HOST", ""),
|
||||
Port: env.UInt16("DB_PORT", 5432),
|
||||
DB: env.String("DB_NAME", ""),
|
||||
SSL: env.String("DB_SSL", "disable"),
|
||||
}
|
||||
|
||||
// Validate SSL mode
|
||||
validSSLModes := map[string]bool{
|
||||
"disable": true,
|
||||
"require": true,
|
||||
"verify-ca": true,
|
||||
"verify-full": true,
|
||||
"allow": true,
|
||||
"prefer": true,
|
||||
}
|
||||
if !validSSLModes[cfg.SSL] {
|
||||
return nil, errors.Errorf("Invalid DB_SSL value: %s. Must be one of: disable, allow, prefer, require, verify-ca, verify-full", cfg.SSL)
|
||||
}
|
||||
|
||||
// Check required fields
|
||||
if cfg.User == "" {
|
||||
return nil, errors.New("Envar not set: DB_USER")
|
||||
}
|
||||
if cfg.Password == "" {
|
||||
return nil, errors.New("Envar not set: DB_PASSWORD")
|
||||
}
|
||||
if cfg.Host == "" {
|
||||
return nil, errors.New("Envar not set: DB_HOST")
|
||||
}
|
||||
if cfg.DB == "" {
|
||||
return nil, errors.New("Envar not set: DB_NAME")
|
||||
}
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
41
internal/db/ezconf.go
Normal file
41
internal/db/ezconf.go
Normal file
@@ -0,0 +1,41 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// EZConfIntegration provides integration with ezconf for automatic configuration
|
||||
type EZConfIntegration struct {
|
||||
configFunc func() (any, error)
|
||||
name string
|
||||
}
|
||||
|
||||
// PackagePath returns the path to the config package for source parsing
|
||||
func (e EZConfIntegration) PackagePath() string {
|
||||
_, filename, _, _ := runtime.Caller(0)
|
||||
// Return directory of this file
|
||||
return filename[:len(filename)-len("/ezconf.go")]
|
||||
}
|
||||
|
||||
// ConfigFunc returns the ConfigFromEnv function for ezconf
|
||||
func (e EZConfIntegration) ConfigFunc() func() (any, error) {
|
||||
return func() (any, error) {
|
||||
return e.configFunc()
|
||||
}
|
||||
}
|
||||
|
||||
// Name returns the name to use when registering with ezconf
|
||||
func (e EZConfIntegration) Name() string {
|
||||
return strings.ToLower(e.name)
|
||||
}
|
||||
|
||||
// GroupName returns the display name for grouping environment variables
|
||||
func (e EZConfIntegration) GroupName() string {
|
||||
return e.name
|
||||
}
|
||||
|
||||
// NewEZConfIntegration creates a new EZConf integration helper
|
||||
func NewEZConfIntegration() EZConfIntegration {
|
||||
return EZConfIntegration{name: "db", configFunc: ConfigFromEnv}
|
||||
}
|
||||
163
internal/db/user.go
Normal file
163
internal/db/user.go
Normal file
@@ -0,0 +1,163 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/uptrace/bun"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
type User struct {
|
||||
bun.BaseModel `bun:"table:users,alias:u"`
|
||||
|
||||
ID int `bun:"id,pk,autoincrement"` // Integer ID (index primary key)
|
||||
Username string `bun:"username,unique"` // Username (unique)
|
||||
PasswordHash string `bun:"password_hash,nullzero"` // Bcrypt hashed password (not exported in JSON)
|
||||
CreatedAt int64 `bun:"created_at"` // Epoch timestamp when the user was added to the database
|
||||
Bio string `bun:"bio"` // Short byline set by the user
|
||||
}
|
||||
|
||||
func (user *User) GetID() int {
|
||||
return user.ID
|
||||
}
|
||||
|
||||
// Uses bcrypt to set the users password_hash from the given password
|
||||
func (user *User) SetPassword(ctx context.Context, tx bun.Tx, password string) error {
|
||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "bcrypt.GenerateFromPassword")
|
||||
}
|
||||
newPassword := string(hashedPassword)
|
||||
|
||||
_, err = tx.NewUpdate().
|
||||
Model(user).
|
||||
Set("password_hash = ?", newPassword).
|
||||
Where("id = ?", user.ID).
|
||||
Exec(ctx)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "tx.Update")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Uses bcrypt to check if the given password matches the users password_hash
|
||||
func (user *User) CheckPassword(ctx context.Context, tx bun.Tx, password string) error {
|
||||
var hashedPassword string
|
||||
err := tx.NewSelect().
|
||||
Table("users").
|
||||
Column("password_hash").
|
||||
Where("id = ?", user.ID).
|
||||
Limit(1).
|
||||
Scan(ctx, &hashedPassword)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "tx.Select")
|
||||
}
|
||||
|
||||
err = bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password))
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Username or password incorrect")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Change the user's username
|
||||
func (user *User) ChangeUsername(ctx context.Context, tx bun.Tx, newUsername string) error {
|
||||
_, err := tx.NewUpdate().
|
||||
Model(user).
|
||||
Set("username = ?", newUsername).
|
||||
Where("id = ?", user.ID).
|
||||
Exec(ctx)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "tx.Update")
|
||||
}
|
||||
user.Username = newUsername
|
||||
return nil
|
||||
}
|
||||
|
||||
// Change the user's bio
|
||||
func (user *User) ChangeBio(ctx context.Context, tx bun.Tx, newBio string) error {
|
||||
_, err := tx.NewUpdate().
|
||||
Model(user).
|
||||
Set("bio = ?", newBio).
|
||||
Where("id = ?", user.ID).
|
||||
Exec(ctx)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "tx.Update")
|
||||
}
|
||||
user.Bio = newBio
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreateUser creates a new user with the given username and password
|
||||
func CreateUser(ctx context.Context, tx bun.Tx, username, password string) (*User, error) {
|
||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "bcrypt.GenerateFromPassword")
|
||||
}
|
||||
|
||||
user := &User{
|
||||
Username: username,
|
||||
PasswordHash: string(hashedPassword),
|
||||
CreatedAt: 0, // You may want to set this to time.Now().Unix()
|
||||
Bio: "",
|
||||
}
|
||||
|
||||
_, err = tx.NewInsert().
|
||||
Model(user).
|
||||
Exec(ctx)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "tx.Insert")
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// GetUserByID queries the database for a user matching the given ID
|
||||
// Returns nil, nil if no user is found
|
||||
func GetUserByID(ctx context.Context, tx bun.Tx, id int) (*User, error) {
|
||||
user := new(User)
|
||||
err := tx.NewSelect().
|
||||
Model(user).
|
||||
Where("id = ?", id).
|
||||
Limit(1).
|
||||
Scan(ctx)
|
||||
if err != nil {
|
||||
if err.Error() == "sql: no rows in result set" {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, errors.Wrap(err, "tx.Select")
|
||||
}
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// GetUserByUsername queries the database for a user matching the given username
|
||||
// Returns nil, nil if no user is found
|
||||
func GetUserByUsername(ctx context.Context, tx bun.Tx, username string) (*User, error) {
|
||||
user := new(User)
|
||||
err := tx.NewSelect().
|
||||
Model(user).
|
||||
Where("username = ?", username).
|
||||
Limit(1).
|
||||
Scan(ctx)
|
||||
if err != nil {
|
||||
if err.Error() == "sql: no rows in result set" {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, errors.Wrap(err, "tx.Select")
|
||||
}
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// IsUsernameUnique checks if the given username is unique (not already taken)
|
||||
// Returns true if the username is available, false if it's taken
|
||||
func IsUsernameUnique(ctx context.Context, tx bun.Tx, username string) (bool, error) {
|
||||
count, err := tx.NewSelect().
|
||||
Model((*User)(nil)).
|
||||
Where("username = ?", username).
|
||||
Count(ctx)
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "tx.Count")
|
||||
}
|
||||
return count == 0, nil
|
||||
}
|
||||
Reference in New Issue
Block a user