[server][auth] Create auth package with extensible config for LDAP and OAuth

This commit is contained in:
Abhishek Shroff
2025-06-10 02:38:03 +05:30
parent aaa2986bd2
commit a4b7bd1bed
22 changed files with 284 additions and 260 deletions

View File

@@ -5,6 +5,7 @@ import (
"net/http"
"strings"
"codeberg.org/shroff/phylum/server/internal/auth"
"codeberg.org/shroff/phylum/server/internal/core"
"github.com/gin-gonic/gin"
)
@@ -41,11 +42,10 @@ func Require(c *gin.Context) {
}
func extractUserDetails(c *gin.Context) (core.User, error) {
userManager := core.UserManagerFromContext(c.Request.Context())
if header := c.Request.Header.Get("Authorization"); header == "" {
if cookie, err := c.Request.Cookie("auth_token"); err == nil {
token := cookie.Value
if u, err := userManager.ReadAccessToken(token); err == nil {
if u, err := auth.ReadAccessToken(c.Request.Context(), token); err == nil {
return u, nil
} else {
return core.User{}, err
@@ -54,16 +54,16 @@ func extractUserDetails(c *gin.Context) (core.User, error) {
return core.User{}, err
}
return core.User{}, errAuthRequired
} else if auth, ok := checkAuthHeader(header, "basic"); ok {
if email, password, ok := decodeBasicAuth(auth); ok {
if u, err := userManager.VerifyUserPassword(email, password); err == nil {
} else if authHeader, ok := checkAuthHeader(header, "basic"); ok {
if email, password, ok := decodeBasicAuth(authHeader); ok {
if u, err := auth.VerifyUserPassword(c.Request.Context(), email, password); err == nil {
return u, nil
} else {
return core.User{}, err
}
}
} else if token, ok := checkAuthHeader(header, "bearer"); ok {
if u, err := userManager.ReadAccessToken(token); err == nil {
if u, err := auth.ReadAccessToken(c.Request.Context(), token); err == nil {
return u, nil
} else {
return core.User{}, err

View File

@@ -7,7 +7,9 @@ import (
"codeberg.org/shroff/phylum/server/internal/api/authenticator"
"codeberg.org/shroff/phylum/server/internal/api/v1/my"
"codeberg.org/shroff/phylum/server/internal/api/v1/responses"
"codeberg.org/shroff/phylum/server/internal/auth"
"codeberg.org/shroff/phylum/server/internal/core"
"codeberg.org/shroff/phylum/server/internal/db"
"codeberg.org/shroff/phylum/server/internal/mail"
"github.com/gin-gonic/gin"
)
@@ -42,10 +44,9 @@ func handlePasswordAuth(c *gin.Context) {
panic(core.NewError(http.StatusBadRequest, "missing_params", "Email or password not specified"))
}
userManager := core.UserManagerFromContext(c.Request.Context())
if user, err := userManager.VerifyUserPassword(params.Email, params.Password); err != nil {
if user, err := auth.VerifyUserPassword(c.Request.Context(), params.Email, params.Password); err != nil {
panic(err)
} else if token, err := userManager.CreateAccessToken(user); err != nil {
} else if token, err := auth.CreateAccessToken(c.Request.Context(), user); err != nil {
panic(err)
} else if bootstrap, err := my.Bootstrap(c.Request.Context(), user, 0); err != nil {
panic(err)
@@ -75,7 +76,7 @@ func handleRequestPasswordReset(c *gin.Context) {
panic(err)
}
if token, err := userManager.CreateResetToken(u); err != nil {
if token, err := auth.CreateResetToken(db.Get(c.Request.Context()), u); err != nil {
panic(err)
} else {
go func() {
@@ -95,9 +96,9 @@ func handleResetPassword(c *gin.Context) {
userManager := core.UserManagerFromContext(c.Request.Context())
if user, err := userManager.UserByEmail(params.Email); err != nil {
panic(err)
} else if err := userManager.ResetUserPassword(user, params.Token, params.Password); err != nil {
} else if err := auth.ResetUserPassword(db.Get(c.Request.Context()), user, params.Token, params.Password); err != nil {
panic(err)
} else if token, err := userManager.CreateAccessToken(user); err != nil {
} else if token, err := auth.CreateAccessToken(c.Request.Context(), user); err != nil {
panic(err)
} else if bootstrap, err := my.Bootstrap(c.Request.Context(), user, 0); err != nil {
panic(err)

View File

@@ -5,6 +5,7 @@ import (
"net/http"
webdav "codeberg.org/shroff/phylum/server/internal/api/webdav/impl"
"codeberg.org/shroff/phylum/server/internal/auth"
"codeberg.org/shroff/phylum/server/internal/core"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
@@ -44,7 +45,7 @@ func (h *handler) HandleRequest(c *gin.Context) {
if email, pass, ok := c.Request.BasicAuth(); ok {
ctx := c.Request.Context()
userManager := core.UserManagerFromContext(ctx)
if u, err := userManager.VerifyUserPassword(email, pass); err == nil {
if u, err := auth.VerifyUserPassword(ctx, email, pass); err == nil {
authSuccess = true
root := c.Param("root")
f = u.OpenFileSystem(ctx)
@@ -65,7 +66,7 @@ func (h *handler) HandleRequest(c *gin.Context) {
c.AbortWithStatus(http.StatusNotFound)
return
}
} else if !errors.Is(err, core.ErrCredentialsInvalid) {
} else if !errors.Is(err, auth.ErrCredentialsInvalid) {
panic(err)
}
}

View File

@@ -0,0 +1,151 @@
package auth
import (
"context"
"errors"
"strings"
"time"
"codeberg.org/shroff/phylum/server/internal/auth/crypt"
"codeberg.org/shroff/phylum/server/internal/core"
"codeberg.org/shroff/phylum/server/internal/db"
"codeberg.org/shroff/phylum/server/internal/rand"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgtype"
)
var Cfg Config
const accessTokenLength = 16
const resetTokenLength = 64
const resetTokenDuration = 10 * time.Minute
var accessTokenValidity = pgtype.Interval{
Days: 30,
Valid: true,
}
var ErrCredentialsInvalid = errors.New("invalid credentials")
func VerifyUserPassword(ctx context.Context, email, password string) (core.User, error) {
if user, passwordHash, err := userPasswordHashByEmail(db.Get(ctx), email); err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return core.User{}, ErrCredentialsInvalid
}
return core.User{}, err
} else if passwordHash == "" {
return core.User{}, ErrCredentialsInvalid
} else {
if b, err := crypt.Verify(password, passwordHash); err != nil {
return core.User{}, err
} else if !b {
return core.User{}, ErrCredentialsInvalid
}
return user, nil
}
}
func CreateAccessToken(ctx context.Context, user core.User) (string, error) {
const q = `INSERT INTO access_tokens(id, expires, user_id) VALUES ($1::TEXT, NOW() + $2::INTERVAL, $3::INT)`
id := rand.GenerateRandomString(accessTokenLength)
if _, err := db.Get(ctx).Exec(q, id, accessTokenValidity, user.ID); err != nil {
return "", err
} else {
return id, nil
}
}
func ReadAccessToken(ctx context.Context, accessToken string) (user core.User, err error) {
const q = `SELECT t.expires, u.id, u.email, u.name, u.permissions, u.home FROM access_tokens t JOIN users u ON t.user_id = u.id WHERE t.id = $1; `
row := db.Get(ctx).QueryRow(q, accessToken)
var expires pgtype.Timestamp
err = row.Scan(&expires, &user.ID, &user.Email, &user.Name, &user.Permissions, &user.Home)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
err = ErrCredentialsInvalid
}
} else if time.Now().After(expires.Time) {
return core.User{}, ErrCredentialsInvalid
}
return
}
func CreateResetToken(db db.Handler, user core.User) (string, error) {
const q = `INSERT INTO reset_tokens(user_id, token, expires)
VALUES (@user_id::INT, @token::TEXT, @expires::TIMESTAMP)
ON CONFLICT(user_id) DO UPDATE SET token = @token::TEXT, expires = @expires::TIMESTAMP`
token := rand.GenerateRandomString(resetTokenLength)
args := pgx.NamedArgs{
"user_id": user.ID,
"token": token,
"expires": time.Now().Add(resetTokenDuration),
}
if _, err := db.Exec(q, args); err != nil {
return "", err
}
return token, nil
}
func ResetUserPassword(d db.Handler, user core.User, token, password string) error {
const q = `DELETE FROM reset_tokens WHERE user_id = @user_id::INT AND token = @token::TEXT RETURNING expires`
args := pgx.NamedArgs{
"user_id": user.ID,
"token": token,
"expires": time.Now().Add(resetTokenDuration),
}
return d.RunInTx(func(d db.Handler) error {
// UpdateUserPassword will ensure the password strength
// Not incorrect to do this before token verification because we are in a transaction.
// TODO: Are there perf implications for this in case of malicious actors?
err := UpdateUserPassword(d, user, password)
if err != nil {
return err
}
row := d.QueryRow(q, args)
var expires pgtype.Timestamp
if err := row.Scan(&expires); err == nil {
if time.Now().After(expires.Time) {
return ErrCredentialsInvalid
}
return nil
} else if errors.Is(err, pgx.ErrNoRows) {
return ErrCredentialsInvalid
} else {
return err
}
})
}
func UpdateUserPassword(db db.Handler, user core.User, password string) error {
if err := checkPasswordStrength(password); err != nil {
return err
}
const q = "UPDATE users SET password_hash = $2::TEXT, modified = NOW() WHERE id = $1::INT"
if hash, err := crypt.Generate(password); err != nil {
return err
} else {
if _, err := db.Exec(q, user.ID, hash); err != nil {
return err
}
}
return nil
}
func userPasswordHashByEmail(db db.Handler, email string) (user core.User, passwordHash string, err error) {
const q = "SELECT id, email, name, home, permissions, password_hash FROM users WHERE email = $1"
row := db.QueryRow(q, strings.ToLower(email))
err = row.Scan(
&user.ID,
&user.Email,
&user.Name,
&user.Home,
&user.Permissions,
&passwordHash)
if errors.Is(err, pgx.ErrNoRows) {
err = ErrCredentialsInvalid
}
return
}

View File

@@ -0,0 +1,21 @@
package auth
import "codeberg.org/shroff/phylum/server/internal/auth/crypt"
type Config struct {
Password PasswordConfig `koanf:"password"`
}
type PasswordConfig struct {
Backend string `koanf:"backend" `
Crypt crypt.Config `koanf:"crypt"`
Requirements PasswordRequirements `koanf:"requirements"`
}
type PasswordRequirements struct {
Length int `koanf:"length"`
Lower int `koanf:"lower"`
Upper int `koanf:"upper"`
Numeric int `koanf:"numeric"`
Symbols int `koanf:"symbols"`
}

View File

@@ -0,0 +1,64 @@
package auth
import (
"strconv"
)
type charType int
const (
charTypeOther charType = iota
charTypeLower
charTypeUpper
charTypeNumeric
charTypeSymbol
)
var charTypes = map[rune]charType{}
type PasswordStrengthError struct {
Reason string
}
func (e PasswordStrengthError) Error() string {
return e.Reason
}
func init() {
for _, c := range "abcdefghijklmnopqrstuvwxyz" {
charTypes[c] = charTypeLower
}
for _, c := range "ABCDEFGHIJKLMNOPQRSTUVWXYZ" {
charTypes[c] = charTypeUpper
}
for _, c := range "0123456789" {
charTypes[c] = charTypeNumeric
}
for _, c := range "`~!@#$%^&*()-_=+[]{}\\|;:'\",.<>/?" {
charTypes[c] = charTypeSymbol
}
}
func checkPasswordStrength(password string) error {
if len(password) < Cfg.Password.Requirements.Length {
return &PasswordStrengthError{Reason: "password must be at least " + strconv.Itoa(Cfg.Password.Requirements.Length) + " characters long."}
}
count := map[charType]int{}
for _, c := range password {
count[charTypes[c]]++
}
if count[charTypeLower] < Cfg.Password.Requirements.Lower {
return &PasswordStrengthError{Reason: "password must have at least " + strconv.Itoa(Cfg.Password.Requirements.Lower) + " lower case."}
}
if count[charTypeUpper] < Cfg.Password.Requirements.Upper {
return &PasswordStrengthError{Reason: "password must have at least " + strconv.Itoa(Cfg.Password.Requirements.Upper) + " upper case."}
}
if count[charTypeNumeric] < Cfg.Password.Requirements.Numeric {
return &PasswordStrengthError{Reason: "password must have at least " + strconv.Itoa(Cfg.Password.Requirements.Numeric) + " numeric."}
}
if count[charTypeSymbol] < Cfg.Password.Requirements.Symbols {
return &PasswordStrengthError{"password must have at least " + strconv.Itoa(Cfg.Password.Requirements.Symbols) + " symbols."}
}
return nil
}

View File

@@ -6,7 +6,9 @@ import (
"os"
"syscall"
"codeberg.org/shroff/phylum/server/internal/auth"
"codeberg.org/shroff/phylum/server/internal/core"
"codeberg.org/shroff/phylum/server/internal/db"
"github.com/spf13/cobra"
"golang.org/x/term"
)
@@ -53,7 +55,7 @@ func setupPasswdCommand() *cobra.Command {
}
}
err = core.UserManagerFromContext(context.Background()).UpdateUserPassword(u, password)
err = auth.UpdateUserPassword(db.Get(context.Background()), u, password)
if err != nil {
fmt.Println("could not add user: " + err.Error())
os.Exit(1)

View File

@@ -5,7 +5,9 @@ import (
"fmt"
"os"
"codeberg.org/shroff/phylum/server/internal/auth"
"codeberg.org/shroff/phylum/server/internal/core"
"codeberg.org/shroff/phylum/server/internal/db"
"codeberg.org/shroff/phylum/server/internal/mail"
"github.com/spf13/cobra"
)
@@ -22,7 +24,7 @@ func setupPwresetResetCommand() *cobra.Command {
if user, err := manager.UserByEmail(email); err != nil {
fmt.Println("unable to find user" + email + ": " + err.Error())
os.Exit(1)
} else if token, err := manager.CreateResetToken(user); err != nil {
} else if token, err := auth.CreateResetToken(db.Get(context.Background()), user); err != nil {
fmt.Println("unable to create reset token: " + err.Error())
os.Exit(1)
} else {

View File

@@ -8,12 +8,13 @@ import (
"path"
"strings"
"codeberg.org/shroff/phylum/server/internal/auth"
"codeberg.org/shroff/phylum/server/internal/auth/crypt"
"codeberg.org/shroff/phylum/server/internal/command/admin"
"codeberg.org/shroff/phylum/server/internal/command/fs"
"codeberg.org/shroff/phylum/server/internal/command/serve"
"codeberg.org/shroff/phylum/server/internal/command/user"
"codeberg.org/shroff/phylum/server/internal/core"
"codeberg.org/shroff/phylum/server/internal/crypt"
"codeberg.org/shroff/phylum/server/internal/db"
"codeberg.org/shroff/phylum/server/internal/mail"
"codeberg.org/shroff/phylum/server/internal/storage"
@@ -117,7 +118,9 @@ func SetupCommand() {
serve.Cfg = cfg.Server
mail.Cfg = cfg.Mail
core.Cfg = cfg.User
crypt.Cfg = cfg.Crypt
auth.Cfg = cfg.Auth
crypt.Cfg = cfg.Auth.Password.Crypt
if err := storage.Initialize(db.Get(context.Background())); err != nil {
logrus.Fatal("Failed to initialize storage: " + err.Error())
}

View File

@@ -13,22 +13,26 @@ storage:
user:
password:
length: 12
lower: 1
upper: 1
numeric: 1
symbols: 1
basedir: /home
permission: 0x10 # Invite users
crypt:
hash: argon2
argon2:
memory: 2048
iterations: 6
parallelism: 4
salt: 32
key: 32
auth:
password:
backend: crypt
crypt:
hash: argon2
argon2:
memory: 2048
iterations: 6
parallelism: 4
salt: 32
key: 32
requirements:
length: 12
lower: 1
upper: 1
numeric: 1
symbols: 1
server:
host:

View File

@@ -1,9 +1,9 @@
package command
import (
"codeberg.org/shroff/phylum/server/internal/auth"
"codeberg.org/shroff/phylum/server/internal/command/serve"
"codeberg.org/shroff/phylum/server/internal/core"
"codeberg.org/shroff/phylum/server/internal/crypt"
"codeberg.org/shroff/phylum/server/internal/db"
"codeberg.org/shroff/phylum/server/internal/mail"
"codeberg.org/shroff/phylum/server/internal/storage"
@@ -16,5 +16,5 @@ type Config struct {
Server serve.Config `koanf:"server"`
Mail mail.Config `koanf:"mail"`
User core.Config `koanf:"user"`
Crypt crypt.Config `koanf:"crypt"`
Auth auth.Config `koanf:"auth"`
}

View File

@@ -1,15 +1,6 @@
package core
type Config struct {
Password PasswordConfig `koanf:"password"`
BaseDir string `koanf:"basedir"`
Permisison UserPermissions `koanf:"permission"`
}
type PasswordConfig struct {
Length int `koanf:"length"`
Lower int `koanf:"lower"`
Upper int `koanf:"upper"`
Numeric int `koanf:"numeric"`
Symbols int `koanf:"symbols"`
}

View File

@@ -91,17 +91,9 @@ type UserManager interface {
UserEmailByID(id int) (string, error)
UserHome(email string) (pgtype.UUID, error)
// user_auth.go
VerifyUserPassword(email, password string) (User, error)
CreateAccessToken(user User) (string, error)
ReadAccessToken(accessToken string) (User, error)
CreateResetToken(user User) (string, error)
ResetUserPassword(user User, token, password string) error
// user_update.go
UpdateUserHome(user User, home pgtype.UUID) error
UpdateUserName(user User, name string) error
UpdateUserPassword(user User, password string) error
GrantUserPermissions(user User, permissions UserPermissions) error
RevokeUserPermissions(user User, permissions UserPermissions) error

View File

@@ -5,7 +5,7 @@ import (
"errors"
"time"
"codeberg.org/shroff/phylum/server/internal/crypt"
"codeberg.org/shroff/phylum/server/internal/auth/crypt"
"codeberg.org/shroff/phylum/server/internal/db"
"github.com/google/uuid"
"github.com/jackc/pgx/v5"

View File

@@ -1,57 +0,0 @@
package core
import (
"net/http"
"strconv"
)
type charType int
const (
charTypeOther charType = iota
charTypeLower
charTypeUpper
charTypeNumeric
charTypeSymbol
)
var charTypes = map[rune]charType{}
func init() {
for _, c := range "abcdefghijklmnopqrstuvwxyz" {
charTypes[c] = charTypeLower
}
for _, c := range "ABCDEFGHIJKLMNOPQRSTUVWXYZ" {
charTypes[c] = charTypeUpper
}
for _, c := range "0123456789" {
charTypes[c] = charTypeNumeric
}
for _, c := range "`~!@#$%^&*()-_=+[]{}\\|;:'\",.<>/?" {
charTypes[c] = charTypeSymbol
}
}
func checkPasswordStrength(password string) error {
if len(password) < Cfg.Password.Length {
return NewError(http.StatusBadRequest, "password_invalid", "Must be at least "+strconv.Itoa(Cfg.Password.Length)+" characters long.")
}
count := map[charType]int{}
for _, c := range password {
count[charTypes[c]]++
}
if count[charTypeLower] < Cfg.Password.Lower {
return NewError(http.StatusBadRequest, "password_invalid", "Must have at least "+strconv.Itoa(Cfg.Password.Lower)+" lower case.")
}
if count[charTypeUpper] < Cfg.Password.Upper {
return NewError(http.StatusBadRequest, "password_invalid", "Must have at least "+strconv.Itoa(Cfg.Password.Upper)+" upper case.")
}
if count[charTypeNumeric] < Cfg.Password.Numeric {
return NewError(http.StatusBadRequest, "password_invalid", "Must have at least "+strconv.Itoa(Cfg.Password.Numeric)+" numeric.")
}
if count[charTypeSymbol] < Cfg.Password.Symbols {
return NewError(http.StatusBadRequest, "password_invalid", "Must have at least "+strconv.Itoa(Cfg.Password.Symbols)+" symbols.")
}
return nil
}

View File

@@ -3,7 +3,7 @@ package core
import (
"strings"
"codeberg.org/shroff/phylum/server/internal/crypt"
"codeberg.org/shroff/phylum/server/internal/auth/crypt"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgtype"
)

View File

@@ -1,118 +0,0 @@
package core
import (
"net/http"
"time"
"codeberg.org/shroff/phylum/server/internal/crypt"
"codeberg.org/shroff/phylum/server/internal/db"
"codeberg.org/shroff/phylum/server/internal/rand"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgtype"
)
const accessTokenLength = 16
const resetTokenLength = 64
const resetTokenDuration = 10 * time.Minute
var accessTokenValidity = pgtype.Interval{
Days: 30,
Valid: true,
}
var ErrCredentialsInvalid = NewError(http.StatusUnauthorized, "credentials_invalid", "invalid Credentials")
var errTokenNotFound = NewError(http.StatusBadRequest, "token_not_found", "Token Not Found")
func (m manager) VerifyUserPassword(email, password string) (User, error) {
if user, passwordHash, err := m.userPasswordHashByEmail(email); err != nil {
if Is(err, pgx.ErrNoRows) {
return User{}, ErrCredentialsInvalid
}
return User{}, err
} else if passwordHash == "" {
return User{}, ErrCredentialsInvalid
} else {
if b, err := crypt.Verify(password, passwordHash); err != nil {
return User{}, err
} else if !b {
return User{}, ErrCredentialsInvalid
}
return user, nil
}
}
func (m manager) CreateAccessToken(user User) (string, error) {
const q = `INSERT INTO access_tokens(id, expires, user_id) VALUES ($1::TEXT, NOW() + $2::INTERVAL, $3::INT)`
id := rand.GenerateRandomString(accessTokenLength)
if _, err := m.db.Exec(q, id, accessTokenValidity, user.ID); err != nil {
return "", err
} else {
return id, nil
}
}
func (m manager) ReadAccessToken(accessToken string) (user User, err error) {
const q = `SELECT t.expires, u.id, u.email, u.name, u.permissions, u.home FROM access_tokens t JOIN users u ON t.user_id = u.id WHERE t.id = $1; `
row := m.db.QueryRow(q, accessToken)
var expires pgtype.Timestamp
err = row.Scan(&expires, &user.ID, &user.Email, &user.Name, &user.Permissions, &user.Home)
if err != nil {
if Is(err, pgx.ErrNoRows) {
err = ErrCredentialsInvalid
}
} else if time.Now().After(expires.Time) {
return User{}, ErrCredentialsInvalid
}
return
}
func (m manager) CreateResetToken(user User) (string, error) {
const q = `INSERT INTO reset_tokens(user_id, token, expires)
VALUES (@user_id::INT, @token::TEXT, @expires::TIMESTAMP)
ON CONFLICT(user_id) DO UPDATE SET token = @token::TEXT, expires = @expires::TIMESTAMP`
token := rand.GenerateRandomString(resetTokenLength)
args := pgx.NamedArgs{
"user_id": user.ID,
"token": token,
"expires": time.Now().Add(resetTokenDuration),
}
if _, err := m.db.Exec(q, args); err != nil {
return "", err
}
return token, nil
}
func (m manager) ResetUserPassword(user User, token, password string) error {
const q = `DELETE FROM reset_tokens WHERE user_id = @user_id::INT AND token = @token::TEXT RETURNING expires`
args := pgx.NamedArgs{
"user_id": user.ID,
"token": token,
"expires": time.Now().Add(resetTokenDuration),
}
return m.db.RunInTx(func(d db.Handler) error {
m := m.withDb(d)
// UpdateUserPassword will ensure the password strength
// Not incorrect to do this before token verification because we are in a transaction.
// TODO: Are there perf implications for this in case of malicious actors?
err := m.UpdateUserPassword(user, password)
if err != nil {
return err
}
row := m.db.QueryRow(q, args)
var expires pgtype.Timestamp
if err := row.Scan(&expires); err == nil {
if time.Now().After(expires.Time) {
return errTokenNotFound
}
return nil
} else if Is(err, pgx.ErrNoRows) {
return errTokenNotFound
} else {
return err
}
})
}

View File

@@ -137,19 +137,3 @@ RETURNING id, email, name, home, permissions`
return user, err
}
}
func (m manager) userPasswordHashByEmail(email string) (user User, passwordHash string, err error) {
const q = "SELECT id, email, name, home, permissions, password_hash FROM users WHERE email = $1"
row := m.db.QueryRow(q, strings.ToLower(email))
err = row.Scan(
&user.ID,
&user.Email,
&user.Name,
&user.Home,
&user.Permissions,
&passwordHash)
if Is(err, pgx.ErrNoRows) {
err = ErrUserNotFound
}
return
}

View File

@@ -1,7 +1,6 @@
package core
import (
"codeberg.org/shroff/phylum/server/internal/crypt"
"github.com/jackc/pgx/v5/pgtype"
)
@@ -21,22 +20,6 @@ func (m manager) UpdateUserName(user User, name string) error {
return nil
}
func (m manager) UpdateUserPassword(user User, password string) error {
if err := checkPasswordStrength(password); err != nil {
return err
}
const q = "UPDATE users SET password_hash = $2::TEXT, modified = NOW() WHERE id = $1::INT"
if hash, err := crypt.Generate(password); err != nil {
return err
} else {
if _, err := m.db.Exec(q, user.ID, hash); err != nil {
return err
}
}
return nil
}
func (m manager) GrantUserPermissions(user User, permissions UserPermissions) error {
const q = "UPDATE users SET permissions = permissions | $2::INTEGER, modified = NOW() WHERE id = $1::INT"
if _, err := m.db.Exec(q, user.ID, permissions); err != nil {