mirror of
https://github.com/abhinavxd/libredesk.git
synced 2025-10-23 05:11:57 +00:00
feat: dockerize libredesk
- feat: new flag for idempotent installation `--idempotent-install` - feat: new flag to skip yes/no prompt `--yes` - feat: new flag for upgrades `--upgrade` - refactor: update doc strings and sample toml file. - chore: update .gitignore.
This commit is contained in:
6
.gitignore
vendored
6
.gitignore
vendored
@@ -1,5 +1,7 @@
|
|||||||
node_modules
|
node_modules
|
||||||
config.toml
|
config.toml
|
||||||
|
config.toml.*
|
||||||
libredesk.bin
|
libredesk.bin
|
||||||
uploads/*
|
uploads
|
||||||
.env
|
.env
|
||||||
|
dist/
|
17
Dockerfile
Normal file
17
Dockerfile
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# Use the latest version of Alpine Linux as the base image
|
||||||
|
FROM alpine:latest
|
||||||
|
|
||||||
|
# Install necessary packages
|
||||||
|
RUN apk --no-cache add ca-certificates
|
||||||
|
|
||||||
|
# Set the working directory to /libredesk
|
||||||
|
WORKDIR /libredesk
|
||||||
|
|
||||||
|
# Copy the libredesk binary to the working directory
|
||||||
|
COPY libredesk.bin .
|
||||||
|
|
||||||
|
# Expose port 9000 for the application
|
||||||
|
EXPOSE 9000
|
||||||
|
|
||||||
|
# Set the default command to run the libredesk binary
|
||||||
|
CMD ["./libredesk.bin"]
|
@@ -98,6 +98,9 @@ func initFlags() {
|
|||||||
"path to one or more config files (will be merged in order)")
|
"path to one or more config files (will be merged in order)")
|
||||||
f.Bool("version", false, "show current version of the build")
|
f.Bool("version", false, "show current version of the build")
|
||||||
f.Bool("install", false, "setup database")
|
f.Bool("install", false, "setup database")
|
||||||
|
f.Bool("idempotent-install", false, "run idempotent installation, i.e., skip installion if schema is already installed useful for the first time setup")
|
||||||
|
f.Bool("yes", false, "skip confirmation prompt")
|
||||||
|
f.Bool("upgrade", false, "upgrade the database schema")
|
||||||
f.Bool("set-system-user-password", false, "set password for the system user")
|
f.Bool("set-system-user-password", false, "set password for the system user")
|
||||||
|
|
||||||
if err := f.Parse(os.Args[1:]); err != nil {
|
if err := f.Parse(os.Args[1:]); err != nil {
|
||||||
|
@@ -4,23 +4,38 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/abhinavxd/libredesk/internal/colorlog"
|
||||||
"github.com/abhinavxd/libredesk/internal/user"
|
"github.com/abhinavxd/libredesk/internal/user"
|
||||||
"github.com/jmoiron/sqlx"
|
"github.com/jmoiron/sqlx"
|
||||||
"github.com/knadh/stuffbin"
|
"github.com/knadh/stuffbin"
|
||||||
"github.com/lib/pq"
|
"github.com/lib/pq"
|
||||||
)
|
)
|
||||||
|
|
||||||
// install checks if the schema is already installed, prompts for confirmation, and installs the schema if needed.
|
// Install checks if the schema is already installed, prompts for confirmation, and installs the schema if needed.
|
||||||
func install(ctx context.Context, db *sqlx.DB, fs stuffbin.FileSystem) error {
|
// idempotent install skips the installation if the database schema is already installed.
|
||||||
installed, err := checkSchema(db)
|
func install(ctx context.Context, db *sqlx.DB, fs stuffbin.FileSystem, idempotentInstall, prompt bool) error {
|
||||||
|
schemaInstalled, err := checkSchema(db)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("error checking db schema: %v", err)
|
log.Fatalf("error checking existing db schema: %v", err)
|
||||||
}
|
}
|
||||||
if installed {
|
|
||||||
fmt.Printf("\033[31m** WARNING: This will wipe your entire database - '%s' **\033[0m\n", ko.String("db.database"))
|
// Make sure the system user password is strong enough.
|
||||||
fmt.Print("Continue (y/n)? ")
|
password := strings.TrimSpace(os.Getenv("LIBREDESK_SYSTEM_USER_PASSWORD"))
|
||||||
|
if password != "" && !user.IsStrongSystemUserPassword(password) && !schemaInstalled {
|
||||||
|
log.Fatalf("system user password is not strong, %s", user.SystemUserPasswordHint)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !idempotentInstall {
|
||||||
|
log.Println("running first time setup...")
|
||||||
|
colorlog.Red(fmt.Sprintf("WARNING: This will wipe your entire database - '%s'", ko.String("db.database")))
|
||||||
|
}
|
||||||
|
|
||||||
|
if prompt {
|
||||||
|
log.Print("Continue (y/n)? ")
|
||||||
var ok string
|
var ok string
|
||||||
fmt.Scanf("%s", &ok)
|
fmt.Scanf("%s", &ok)
|
||||||
if !strings.EqualFold(ok, "y") {
|
if !strings.EqualFold(ok, "y") {
|
||||||
@@ -28,15 +43,26 @@ func install(ctx context.Context, db *sqlx.DB, fs stuffbin.FileSystem) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if idempotentInstall {
|
||||||
|
if schemaInstalled {
|
||||||
|
log.Println("skipping installation as schema is already installed")
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
time.Sleep(5 * time.Second)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("installing database schema...")
|
||||||
|
|
||||||
// Install schema.
|
// Install schema.
|
||||||
if err := installSchema(db, fs); err != nil {
|
if err := installSchema(db, fs); err != nil {
|
||||||
log.Fatalf("error installing schema: %v", err)
|
log.Fatalf("error installing schema: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Println("Schema installed successfully")
|
log.Println("database schema installed successfully")
|
||||||
|
|
||||||
// Create system user.
|
// Create system user.
|
||||||
if err := user.CreateSystemUser(ctx, db); err != nil {
|
if err := user.CreateSystemUser(ctx, password, db); err != nil {
|
||||||
log.Fatalf("error creating system user: %v", err)
|
log.Fatalf("error creating system user: %v", err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
10
cmd/main.go
10
cmd/main.go
@@ -114,7 +114,7 @@ func main() {
|
|||||||
|
|
||||||
// Installer.
|
// Installer.
|
||||||
if ko.Bool("install") {
|
if ko.Bool("install") {
|
||||||
install(ctx, db, fs)
|
install(ctx, db, fs, ko.Bool("idempotent-install"), !ko.Bool("yes"))
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -130,7 +130,13 @@ func main() {
|
|||||||
log.Fatalf("error checking db schema: %v", err)
|
log.Fatalf("error checking db schema: %v", err)
|
||||||
}
|
}
|
||||||
if !installed {
|
if !installed {
|
||||||
log.Println("Database tables are missing. Use the `--install` flag to set up the database schema.")
|
log.Println("database tables are missing. Use the `--install` flag to set up the database schema.")
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Upgrade.
|
||||||
|
if ko.Bool("upgrade") {
|
||||||
|
log.Println("no upgrades available")
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -12,13 +12,13 @@ write_timeout = "5s"
|
|||||||
max_body_size = 500000000
|
max_body_size = 500000000
|
||||||
keepalive_timeout = "10s"
|
keepalive_timeout = "10s"
|
||||||
|
|
||||||
# File upload provider.
|
# File upload provider to use.
|
||||||
[upload]
|
[upload]
|
||||||
provider = "fs"
|
provider = "fs"
|
||||||
|
|
||||||
# Filesytem provider.
|
# Filesytem provider.
|
||||||
[upload.fs]
|
[upload.fs]
|
||||||
upload_path = '/home/ubuntu/uploads'
|
upload_path = 'uploads'
|
||||||
|
|
||||||
# S3 provider.
|
# S3 provider.
|
||||||
[upload.s3]
|
[upload.s3]
|
||||||
@@ -32,6 +32,7 @@ expiry = "6h"
|
|||||||
|
|
||||||
# Postgres.
|
# Postgres.
|
||||||
[db]
|
[db]
|
||||||
|
# If using docker compose, use the service name as the host.
|
||||||
host = "127.0.0.1"
|
host = "127.0.0.1"
|
||||||
port = 5432
|
port = 5432
|
||||||
user = "postgres"
|
user = "postgres"
|
||||||
@@ -44,6 +45,7 @@ max_lifetime = "300s"
|
|||||||
|
|
||||||
# Redis.
|
# Redis.
|
||||||
[redis]
|
[redis]
|
||||||
|
# If using docker compose, use the service name as the host.
|
||||||
address = "127.0.0.1:6379"
|
address = "127.0.0.1:6379"
|
||||||
password = ""
|
password = ""
|
||||||
db = 0
|
db = 0
|
||||||
|
62
docker-compose.yml
Normal file
62
docker-compose.yml
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
services:
|
||||||
|
# Libredesk app
|
||||||
|
app:
|
||||||
|
image: libredesk:latest
|
||||||
|
container_name: libredesk_app
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "9000:9000"
|
||||||
|
environment:
|
||||||
|
# If the password is set during first docker-compose up, the system user password will be set to this value.
|
||||||
|
# You can always set system user password later by running `docker exec -it libredesk_app ./libredesk.bin --set-system-user-password`.
|
||||||
|
LIBREDESK_SYSTEM_USER_PASSWORD: ${LIBREDESK_SYSTEM_USER_PASSWORD:-}
|
||||||
|
networks:
|
||||||
|
- libredesk
|
||||||
|
depends_on:
|
||||||
|
- db
|
||||||
|
- redis
|
||||||
|
volumes:
|
||||||
|
- ./uploads:/libredesk/uploads:rw
|
||||||
|
- ./config.toml:/libredesk/config.toml
|
||||||
|
command: [sh, -c, "./libredesk.bin --install --idempotent-install --yes --config /libredesk/config.toml && ./libredesk.bin --upgrade --yes --config /libredesk/config.toml && ./libredesk.bin --config /libredesk/config.toml"]
|
||||||
|
|
||||||
|
# PostgreSQL database
|
||||||
|
db:
|
||||||
|
image: postgres:17-alpine
|
||||||
|
container_name: libredesk_db
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- libredesk
|
||||||
|
ports:
|
||||||
|
- "5432:5432"
|
||||||
|
environment:
|
||||||
|
# Set these environment variables to configure the database, defaults to libredesk.
|
||||||
|
POSTGRES_USER: ${POSTGRES_USER:-libredesk}
|
||||||
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-libredesk}
|
||||||
|
POSTGRES_DB: ${POSTGRES_DB:-libredesk}
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U libredesk"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 6
|
||||||
|
volumes:
|
||||||
|
- postgres-data:/var/lib/postgresql/data
|
||||||
|
|
||||||
|
# Redis
|
||||||
|
redis:
|
||||||
|
image: redis:7-alpine
|
||||||
|
container_name: libredesk_redis
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "6379:6379"
|
||||||
|
networks:
|
||||||
|
- libredesk
|
||||||
|
volumes:
|
||||||
|
- redis-data:/data
|
||||||
|
|
||||||
|
networks:
|
||||||
|
libredesk:
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
postgres-data:
|
||||||
|
redis-data:
|
@@ -289,6 +289,7 @@ func (a *Auth) SetCSRFCookie(r *fastglue.Request) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ValidateSession validates the session and returns the user.
|
||||||
func (a *Auth) ValidateSession(r *fastglue.Request) (models.User, error) {
|
func (a *Auth) ValidateSession(r *fastglue.Request) (models.User, error) {
|
||||||
a.mu.RLock()
|
a.mu.RLock()
|
||||||
defer a.mu.RUnlock()
|
defer a.mu.RUnlock()
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
// package colorlog provides logging in color.
|
// package colorlog provides ANSI color logging for the terminal.
|
||||||
package colorlog
|
package colorlog
|
||||||
|
|
||||||
import "log"
|
import "log"
|
||||||
|
@@ -54,7 +54,7 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Run starts a pool of worker goroutines to handle message dispatching via inbox's channel and processes incoming messages. It scans for
|
// Run starts a pool of worker goroutines to handle message dispatching via inbox's channel and processes incoming messages. It scans for
|
||||||
// pending outgoing messages at the specified read interval and pushes them to the outgoing queue.
|
// pending outgoing messages at the specified read interval and pushes them to the outgoing queue to be sent.
|
||||||
func (m *Manager) Run(ctx context.Context, incomingQWorkers, outgoingQWorkers, scanInterval time.Duration) {
|
func (m *Manager) Run(ctx context.Context, incomingQWorkers, outgoingQWorkers, scanInterval time.Duration) {
|
||||||
dbScanner := time.NewTicker(scanInterval)
|
dbScanner := time.NewTicker(scanInterval)
|
||||||
defer dbScanner.Stop()
|
defer dbScanner.Stop()
|
||||||
|
@@ -11,6 +11,8 @@ import (
|
|||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"log"
|
||||||
|
|
||||||
"github.com/abhinavxd/libredesk/internal/dbutil"
|
"github.com/abhinavxd/libredesk/internal/dbutil"
|
||||||
"github.com/abhinavxd/libredesk/internal/envelope"
|
"github.com/abhinavxd/libredesk/internal/envelope"
|
||||||
rmodels "github.com/abhinavxd/libredesk/internal/role/models"
|
rmodels "github.com/abhinavxd/libredesk/internal/role/models"
|
||||||
@@ -24,6 +26,14 @@ import (
|
|||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
systemUserEmail = "System"
|
||||||
|
minSystemUserPassword = 8
|
||||||
|
maxSystemUserPassword = 50
|
||||||
|
UserTypeAgent = "agent"
|
||||||
|
UserTypeContact = "contact"
|
||||||
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
//go:embed queries.sql
|
//go:embed queries.sql
|
||||||
efs embed.FS
|
efs embed.FS
|
||||||
@@ -31,14 +41,8 @@ var (
|
|||||||
// ErrPasswordTooLong is returned when the password passed to
|
// ErrPasswordTooLong is returned when the password passed to
|
||||||
// GenerateFromPassword is too long (i.e. > 72 bytes).
|
// GenerateFromPassword is too long (i.e. > 72 bytes).
|
||||||
ErrPasswordTooLong = errors.New("password length exceeds 72 bytes")
|
ErrPasswordTooLong = errors.New("password length exceeds 72 bytes")
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
SystemUserPasswordHint = fmt.Sprintf("Password must be %d-%d characters long and contain at least one uppercase letter and one number", minSystemUserPassword, maxSystemUserPassword)
|
||||||
SystemUserEmail = "System"
|
|
||||||
MinSystemUserPasswordLen = 8
|
|
||||||
MaxSystemUserPasswordLen = 50
|
|
||||||
UserTypeAgent = "agent"
|
|
||||||
UserTypeContact = "contact"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Manager handles user-related operations.
|
// Manager handles user-related operations.
|
||||||
@@ -179,7 +183,7 @@ func (u *Manager) GetByEmail(email string) (models.User, error) {
|
|||||||
|
|
||||||
// GetSystemUser retrieves the system user.
|
// GetSystemUser retrieves the system user.
|
||||||
func (u *Manager) GetSystemUser() (models.User, error) {
|
func (u *Manager) GetSystemUser() (models.User, error) {
|
||||||
return u.GetByEmail(SystemUserEmail)
|
return u.GetByEmail(systemUserEmail)
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateAvatar updates the user avatar.
|
// UpdateAvatar updates the user avatar.
|
||||||
@@ -200,7 +204,7 @@ func (u *Manager) Update(id int, user models.User) error {
|
|||||||
|
|
||||||
if user.NewPassword != "" {
|
if user.NewPassword != "" {
|
||||||
if !u.isStrongPassword(user.NewPassword) {
|
if !u.isStrongPassword(user.NewPassword) {
|
||||||
return envelope.NewError(envelope.InputError, "Entered password is not strong please make sure the password has min 8, max 50 characters, at least 1 uppercase letter, 1 number", nil)
|
return envelope.NewError(envelope.InputError, SystemUserPasswordHint, nil)
|
||||||
}
|
}
|
||||||
hashedPassword, err = bcrypt.GenerateFromPassword([]byte(user.NewPassword), bcrypt.DefaultCost)
|
hashedPassword, err = bcrypt.GenerateFromPassword([]byte(user.NewPassword), bcrypt.DefaultCost)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -265,7 +269,7 @@ func (u *Manager) SetResetPasswordToken(id int) (string, error) {
|
|||||||
// ResetPassword sets a new password for a user.
|
// ResetPassword sets a new password for a user.
|
||||||
func (u *Manager) ResetPassword(token, password string) error {
|
func (u *Manager) ResetPassword(token, password string) error {
|
||||||
if !u.isStrongPassword(password) {
|
if !u.isStrongPassword(password) {
|
||||||
return envelope.NewError(envelope.InputError, "Entered password is not strong please make sure the password has min 8, max 50 characters, at least 1 uppercase letter, 1 number", nil)
|
return envelope.NewError(envelope.InputError, "Password is not strong enough, " + SystemUserPasswordHint, nil)
|
||||||
}
|
}
|
||||||
// Hash password.
|
// Hash password.
|
||||||
passwordHash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
passwordHash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||||
@@ -311,7 +315,7 @@ func (u *Manager) generatePassword() ([]byte, error) {
|
|||||||
|
|
||||||
// isStrongPassword checks if the password meets the required strength.
|
// isStrongPassword checks if the password meets the required strength.
|
||||||
func (u *Manager) isStrongPassword(password string) bool {
|
func (u *Manager) isStrongPassword(password string) bool {
|
||||||
if len(password) < MinSystemUserPasswordLen || len(password) > MaxSystemUserPasswordLen {
|
if len(password) < minSystemUserPassword || len(password) > maxSystemUserPassword {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
hasUppercase := regexp.MustCompile(`[A-Z]`).MatchString(password)
|
hasUppercase := regexp.MustCompile(`[A-Z]`).MatchString(password)
|
||||||
@@ -331,16 +335,29 @@ func ChangeSystemUserPassword(ctx context.Context, db *sqlx.DB) error {
|
|||||||
if err := updateSystemUserPassword(db, hashedPassword); err != nil {
|
if err := updateSystemUserPassword(db, hashedPassword); err != nil {
|
||||||
return fmt.Errorf("error updating system user password: %v", err)
|
return fmt.Errorf("error updating system user password: %v", err)
|
||||||
}
|
}
|
||||||
fmt.Println("System user password updated successfully.")
|
fmt.Println("password updated successfully.")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateSystemUser inserts a default system user into the users table with the prompted password.
|
// CreateSystemUser creates a system user with the provided password or a random one.
|
||||||
func CreateSystemUser(ctx context.Context, db *sqlx.DB) error {
|
func CreateSystemUser(ctx context.Context, password string, db *sqlx.DB) error {
|
||||||
hashedPassword, err := promptAndHashPassword(ctx)
|
var err error
|
||||||
if err != nil {
|
|
||||||
return err
|
// Set random password if not provided.
|
||||||
|
if password == "" {
|
||||||
|
password, err = stringutil.RandomAlphanumeric(32)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to generate system used password: %v", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.Print("using provided password for system user")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to hash system user password: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
_, err = db.Exec(`
|
_, err = db.Exec(`
|
||||||
WITH sys_user AS (
|
WITH sys_user AS (
|
||||||
INSERT INTO users (email, type, first_name, last_name, password)
|
INSERT INTO users (email, type, first_name, last_name, password)
|
||||||
@@ -351,14 +368,24 @@ func CreateSystemUser(ctx context.Context, db *sqlx.DB) error {
|
|||||||
SELECT sys_user.id, roles.id
|
SELECT sys_user.id, roles.id
|
||||||
FROM sys_user, roles
|
FROM sys_user, roles
|
||||||
WHERE roles.name = $6`,
|
WHERE roles.name = $6`,
|
||||||
SystemUserEmail, UserTypeAgent, "System", "", hashedPassword, rmodels.RoleAdmin)
|
systemUserEmail, UserTypeAgent, "System", "", hashedPassword, rmodels.RoleAdmin)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to create system user: %v", err)
|
return fmt.Errorf("failed to create system user: %v", err)
|
||||||
}
|
}
|
||||||
fmt.Println("System user created successfully")
|
log.Print("system user created successfully")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsStrongSystemUserPassword checks if the password meets the required strength for system user.
|
||||||
|
func IsStrongSystemUserPassword(password string) bool {
|
||||||
|
if len(password) < minSystemUserPassword || len(password) > maxSystemUserPassword {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
hasUppercase := regexp.MustCompile(`[A-Z]`).MatchString(password)
|
||||||
|
hasNumber := regexp.MustCompile(`[0-9]`).MatchString(password)
|
||||||
|
return hasUppercase && hasNumber
|
||||||
|
}
|
||||||
|
|
||||||
// promptAndHashPassword handles password input and validation, and returns the hashed password.
|
// promptAndHashPassword handles password input and validation, and returns the hashed password.
|
||||||
func promptAndHashPassword(ctx context.Context) ([]byte, error) {
|
func promptAndHashPassword(ctx context.Context) ([]byte, error) {
|
||||||
for {
|
for {
|
||||||
@@ -366,15 +393,14 @@ func promptAndHashPassword(ctx context.Context) ([]byte, error) {
|
|||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
return nil, ctx.Err()
|
return nil, ctx.Err()
|
||||||
default:
|
default:
|
||||||
fmt.Print("Please set System admin password (min 8, max 50 characters, at least 1 uppercase letter, 1 number): ")
|
fmt.Printf("Please set System user password (%s): ", SystemUserPasswordHint)
|
||||||
buffer := make([]byte, 256)
|
buffer := make([]byte, 256)
|
||||||
n, err := os.Stdin.Read(buffer)
|
n, err := os.Stdin.Read(buffer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("error reading input: %v", err)
|
return nil, fmt.Errorf("error reading input: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
password := strings.TrimSpace(string(buffer[:n]))
|
password := strings.TrimSpace(string(buffer[:n]))
|
||||||
if isStrongSystemUserPassword(password) {
|
if IsStrongSystemUserPassword(password) {
|
||||||
// Hash the password using bcrypt.
|
// Hash the password using bcrypt.
|
||||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -389,19 +415,9 @@ func promptAndHashPassword(ctx context.Context) ([]byte, error) {
|
|||||||
|
|
||||||
// updateSystemUserPassword updates the password of the system user in the database.
|
// updateSystemUserPassword updates the password of the system user in the database.
|
||||||
func updateSystemUserPassword(db *sqlx.DB, hashedPassword []byte) error {
|
func updateSystemUserPassword(db *sqlx.DB, hashedPassword []byte) error {
|
||||||
_, err := db.Exec(`UPDATE users SET password = $1 WHERE email = $2`, hashedPassword, SystemUserEmail)
|
_, err := db.Exec(`UPDATE users SET password = $1 WHERE email = $2`, hashedPassword, systemUserEmail)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to update system user password: %v", err)
|
return fmt.Errorf("failed to update system user password: %v", err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// isStrongSystemUserPassword checks if the password meets the required strength for system user.
|
|
||||||
func isStrongSystemUserPassword(password string) bool {
|
|
||||||
if len(password) < MinSystemUserPasswordLen || len(password) > MaxSystemUserPasswordLen {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
hasUppercase := regexp.MustCompile(`[A-Z]`).MatchString(password)
|
|
||||||
hasNumber := regexp.MustCompile(`[0-9]`).MatchString(password)
|
|
||||||
return hasUppercase && hasNumber
|
|
||||||
}
|
|
||||||
|
Reference in New Issue
Block a user