Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 24 additions & 2 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,35 @@
"version": "0.2.0",
"configurations": [
{
"name": "Launch Lemma Server",
"name": "Launch Backend",
"type": "go",
"request": "launch",
"mode": "auto",
"program": "${workspaceFolder}/server/cmd/server/main.go",
"cwd": "${workspaceFolder}",
"envFile": "${workspaceFolder}/server/.env"
"envFile": "${workspaceFolder}/.env.local"
},
{
"name": "Launch Frontend",
"type": "node",
"request": "launch",
"runtimeExecutable": "npm",
"runtimeArgs": ["start"],
"cwd": "${workspaceFolder}/app",
"console": "integratedTerminal",
"envFile": "${workspaceFolder}/.env.local"
}
],
"compounds": [
{
"name": "Launch Backend + Frontend",
"configurations": ["Launch Backend", "Launch Frontend"],
"presentation": {
"hidden": false,
"group": "",
"order": 1
},
"stopAll": true
}
]
}
15 changes: 5 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ Lemma can be configured using environment variables. Here are the available conf

- `LEMMA_ADMIN_EMAIL`: Email address for the admin account
- `LEMMA_ADMIN_PASSWORD`: Password for the admin account
- `LEMMA_ENCRYPTION_KEY`: Base64-encoded 32-byte key used for encrypting sensitive data

### Optional Environment Variables

Expand All @@ -39,21 +38,17 @@ Lemma can be configured using environment variables. Here are the available conf
- `LEMMA_PORT`: Port to run the server on (default: "8080")
- `LEMMA_DOMAIN`: Domain name where the application is hosted for cookie authentication
- `LEMMA_CORS_ORIGINS`: Comma-separated list of allowed CORS origins
- `LEMMA_JWT_SIGNING_KEY`: Key used for signing JWT tokens
- `LEMMA_ENCRYPTION_KEY`: Base64-encoded 32-byte key used for encrypting sensitive data. If not provided, a key will be automatically generated and stored in `{LEMMA_WORKDIR}/secrets/encryption_key`
- `LEMMA_JWT_SIGNING_KEY`: Key used for signing JWT tokens. If not provided, a key will be automatically generated and stored in `{LEMMA_WORKDIR}/secrets/jwt_signing_key`
- `LEMMA_LOG_LEVEL`: Logging level (defaults to DEBUG in development mode, INFO in production)
- `LEMMA_RATE_LIMIT_REQUESTS`: Number of allowed requests per window (default: 100)
- `LEMMA_RATE_LIMIT_WINDOW`: Duration of the rate limit window (default: 15m)

### Generating Encryption Keys
### Security Keys

The encryption key must be a base64-encoded 32-byte value. You can generate a secure encryption key using OpenSSL:
Both the encryption key and JWT signing key are automatically generated on first startup if not provided via environment variables. The keys are stored in `{LEMMA_WORKDIR}/secrets/` with restrictive file permissions (0600).

```bash
# Generate a random 32-byte key and encode it as base64
openssl rand -base64 32
```

Store the generated key securely - it will be needed to decrypt any data encrypted by the application. If the key is lost or changed, previously encrypted data will become inaccessible.
**Important**: Back up the `secrets` directory! If these keys are lost, encrypted data will become inaccessible and all users will need to re-authenticate.

## Running the backend server

Expand Down
6 changes: 3 additions & 3 deletions app/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 12 additions & 4 deletions server/internal/app/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,20 @@ func DefaultConfig() *Config {
// validate checks if the configuration is valid
func (c *Config) validate() error {
if c.AdminEmail == "" || c.AdminPassword == "" {
return fmt.Errorf("LEMMA_ADMIN_EMAIL and LEMMA_ADMIN_PASSWORD must be set")
return fmt.Errorf(`admin credentials not configured

To get started, set these environment variables:
export LEMMA_ADMIN_EMAIL="[email protected]"
export LEMMA_ADMIN_PASSWORD="your-secure-password"

Then start the server again.`)
}

// Validate encryption key
if err := secrets.ValidateKey(c.EncryptionKey); err != nil {
return fmt.Errorf("invalid LEMMA_ENCRYPTION_KEY: %w", err)
// Validate encryption key if provided (if not provided, it will be auto-generated)
if c.EncryptionKey != "" {
if err := secrets.ValidateKey(c.EncryptionKey); err != nil {
return fmt.Errorf("invalid LEMMA_ENCRYPTION_KEY: %w", err)
}
}

return nil
Expand Down
25 changes: 14 additions & 11 deletions server/internal/app/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,13 @@ func TestLoad(t *testing.T) {
setEnv(t, "LEMMA_ADMIN_PASSWORD", "password123")
setEnv(t, "LEMMA_ENCRYPTION_KEY", "YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXoxMjM0NTY=")
},
expectedError: "LEMMA_ADMIN_EMAIL and LEMMA_ADMIN_PASSWORD must be set",
expectedError: `admin credentials not configured

To get started, set these environment variables:
export LEMMA_ADMIN_EMAIL="[email protected]"
export LEMMA_ADMIN_PASSWORD="your-secure-password"

Then start the server again.`,
},
{
name: "missing admin password",
Expand All @@ -177,16 +183,13 @@ func TestLoad(t *testing.T) {
setEnv(t, "LEMMA_ADMIN_EMAIL", "[email protected]")
setEnv(t, "LEMMA_ENCRYPTION_KEY", "YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXoxMjM0NTY=")
},
expectedError: "LEMMA_ADMIN_EMAIL and LEMMA_ADMIN_PASSWORD must be set",
},
{
name: "missing encryption key",
setupEnv: func(t *testing.T) {
cleanup()
setEnv(t, "LEMMA_ADMIN_EMAIL", "[email protected]")
setEnv(t, "LEMMA_ADMIN_PASSWORD", "password123")
},
expectedError: "invalid LEMMA_ENCRYPTION_KEY: encryption key is required",
expectedError: `admin credentials not configured

To get started, set these environment variables:
export LEMMA_ADMIN_EMAIL="[email protected]"
export LEMMA_ADMIN_PASSWORD="your-secure-password"

Then start the server again.`,
},
{
name: "invalid encryption key",
Expand Down
26 changes: 22 additions & 4 deletions server/internal/app/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,22 @@ import (
// initSecretsService initializes the secrets service
func initSecretsService(cfg *Config) (secrets.Service, error) {
logging.Debug("initializing secrets service")
secretsService, err := secrets.NewService(cfg.EncryptionKey)

// Get or generate encryption key
encryptionKey := cfg.EncryptionKey
if encryptionKey == "" {
logging.Debug("no encryption key provided, loading/generating from file")

// Load or generate key from file
secretsDir := cfg.WorkDir + "/secrets"
var err error
encryptionKey, err = secrets.EnsureEncryptionKey(secretsDir)
if err != nil {
return nil, fmt.Errorf("failed to ensure encryption key: %w", err)
}
}

secretsService, err := secrets.NewService(encryptionKey)
if err != nil {
return nil, fmt.Errorf("failed to initialize secrets service: %w", err)
}
Expand Down Expand Up @@ -52,11 +67,14 @@ func initAuth(cfg *Config, database db.Database) (auth.JWTManager, auth.SessionM
// Get or generate JWT signing key
signingKey := cfg.JWTSigningKey
if signingKey == "" {
logging.Debug("no JWT signing key provided, generating new key")
logging.Debug("no JWT signing key provided, loading/generating from file")

// Load or generate key from file
secretsDir := cfg.WorkDir + "/secrets"
var err error
signingKey, err = database.EnsureJWTSecret()
signingKey, err = secrets.EnsureJWTSigningKey(secretsDir)
if err != nil {
return nil, nil, nil, fmt.Errorf("failed to ensure JWT secret: %w", err)
return nil, nil, nil, fmt.Errorf("failed to ensure JWT signing key: %w", err)
}
}

Expand Down
5 changes: 1 addition & 4 deletions server/internal/db/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,12 +68,9 @@ type SessionStore interface {
CleanExpiredSessions() error
}

// SystemStore defines the methods for interacting with system settings and stats in the database
// SystemStore defines the methods for interacting with system stats in the database
type SystemStore interface {
GetSystemStats() (*UserStats, error)
EnsureJWTSecret() (string, error)
GetSystemSetting(key string) (string, error)
SetSystemSetting(key, value string) error
}

type StructScanner interface {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,4 @@ DROP INDEX IF EXISTS idx_sessions_user_id;
DROP INDEX IF EXISTS idx_workspaces_user_id;
DROP TABLE IF EXISTS sessions;
DROP TABLE IF EXISTS workspaces;
DROP TABLE IF EXISTS system_settings;
DROP TABLE IF EXISTS users;
Original file line number Diff line number Diff line change
Expand Up @@ -46,14 +46,6 @@ CREATE TABLE IF NOT EXISTS sessions (
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
);

-- Create system_settings table for application settings
CREATE TABLE IF NOT EXISTS system_settings (
key TEXT PRIMARY KEY,
value TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- Create indexes for performance
CREATE INDEX IF NOT EXISTS idx_sessions_user_id ON sessions(user_id);
CREATE INDEX IF NOT EXISTS idx_sessions_expires_at ON sessions(expires_at);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,4 @@ DROP INDEX IF EXISTS idx_sessions_user_id;
DROP INDEX IF EXISTS idx_workspaces_user_id;
DROP TABLE IF EXISTS sessions;
DROP TABLE IF EXISTS workspaces;
DROP TABLE IF EXISTS system_settings;
DROP TABLE IF EXISTS users;
Original file line number Diff line number Diff line change
Expand Up @@ -45,14 +45,6 @@ CREATE TABLE IF NOT EXISTS sessions (
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
);

-- Create system_settings table for application settings
CREATE TABLE IF NOT EXISTS system_settings (
key TEXT PRIMARY KEY,
value TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- Create indexes for performance
CREATE INDEX IF NOT EXISTS idx_sessions_user_id ON sessions(user_id);
CREATE INDEX IF NOT EXISTS idx_sessions_expires_at ON sessions(expires_at);
Expand Down
1 change: 0 additions & 1 deletion server/internal/db/migrations_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ func TestMigrate(t *testing.T) {
"users",
"workspaces",
"sessions",
"system_settings",
"schema_migrations",
}

Expand Down
84 changes: 0 additions & 84 deletions server/internal/db/system.go
Original file line number Diff line number Diff line change
@@ -1,100 +1,16 @@
package db

import (
"crypto/rand"
"encoding/base64"
"fmt"
)

const (
// JWTSecretKey is the key for the JWT secret in the system settings
JWTSecretKey = "jwt_secret"
)

// UserStats represents system-wide statistics
type UserStats struct {
TotalUsers int `json:"totalUsers"`
TotalWorkspaces int `json:"totalWorkspaces"`
ActiveUsers int `json:"activeUsers"` // Users with activity in last 30 days
}

// EnsureJWTSecret makes sure a JWT signing secret exists in the database
// If no secret exists, it generates and stores a new one
func (db *database) EnsureJWTSecret() (string, error) {
log := getLogger().WithGroup("system")

// First, try to get existing secret
secret, err := db.GetSystemSetting(JWTSecretKey)
if err == nil {
return secret, nil
}

// Generate new secret if none exists
newSecret, err := generateRandomSecret(32) // 256 bits
if err != nil {
return "", fmt.Errorf("failed to generate JWT secret: %w", err)
}

// Store the new secret
err = db.SetSystemSetting(JWTSecretKey, newSecret)
if err != nil {
return "", fmt.Errorf("failed to store JWT secret: %w", err)
}

log.Info("new JWT secret generated and stored")

return newSecret, nil
}

// GetSystemSetting retrieves a system setting by key
func (db *database) GetSystemSetting(key string) (string, error) {
var value string
query := db.NewQuery().
Select("value").
From("system_settings").
Where("key = ").
Placeholder(key)
err := db.QueryRow(query.String(), query.args...).Scan(&value)
if err != nil {
return "", err
}

return value, nil
}

// SetSystemSetting stores or updates a system setting
func (db *database) SetSystemSetting(key, value string) error {
query := db.NewQuery().
Insert("system_settings", "key", "value").
Values(2).
AddArgs(key, value).
Write("ON CONFLICT(key) DO UPDATE SET value = ").
Placeholder(value)

_, err := db.Exec(query.String(), query.args...)

if err != nil {
return fmt.Errorf("failed to store system setting: %w", err)
}

return nil
}

// generateRandomSecret generates a cryptographically secure random string
func generateRandomSecret(bytes int) (string, error) {
log := getLogger().WithGroup("system")
log.Debug("generating random secret", "bytes", bytes)

b := make([]byte, bytes)
_, err := rand.Read(b)
if err != nil {
return "", fmt.Errorf("failed to generate random bytes: %w", err)
}

secret := base64.StdEncoding.EncodeToString(b)
return secret, nil
}

// GetSystemStats returns system-wide statistics
func (db *database) GetSystemStats() (*UserStats, error) {
stats := &UserStats{}
Expand Down
Loading
Loading