Merge pull request #22 from LordMathis/fix/missing-git-email

Fix missing commit author name and email
This commit is contained in:
2024-11-30 21:36:14 +01:00
committed by GitHub
18 changed files with 266 additions and 182 deletions

View File

@@ -16,13 +16,15 @@ const GitSettings = ({
gitToken, gitToken,
gitAutoCommit, gitAutoCommit,
gitCommitMsgTemplate, gitCommitMsgTemplate,
gitCommitName,
gitCommitEmail,
onInputChange, onInputChange,
}) => { }) => {
return ( return (
<Stack spacing="md"> <Stack spacing="md">
<Grid gutter="md" align="center"> <Grid gutter="md" align="center">
<Grid.Col span={6}> <Grid.Col span={6}>
<Text size="sm">Enable Git</Text> <Text size="sm">Enable Git Repository</Text>
</Grid.Col> </Grid.Col>
<Grid.Col span={6}> <Grid.Col span={6}>
<Group justify="flex-end"> <Group justify="flex-end">
@@ -41,6 +43,7 @@ const GitSettings = ({
<Grid.Col span={6}> <Grid.Col span={6}>
<TextInput <TextInput
value={gitUrl} value={gitUrl}
description="The URL of your Git repository"
onChange={(event) => onChange={(event) =>
onInputChange('gitUrl', event.currentTarget.value) onInputChange('gitUrl', event.currentTarget.value)
} }
@@ -50,11 +53,12 @@ const GitSettings = ({
</Grid.Col> </Grid.Col>
<Grid.Col span={6}> <Grid.Col span={6}>
<Text size="sm">Git Username</Text> <Text size="sm">Username</Text>
</Grid.Col> </Grid.Col>
<Grid.Col span={6}> <Grid.Col span={6}>
<TextInput <TextInput
value={gitUser} value={gitUser}
description="The username used to authenticate with the repository"
onChange={(event) => onChange={(event) =>
onInputChange('gitUser', event.currentTarget.value) onInputChange('gitUser', event.currentTarget.value)
} }
@@ -64,11 +68,12 @@ const GitSettings = ({
</Grid.Col> </Grid.Col>
<Grid.Col span={6}> <Grid.Col span={6}>
<Text size="sm">Git Token</Text> <Text size="sm">Access Token</Text>
</Grid.Col> </Grid.Col>
<Grid.Col span={6}> <Grid.Col span={6}>
<PasswordInput <PasswordInput
value={gitToken} value={gitToken}
description="Personal access token with repository read/write permissions"
onChange={(event) => onChange={(event) =>
onInputChange('gitToken', event.currentTarget.value) onInputChange('gitToken', event.currentTarget.value)
} }
@@ -78,7 +83,7 @@ const GitSettings = ({
</Grid.Col> </Grid.Col>
<Grid.Col span={6}> <Grid.Col span={6}>
<Text size="sm">Auto Commit</Text> <Text size="sm">Commit on Save</Text>
</Grid.Col> </Grid.Col>
<Grid.Col span={6}> <Grid.Col span={6}>
<Group justify="flex-end"> <Group justify="flex-end">
@@ -98,6 +103,7 @@ const GitSettings = ({
<Grid.Col span={6}> <Grid.Col span={6}>
<TextInput <TextInput
value={gitCommitMsgTemplate} value={gitCommitMsgTemplate}
description="Template for automated commit messages. Use ${filename} and ${action} as a placeholder."
onChange={(event) => onChange={(event) =>
onInputChange('gitCommitMsgTemplate', event.currentTarget.value) onInputChange('gitCommitMsgTemplate', event.currentTarget.value)
} }
@@ -105,6 +111,36 @@ const GitSettings = ({
placeholder="Enter commit message template" placeholder="Enter commit message template"
/> />
</Grid.Col> </Grid.Col>
<Grid.Col span={6}>
<Text size="sm">Commit Author</Text>
</Grid.Col>
<Grid.Col span={6}>
<TextInput
value={gitCommitName}
description="Name to appear in commit history. Leave empty to use Git username."
onChange={(event) =>
onInputChange('gitCommitName', event.currentTarget.value)
}
disabled={!gitEnabled}
placeholder="Enter commit author name."
/>
</Grid.Col>
<Grid.Col span={6}>
<Text size="sm">Commit Author Email</Text>
</Grid.Col>
<Grid.Col span={6}>
<TextInput
value={gitCommitEmail}
description="Email address to associate with your commits"
onChange={(event) =>
onInputChange('gitCommitEmail', event.currentTarget.value)
}
disabled={!gitEnabled}
placeholder="Enter commit author email."
/>
</Grid.Col>
</Grid> </Grid>
</Stack> </Stack>
); );

View File

@@ -74,6 +74,8 @@ const WorkspaceSettings = () => {
gitToken: currentWorkspace.gitToken, gitToken: currentWorkspace.gitToken,
gitAutoCommit: currentWorkspace.gitAutoCommit, gitAutoCommit: currentWorkspace.gitAutoCommit,
gitCommitMsgTemplate: currentWorkspace.gitCommitMsgTemplate, gitCommitMsgTemplate: currentWorkspace.gitCommitMsgTemplate,
gitCommitName: currentWorkspace.gitCommitName,
gitCommitEmail: currentWorkspace.gitCommitEmail,
}; };
dispatch({ type: 'INIT_SETTINGS', payload: settings }); dispatch({ type: 'INIT_SETTINGS', payload: settings });
} }
@@ -204,6 +206,8 @@ const WorkspaceSettings = () => {
gitToken={state.localSettings.gitToken} gitToken={state.localSettings.gitToken}
gitAutoCommit={state.localSettings.gitAutoCommit} gitAutoCommit={state.localSettings.gitAutoCommit}
gitCommitMsgTemplate={state.localSettings.gitCommitMsgTemplate} gitCommitMsgTemplate={state.localSettings.gitCommitMsgTemplate}
gitCommitName={state.localSettings.gitCommitName}
gitCommitEmail={state.localSettings.gitCommitEmail}
onInputChange={handleInputChange} onInputChange={handleInputChange}
/> />
</Accordion.Panel> </Accordion.Panel>

View File

@@ -2,6 +2,7 @@
package app package app
import ( import (
"database/sql"
"fmt" "fmt"
"log" "log"
"net/http" "net/http"
@@ -12,12 +13,14 @@ import (
"github.com/go-chi/cors" "github.com/go-chi/cors"
"github.com/go-chi/httprate" "github.com/go-chi/httprate"
"github.com/unrolled/secure" "github.com/unrolled/secure"
"golang.org/x/crypto/bcrypt"
"novamd/internal/api" "novamd/internal/api"
"novamd/internal/auth" "novamd/internal/auth"
"novamd/internal/config" "novamd/internal/config"
"novamd/internal/db" "novamd/internal/db"
"novamd/internal/handlers" "novamd/internal/handlers"
"novamd/internal/models"
"novamd/internal/secrets" "novamd/internal/secrets"
"novamd/internal/storage" "novamd/internal/storage"
) )
@@ -47,6 +50,12 @@ func NewServer(cfg *config.Config) (*Server, error) {
// Initialize filesystem // Initialize filesystem
storageManager := storage.NewService(cfg.WorkDir) storageManager := storage.NewService(cfg.WorkDir)
// Setup admin user
err = setupAdminUser(database, storageManager, cfg)
if err != nil {
return nil, fmt.Errorf("failed to setup admin user: %w", err)
}
// Initialize router // Initialize router
router := initRouter(cfg) router := initRouter(cfg)
@@ -168,3 +177,47 @@ func (s *Server) setupRoutes(jwtManager auth.JWTManager, sessionService *auth.Se
// Handle all other routes with static file server // Handle all other routes with static file server
s.router.Get("/*", handlers.NewStaticHandler(s.config.StaticPath).ServeHTTP) s.router.Get("/*", handlers.NewStaticHandler(s.config.StaticPath).ServeHTTP)
} }
func setupAdminUser(db db.Database, w storage.WorkspaceManager, cfg *config.Config) error {
adminEmail := cfg.AdminEmail
adminPassword := cfg.AdminPassword
// Check if admin user exists
adminUser, err := db.GetUserByEmail(adminEmail)
if adminUser != nil {
return nil // Admin user already exists
} else if err != sql.ErrNoRows {
return err
}
// Hash the password
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(adminPassword), bcrypt.DefaultCost)
if err != nil {
return fmt.Errorf("failed to hash password: %w", err)
}
// Create admin user
adminUser = &models.User{
Email: adminEmail,
DisplayName: "Admin",
PasswordHash: string(hashedPassword),
Role: models.RoleAdmin,
}
createdUser, err := db.CreateUser(adminUser)
if err != nil {
return fmt.Errorf("failed to create admin user: %w", err)
}
// Initialize workspace directory
err = w.InitializeUserWorkspace(createdUser.ID, createdUser.LastWorkspaceID)
if err != nil {
return fmt.Errorf("failed to initialize admin workspace: %w", err)
}
log.Printf("Created admin user with ID: %d and default workspace with ID: %d", createdUser.ID, createdUser.LastWorkspaceID)
return nil
}

View File

@@ -15,70 +15,65 @@ var migrations = []Migration{
{ {
Version: 1, Version: 1,
SQL: ` SQL: `
-- Create users table -- Create users table
CREATE TABLE IF NOT EXISTS users ( CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
email TEXT NOT NULL UNIQUE, email TEXT NOT NULL UNIQUE,
display_name TEXT, display_name TEXT,
password_hash TEXT NOT NULL, password_hash TEXT NOT NULL,
role TEXT NOT NULL CHECK(role IN ('admin', 'editor', 'viewer')), role TEXT NOT NULL CHECK(role IN ('admin', 'editor', 'viewer')),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
last_workspace_id INTEGER last_workspace_id INTEGER
); );
-- Create workspaces table with integrated settings -- Create workspaces table with integrated settings
CREATE TABLE IF NOT EXISTS workspaces ( CREATE TABLE IF NOT EXISTS workspaces (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL, user_id INTEGER NOT NULL,
name TEXT NOT NULL, name TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
last_opened_file_path TEXT, last_opened_file_path TEXT,
-- Settings fields -- Settings fields
theme TEXT NOT NULL DEFAULT 'light' CHECK(theme IN ('light', 'dark')), theme TEXT NOT NULL DEFAULT 'light' CHECK(theme IN ('light', 'dark')),
auto_save BOOLEAN NOT NULL DEFAULT 0, auto_save BOOLEAN NOT NULL DEFAULT 0,
git_enabled BOOLEAN NOT NULL DEFAULT 0, git_enabled BOOLEAN NOT NULL DEFAULT 0,
git_url TEXT, git_url TEXT,
git_user TEXT, git_user TEXT,
git_token TEXT, git_token TEXT,
git_auto_commit BOOLEAN NOT NULL DEFAULT 0, git_auto_commit BOOLEAN NOT NULL DEFAULT 0,
git_commit_msg_template TEXT DEFAULT '${action} ${filename}', git_commit_msg_template TEXT DEFAULT '${action} ${filename}',
FOREIGN KEY (user_id) REFERENCES users (id) git_commit_name TEXT,
); git_commit_email TEXT,
`, show_hidden_files BOOLEAN NOT NULL DEFAULT 0,
}, created_by INTEGER REFERENCES users(id),
{ updated_by INTEGER REFERENCES users(id),
Version: 2, updated_at TIMESTAMP,
SQL: ` FOREIGN KEY (user_id) REFERENCES users (id)
-- Create sessions table for authentication );
CREATE TABLE IF NOT EXISTS sessions (
id TEXT PRIMARY KEY,
user_id INTEGER NOT NULL,
refresh_token TEXT NOT NULL,
expires_at TIMESTAMP NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
);
-- Add show_hidden_files field to workspaces -- Create sessions table for authentication
ALTER TABLE workspaces ADD COLUMN show_hidden_files BOOLEAN NOT NULL DEFAULT 0; CREATE TABLE IF NOT EXISTS sessions (
id TEXT PRIMARY KEY,
user_id INTEGER NOT NULL,
refresh_token TEXT NOT NULL,
expires_at TIMESTAMP NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
);
-- Add indexes for performance -- Create system_settings table for application settings
CREATE INDEX idx_sessions_user_id ON sessions(user_id); CREATE TABLE IF NOT EXISTS system_settings (
CREATE INDEX idx_sessions_expires_at ON sessions(expires_at); key TEXT PRIMARY KEY,
CREATE INDEX idx_sessions_refresh_token ON sessions(refresh_token); value TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Add audit fields to workspaces -- Create indexes for performance
ALTER TABLE workspaces ADD COLUMN created_by INTEGER REFERENCES users(id); CREATE INDEX idx_sessions_user_id ON sessions(user_id);
ALTER TABLE workspaces ADD COLUMN updated_by INTEGER REFERENCES users(id); CREATE INDEX idx_sessions_expires_at ON sessions(expires_at);
ALTER TABLE workspaces ADD COLUMN updated_at TIMESTAMP; CREATE INDEX idx_sessions_refresh_token ON sessions(refresh_token);
`,
-- 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
);`,
}, },
} }

View File

@@ -27,8 +27,8 @@ func TestMigrate(t *testing.T) {
t.Fatalf("failed to get migration version: %v", err) t.Fatalf("failed to get migration version: %v", err)
} }
if version != 2 { // Current number of migrations in production code if version != 1 { // Current number of migrations in production code
t.Errorf("expected migration version 2, got %d", version) t.Errorf("expected migration version 1, got %d", version)
} }
// Verify number of migration entries matches versions applied // Verify number of migration entries matches versions applied
@@ -38,8 +38,8 @@ func TestMigrate(t *testing.T) {
t.Fatalf("failed to count migrations: %v", err) t.Fatalf("failed to count migrations: %v", err)
} }
if count != 2 { if count != 1 {
t.Errorf("expected 2 migration entries, got %d", count) t.Errorf("expected 1 migration entries, got %d", count)
} }
}) })
@@ -82,8 +82,8 @@ func TestMigrate(t *testing.T) {
t.Fatalf("failed to count migrations: %v", err) t.Fatalf("failed to count migrations: %v", err)
} }
if count != 2 { if count != 1 {
t.Errorf("expected 2 migration entries, got %d", count) t.Errorf("expected 1 migration entries, got %d", count)
} }
}) })
@@ -118,8 +118,8 @@ func TestMigrate(t *testing.T) {
t.Fatalf("failed to get migration version: %v", err) t.Fatalf("failed to get migration version: %v", err)
} }
if version != 2 { if version != 1 {
t.Errorf("expected migration version to remain at 2, got %d", version) t.Errorf("expected migration version to remain at 1, got %d", version)
} }
}) })
} }

View File

@@ -68,12 +68,14 @@ func (db *database) createWorkspaceTx(tx *sql.Tx, workspace *models.Workspace) e
user_id, name, user_id, name,
theme, auto_save, show_hidden_files, theme, auto_save, show_hidden_files,
git_enabled, git_url, git_user, git_token, git_enabled, git_url, git_user, git_token,
git_auto_commit, git_commit_msg_template git_auto_commit, git_commit_msg_template,
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, git_commit_name, git_commit_email
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
workspace.UserID, workspace.Name, workspace.UserID, workspace.Name,
workspace.Theme, workspace.AutoSave, workspace.ShowHiddenFiles, workspace.Theme, workspace.AutoSave, workspace.ShowHiddenFiles,
workspace.GitEnabled, workspace.GitURL, workspace.GitUser, workspace.GitToken, workspace.GitEnabled, workspace.GitURL, workspace.GitUser, workspace.GitToken,
workspace.GitAutoCommit, workspace.GitCommitMsgTemplate, workspace.GitAutoCommit, workspace.GitCommitMsgTemplate,
workspace.GitCommitName, workspace.GitCommitEmail,
) )
if err != nil { if err != nil {
return err return err

View File

@@ -23,11 +23,12 @@ func (db *database) CreateWorkspace(workspace *models.Workspace) error {
INSERT INTO workspaces ( INSERT INTO workspaces (
user_id, name, theme, auto_save, show_hidden_files, user_id, name, theme, auto_save, show_hidden_files,
git_enabled, git_url, git_user, git_token, git_enabled, git_url, git_user, git_token,
git_auto_commit, git_commit_msg_template git_auto_commit, git_commit_msg_template,
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, git_commit_name, git_commit_email
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
workspace.UserID, workspace.Name, workspace.Theme, workspace.AutoSave, workspace.ShowHiddenFiles, workspace.UserID, workspace.Name, workspace.Theme, workspace.AutoSave, workspace.ShowHiddenFiles,
workspace.GitEnabled, workspace.GitURL, workspace.GitUser, encryptedToken, workspace.GitEnabled, workspace.GitURL, workspace.GitUser, encryptedToken,
workspace.GitAutoCommit, workspace.GitCommitMsgTemplate, workspace.GitAutoCommit, workspace.GitCommitMsgTemplate, workspace.GitCommitName, workspace.GitCommitEmail,
) )
if err != nil { if err != nil {
return err return err
@@ -51,7 +52,8 @@ func (db *database) GetWorkspaceByID(id int) (*models.Workspace, error) {
id, user_id, name, created_at, id, user_id, name, created_at,
theme, auto_save, show_hidden_files, theme, auto_save, show_hidden_files,
git_enabled, git_url, git_user, git_token, git_enabled, git_url, git_user, git_token,
git_auto_commit, git_commit_msg_template git_auto_commit, git_commit_msg_template,
git_commit_name, git_commit_email
FROM workspaces FROM workspaces
WHERE id = ?`, WHERE id = ?`,
id, id,
@@ -59,7 +61,7 @@ func (db *database) GetWorkspaceByID(id int) (*models.Workspace, error) {
&workspace.ID, &workspace.UserID, &workspace.Name, &workspace.CreatedAt, &workspace.ID, &workspace.UserID, &workspace.Name, &workspace.CreatedAt,
&workspace.Theme, &workspace.AutoSave, &workspace.ShowHiddenFiles, &workspace.Theme, &workspace.AutoSave, &workspace.ShowHiddenFiles,
&workspace.GitEnabled, &workspace.GitURL, &workspace.GitUser, &encryptedToken, &workspace.GitEnabled, &workspace.GitURL, &workspace.GitUser, &encryptedToken,
&workspace.GitAutoCommit, &workspace.GitCommitMsgTemplate, &workspace.GitAutoCommit, &workspace.GitCommitMsgTemplate, &workspace.GitCommitName, &workspace.GitCommitEmail,
) )
if err != nil { if err != nil {
return nil, err return nil, err
@@ -84,7 +86,8 @@ func (db *database) GetWorkspaceByName(userID int, workspaceName string) (*model
id, user_id, name, created_at, id, user_id, name, created_at,
theme, auto_save, show_hidden_files, theme, auto_save, show_hidden_files,
git_enabled, git_url, git_user, git_token, git_enabled, git_url, git_user, git_token,
git_auto_commit, git_commit_msg_template git_auto_commit, git_commit_msg_template,
git_commit_name, git_commit_email
FROM workspaces FROM workspaces
WHERE user_id = ? AND name = ?`, WHERE user_id = ? AND name = ?`,
userID, workspaceName, userID, workspaceName,
@@ -93,6 +96,7 @@ func (db *database) GetWorkspaceByName(userID int, workspaceName string) (*model
&workspace.Theme, &workspace.AutoSave, &workspace.ShowHiddenFiles, &workspace.Theme, &workspace.AutoSave, &workspace.ShowHiddenFiles,
&workspace.GitEnabled, &workspace.GitURL, &workspace.GitUser, &encryptedToken, &workspace.GitEnabled, &workspace.GitURL, &workspace.GitUser, &encryptedToken,
&workspace.GitAutoCommit, &workspace.GitCommitMsgTemplate, &workspace.GitAutoCommit, &workspace.GitCommitMsgTemplate,
&workspace.GitCommitName, &workspace.GitCommitEmail,
) )
if err != nil { if err != nil {
return nil, err return nil, err
@@ -127,7 +131,9 @@ func (db *database) UpdateWorkspace(workspace *models.Workspace) error {
git_user = ?, git_user = ?,
git_token = ?, git_token = ?,
git_auto_commit = ?, git_auto_commit = ?,
git_commit_msg_template = ? git_commit_msg_template = ?,
git_commit_name = ?,
git_commit_email = ?
WHERE id = ? AND user_id = ?`, WHERE id = ? AND user_id = ?`,
workspace.Name, workspace.Name,
workspace.Theme, workspace.Theme,
@@ -139,6 +145,8 @@ func (db *database) UpdateWorkspace(workspace *models.Workspace) error {
encryptedToken, encryptedToken,
workspace.GitAutoCommit, workspace.GitAutoCommit,
workspace.GitCommitMsgTemplate, workspace.GitCommitMsgTemplate,
workspace.GitCommitName,
workspace.GitCommitEmail,
workspace.ID, workspace.ID,
workspace.UserID, workspace.UserID,
) )
@@ -152,7 +160,8 @@ func (db *database) GetWorkspacesByUserID(userID int) ([]*models.Workspace, erro
id, user_id, name, created_at, id, user_id, name, created_at,
theme, auto_save, show_hidden_files, theme, auto_save, show_hidden_files,
git_enabled, git_url, git_user, git_token, git_enabled, git_url, git_user, git_token,
git_auto_commit, git_commit_msg_template git_auto_commit, git_commit_msg_template,
git_commit_name, git_commit_email
FROM workspaces FROM workspaces
WHERE user_id = ?`, WHERE user_id = ?`,
userID, userID,
@@ -171,6 +180,7 @@ func (db *database) GetWorkspacesByUserID(userID int) ([]*models.Workspace, erro
&workspace.Theme, &workspace.AutoSave, &workspace.ShowHiddenFiles, &workspace.Theme, &workspace.AutoSave, &workspace.ShowHiddenFiles,
&workspace.GitEnabled, &workspace.GitURL, &workspace.GitUser, &encryptedToken, &workspace.GitEnabled, &workspace.GitURL, &workspace.GitUser, &encryptedToken,
&workspace.GitAutoCommit, &workspace.GitCommitMsgTemplate, &workspace.GitAutoCommit, &workspace.GitCommitMsgTemplate,
&workspace.GitCommitName, &workspace.GitCommitEmail,
) )
if err != nil { if err != nil {
return nil, err return nil, err
@@ -201,7 +211,9 @@ func (db *database) UpdateWorkspaceSettings(workspace *models.Workspace) error {
git_user = ?, git_user = ?,
git_token = ?, git_token = ?,
git_auto_commit = ?, git_auto_commit = ?,
git_commit_msg_template = ? git_commit_msg_template = ?,
git_commit_name = ?,
git_commit_email = ?
WHERE id = ?`, WHERE id = ?`,
workspace.Theme, workspace.Theme,
workspace.AutoSave, workspace.AutoSave,
@@ -212,6 +224,8 @@ func (db *database) UpdateWorkspaceSettings(workspace *models.Workspace) error {
workspace.GitToken, workspace.GitToken,
workspace.GitAutoCommit, workspace.GitAutoCommit,
workspace.GitCommitMsgTemplate, workspace.GitCommitMsgTemplate,
workspace.GitCommitName,
workspace.GitCommitEmail,
workspace.ID, workspace.ID,
) )
return err return err
@@ -261,7 +275,8 @@ func (db *database) GetAllWorkspaces() ([]*models.Workspace, error) {
id, user_id, name, created_at, id, user_id, name, created_at,
theme, auto_save, show_hidden_files, theme, auto_save, show_hidden_files,
git_enabled, git_url, git_user, git_token, git_enabled, git_url, git_user, git_token,
git_auto_commit, git_commit_msg_template git_auto_commit, git_commit_msg_template,
git_commit_name, git_commit_email
FROM workspaces`, FROM workspaces`,
) )
if err != nil { if err != nil {
@@ -278,6 +293,7 @@ func (db *database) GetAllWorkspaces() ([]*models.Workspace, error) {
&workspace.Theme, &workspace.AutoSave, &workspace.ShowHiddenFiles, &workspace.Theme, &workspace.AutoSave, &workspace.ShowHiddenFiles,
&workspace.GitEnabled, &workspace.GitURL, &workspace.GitUser, &encryptedToken, &workspace.GitEnabled, &workspace.GitURL, &workspace.GitUser, &encryptedToken,
&workspace.GitAutoCommit, &workspace.GitCommitMsgTemplate, &workspace.GitAutoCommit, &workspace.GitCommitMsgTemplate,
&workspace.GitCommitName, &workspace.GitCommitEmail,
) )
if err != nil { if err != nil {
return nil, err return nil, err

View File

@@ -69,6 +69,8 @@ func TestWorkspaceOperations(t *testing.T) {
GitToken: "secret-token", GitToken: "secret-token",
GitAutoCommit: true, GitAutoCommit: true,
GitCommitMsgTemplate: "${action} ${filename}", GitCommitMsgTemplate: "${action} ${filename}",
GitCommitName: "Test User",
GitCommitEmail: "test@example.com",
}, },
wantErr: false, wantErr: false,
}, },
@@ -244,6 +246,8 @@ func TestWorkspaceOperations(t *testing.T) {
workspace.GitToken = "new-token" workspace.GitToken = "new-token"
workspace.GitAutoCommit = true workspace.GitAutoCommit = true
workspace.GitCommitMsgTemplate = "custom ${filename}" workspace.GitCommitMsgTemplate = "custom ${filename}"
workspace.GitCommitName = "Test User"
workspace.GitCommitEmail = "test@example.com"
if err := database.UpdateWorkspace(workspace); err != nil { if err := database.UpdateWorkspace(workspace); err != nil {
t.Fatalf("failed to update workspace: %v", err) t.Fatalf("failed to update workspace: %v", err)
@@ -424,6 +428,12 @@ func verifyWorkspace(t *testing.T, actual, expected *models.Workspace) {
if actual.GitCommitMsgTemplate != expected.GitCommitMsgTemplate { if actual.GitCommitMsgTemplate != expected.GitCommitMsgTemplate {
t.Errorf("GitCommitMsgTemplate = %v, want %v", actual.GitCommitMsgTemplate, expected.GitCommitMsgTemplate) t.Errorf("GitCommitMsgTemplate = %v, want %v", actual.GitCommitMsgTemplate, expected.GitCommitMsgTemplate)
} }
if actual.GitCommitName != expected.GitCommitName {
t.Errorf("GitCommitName = %v, want %v", actual.GitCommitName, expected.GitCommitName)
}
if actual.GitCommitEmail != expected.GitCommitEmail {
t.Errorf("GitCommitEmail = %v, want %v", actual.GitCommitEmail, expected.GitCommitEmail)
}
if actual.CreatedAt.IsZero() { if actual.CreatedAt.IsZero() {
t.Error("CreatedAt should not be zero") t.Error("CreatedAt should not be zero")
} }

View File

@@ -5,17 +5,21 @@ import (
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
"time"
"github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing/object"
"github.com/go-git/go-git/v5/plumbing/transport/http" "github.com/go-git/go-git/v5/plumbing/transport/http"
) )
// Config holds the configuration for a Git client // Config holds the configuration for a Git client
type Config struct { type Config struct {
URL string URL string
Username string Username string
Token string Token string
WorkDir string WorkDir string
CommitName string
CommitEmail string
} }
// Client defines the interface for Git operations // Client defines the interface for Git operations
@@ -34,13 +38,15 @@ type client struct {
} }
// New creates a new git Client instance // New creates a new git Client instance
func New(url, username, token, workDir string) Client { func New(url, username, token, workDir, commitName, commitEmail string) Client {
return &client{ return &client{
Config: Config{ Config: Config{
URL: url, URL: url,
Username: username, Username: username,
Token: token, Token: token,
WorkDir: workDir, WorkDir: workDir,
CommitName: commitName,
CommitEmail: commitEmail,
}, },
} }
} }
@@ -110,7 +116,13 @@ func (c *client) Commit(message string) error {
return fmt.Errorf("failed to add changes: %w", err) return fmt.Errorf("failed to add changes: %w", err)
} }
_, err = w.Commit(message, &git.CommitOptions{}) _, err = w.Commit(message, &git.CommitOptions{
Author: &object.Signature{
Name: c.CommitName,
Email: c.CommitEmail,
When: time.Now(),
},
})
if err != nil { if err != nil {
return fmt.Errorf("failed to commit changes: %w", err) return fmt.Errorf("failed to commit changes: %w", err)
} }

View File

@@ -70,7 +70,7 @@ func setupTestHarness(t *testing.T) *testHarness {
// Create storage with mock git client // Create storage with mock git client
storageOpts := storage.Options{ storageOpts := storage.Options{
NewGitClient: func(url, user, token, path string) git.Client { NewGitClient: func(url, user, token, path, commitName, commitEmail string) git.Client {
return mockGit return mockGit
}, },
} }

View File

@@ -64,6 +64,8 @@ func (h *Handler) CreateWorkspace() http.HandlerFunc {
workspace.GitURL, workspace.GitURL,
workspace.GitUser, workspace.GitUser,
workspace.GitToken, workspace.GitToken,
workspace.GitCommitName,
workspace.GitCommitEmail,
); err != nil { ); err != nil {
http.Error(w, "Failed to setup git repo: "+err.Error(), http.StatusInternalServerError) http.Error(w, "Failed to setup git repo: "+err.Error(), http.StatusInternalServerError)
return return
@@ -96,7 +98,9 @@ func gitSettingsChanged(new, old *models.Workspace) bool {
if new.GitEnabled { if new.GitEnabled {
return new.GitURL != old.GitURL || return new.GitURL != old.GitURL ||
new.GitUser != old.GitUser || new.GitUser != old.GitUser ||
new.GitToken != old.GitToken new.GitToken != old.GitToken ||
new.GitCommitName != old.GitCommitName ||
new.GitCommitEmail != old.GitCommitEmail
} }
return false return false
@@ -135,6 +139,8 @@ func (h *Handler) UpdateWorkspace() http.HandlerFunc {
workspace.GitURL, workspace.GitURL,
workspace.GitUser, workspace.GitUser,
workspace.GitToken, workspace.GitToken,
workspace.GitCommitName,
workspace.GitCommitEmail,
); err != nil { ); err != nil {
http.Error(w, "Failed to setup git repo: "+err.Error(), http.StatusInternalServerError) http.Error(w, "Failed to setup git repo: "+err.Error(), http.StatusInternalServerError)
return return

View File

@@ -54,12 +54,14 @@ func TestWorkspaceHandlers_Integration(t *testing.T) {
t.Run("create with git settings", func(t *testing.T) { t.Run("create with git settings", func(t *testing.T) {
workspace := &models.Workspace{ workspace := &models.Workspace{
Name: "Git Workspace", Name: "Git Workspace",
GitEnabled: true, GitEnabled: true,
GitURL: "https://github.com/test/repo.git", GitURL: "https://github.com/test/repo.git",
GitUser: "testuser", GitUser: "testuser",
GitToken: "testtoken", GitToken: "testtoken",
GitAutoCommit: true, GitAutoCommit: true,
GitCommitName: "Test User",
GitCommitEmail: "test@example.com",
} }
rr := h.makeRequest(t, http.MethodPost, "/api/v1/workspaces", workspace, h.RegularToken, nil) rr := h.makeRequest(t, http.MethodPost, "/api/v1/workspaces", workspace, h.RegularToken, nil)
@@ -73,6 +75,8 @@ func TestWorkspaceHandlers_Integration(t *testing.T) {
assert.Equal(t, workspace.GitUser, created.GitUser) assert.Equal(t, workspace.GitUser, created.GitUser)
assert.Equal(t, workspace.GitToken, created.GitToken) assert.Equal(t, workspace.GitToken, created.GitToken)
assert.Equal(t, workspace.GitAutoCommit, created.GitAutoCommit) assert.Equal(t, workspace.GitAutoCommit, created.GitAutoCommit)
assert.Equal(t, workspace.GitCommitName, created.GitCommitName)
assert.Equal(t, workspace.GitCommitEmail, created.GitCommitEmail)
}) })
t.Run("invalid workspace", func(t *testing.T) { t.Run("invalid workspace", func(t *testing.T) {
@@ -161,13 +165,15 @@ func TestWorkspaceHandlers_Integration(t *testing.T) {
t.Run("enable git", func(t *testing.T) { t.Run("enable git", func(t *testing.T) {
update := &models.Workspace{ update := &models.Workspace{
Name: workspace.Name, Name: workspace.Name,
Theme: "dark", Theme: "dark",
GitEnabled: true, GitEnabled: true,
GitURL: "https://github.com/test/repo.git", GitURL: "https://github.com/test/repo.git",
GitUser: "testuser", GitUser: "testuser",
GitToken: "testtoken", GitToken: "testtoken",
GitAutoCommit: true, GitAutoCommit: true,
GitCommitName: "Test User",
GitCommitEmail: "test@example.com",
} }
rr := h.makeRequest(t, http.MethodPut, baseURL, update, h.RegularToken, nil) rr := h.makeRequest(t, http.MethodPut, baseURL, update, h.RegularToken, nil)
@@ -180,6 +186,8 @@ func TestWorkspaceHandlers_Integration(t *testing.T) {
assert.Equal(t, update.GitURL, updated.GitURL) assert.Equal(t, update.GitURL, updated.GitURL)
assert.Equal(t, update.GitUser, updated.GitUser) assert.Equal(t, update.GitUser, updated.GitUser)
assert.Equal(t, update.GitToken, updated.GitToken) assert.Equal(t, update.GitToken, updated.GitToken)
assert.Equal(t, update.GitAutoCommit, updated.GitAutoCommit)
assert.Equal(t, update.GitCommitName, updated.GitCommitName)
// Mock should have been called to setup git // Mock should have been called to setup git
assert.True(t, h.MockGit.IsInitialized()) assert.True(t, h.MockGit.IsInitialized())

View File

@@ -22,6 +22,8 @@ type Workspace struct {
GitToken string `json:"gitToken" validate:"required_if=GitEnabled true"` GitToken string `json:"gitToken" validate:"required_if=GitEnabled true"`
GitAutoCommit bool `json:"gitAutoCommit"` GitAutoCommit bool `json:"gitAutoCommit"`
GitCommitMsgTemplate string `json:"gitCommitMsgTemplate"` GitCommitMsgTemplate string `json:"gitCommitMsgTemplate"`
GitCommitName string `json:"gitCommitName"`
GitCommitEmail string `json:"gitCommitEmail" validate:"omitempty,required_if=GitEnabled true,email"`
} }
// Validate validates the workspace struct // Validate validates the workspace struct

View File

@@ -59,6 +59,7 @@ type mockFS struct {
StatError error StatError error
} }
//revive:disable:unexported-return
func NewMockFS() *mockFS { func NewMockFS() *mockFS {
return &mockFS{ return &mockFS{
ReadCalls: make(map[string]int), ReadCalls: make(map[string]int),

View File

@@ -7,7 +7,7 @@ import (
// RepositoryManager defines the interface for managing Git repositories. // RepositoryManager defines the interface for managing Git repositories.
type RepositoryManager interface { type RepositoryManager interface {
SetupGitRepo(userID, workspaceID int, gitURL, gitUser, gitToken string) error SetupGitRepo(userID, workspaceID int, gitURL, gitUser, gitToken, commitName, commitEmail string) error
DisableGitRepo(userID, workspaceID int) DisableGitRepo(userID, workspaceID int)
StageCommitAndPush(userID, workspaceID int, message string) error StageCommitAndPush(userID, workspaceID int, message string) error
Pull(userID, workspaceID int) error Pull(userID, workspaceID int) error
@@ -15,12 +15,12 @@ type RepositoryManager interface {
// SetupGitRepo sets up a Git repository for the given userID and workspaceID. // SetupGitRepo sets up a Git repository for the given userID and workspaceID.
// The repository is cloned from the given gitURL using the given gitUser and gitToken. // The repository is cloned from the given gitURL using the given gitUser and gitToken.
func (s *Service) SetupGitRepo(userID, workspaceID int, gitURL, gitUser, gitToken string) error { func (s *Service) SetupGitRepo(userID, workspaceID int, gitURL, gitUser, gitToken, commitName, commitEmail string) error {
workspacePath := s.GetWorkspacePath(userID, workspaceID) workspacePath := s.GetWorkspacePath(userID, workspaceID)
if _, ok := s.GitRepos[userID]; !ok { if _, ok := s.GitRepos[userID]; !ok {
s.GitRepos[userID] = make(map[int]git.Client) s.GitRepos[userID] = make(map[int]git.Client)
} }
s.GitRepos[userID][workspaceID] = s.newGitClient(gitURL, gitUser, gitToken, workspacePath) s.GitRepos[userID][workspaceID] = s.newGitClient(gitURL, gitUser, gitToken, workspacePath, commitName, commitEmail)
return s.GitRepos[userID][workspaceID].EnsureRepo() return s.GitRepos[userID][workspaceID].EnsureRepo()
} }

View File

@@ -55,6 +55,7 @@ func TestSetupGitRepo(t *testing.T) {
gitURL string gitURL string
gitUser string gitUser string
gitToken string gitToken string
commitEmail string
mockErr error mockErr error
wantErr bool wantErr bool
}{ }{
@@ -65,6 +66,7 @@ func TestSetupGitRepo(t *testing.T) {
gitURL: "https://github.com/user/repo", gitURL: "https://github.com/user/repo",
gitUser: "user", gitUser: "user",
gitToken: "token", gitToken: "token",
commitEmail: "test@example.com",
mockErr: nil, mockErr: nil,
wantErr: false, wantErr: false,
}, },
@@ -75,6 +77,7 @@ func TestSetupGitRepo(t *testing.T) {
gitURL: "https://github.com/user/repo", gitURL: "https://github.com/user/repo",
gitUser: "user", gitUser: "user",
gitToken: "token", gitToken: "token",
commitEmail: "test@example.com",
mockErr: errors.New("git initialization failed"), mockErr: errors.New("git initialization failed"),
wantErr: true, wantErr: true,
}, },
@@ -86,7 +89,7 @@ func TestSetupGitRepo(t *testing.T) {
mockClient := &MockGitClient{ReturnError: tc.mockErr} mockClient := &MockGitClient{ReturnError: tc.mockErr}
// Create a client factory that returns our configured mock // Create a client factory that returns our configured mock
mockClientFactory := func(_, _, _, _ string) git.Client { mockClientFactory := func(_, _, _, _, _, _ string) git.Client {
return mockClient return mockClient
} }
@@ -96,7 +99,7 @@ func TestSetupGitRepo(t *testing.T) {
}) })
// Setup the git repo // Setup the git repo
err := s.SetupGitRepo(tc.userID, tc.workspaceID, tc.gitURL, tc.gitUser, tc.gitToken) err := s.SetupGitRepo(tc.userID, tc.workspaceID, tc.gitURL, tc.gitUser, tc.gitToken, tc.gitUser, tc.commitEmail)
if tc.wantErr { if tc.wantErr {
if err == nil { if err == nil {
@@ -131,7 +134,7 @@ func TestGitOperations(t *testing.T) {
mockFS := NewMockFS() mockFS := NewMockFS()
s := storage.NewServiceWithOptions("test-root", storage.Options{ s := storage.NewServiceWithOptions("test-root", storage.Options{
Fs: mockFS, Fs: mockFS,
NewGitClient: func(_, _, _, _ string) git.Client { return &MockGitClient{} }, NewGitClient: func(_, _, _, _, _, _ string) git.Client { return &MockGitClient{} },
}) })
t.Run("operations on non-configured workspace", func(t *testing.T) { t.Run("operations on non-configured workspace", func(t *testing.T) {
@@ -203,7 +206,7 @@ func TestDisableGitRepo(t *testing.T) {
mockFS := NewMockFS() mockFS := NewMockFS()
s := storage.NewServiceWithOptions("test-root", storage.Options{ s := storage.NewServiceWithOptions("test-root", storage.Options{
Fs: mockFS, Fs: mockFS,
NewGitClient: func(_, _, _, _ string) git.Client { return &MockGitClient{} }, NewGitClient: func(_, _, _, _, _, _ string) git.Client { return &MockGitClient{} },
}) })
testCases := []struct { testCases := []struct {

View File

@@ -14,7 +14,7 @@ type Manager interface {
// Service represents the file system structure. // Service represents the file system structure.
type Service struct { type Service struct {
fs fileSystem fs fileSystem
newGitClient func(url, user, token, path string) git.Client newGitClient func(url, user, token, path, commitName, commitEmail string) git.Client
RootDir string RootDir string
GitRepos map[int]map[int]git.Client // map[userID]map[workspaceID]*git.Client GitRepos map[int]map[int]git.Client // map[userID]map[workspaceID]*git.Client
} }
@@ -22,7 +22,7 @@ type Service struct {
// Options represents the options for the storage service. // Options represents the options for the storage service.
type Options struct { type Options struct {
Fs fileSystem Fs fileSystem
NewGitClient func(url, user, token, path string) git.Client NewGitClient func(url, user, token, path, commitName, commitEmail string) git.Client
} }
// NewService creates a new Storage instance with the default options and the given rootDir root directory. // NewService creates a new Storage instance with the default options and the given rootDir root directory.

View File

@@ -1,64 +0,0 @@
package user
import (
"database/sql"
"fmt"
"log"
"golang.org/x/crypto/bcrypt"
"novamd/internal/db"
"novamd/internal/models"
"novamd/internal/storage"
)
type UserService struct {
DB db.Database
Storage storage.Manager
}
func NewUserService(database db.Database, s storage.Manager) *UserService {
return &UserService{
DB: database,
Storage: s,
}
}
func (s *UserService) SetupAdminUser(adminEmail, adminPassword string) (*models.User, error) {
// Check if admin user exists
adminUser, err := s.DB.GetUserByEmail(adminEmail)
if adminUser != nil {
return adminUser, nil // Admin user already exists
} else if err != sql.ErrNoRows {
return nil, err
}
// Hash the password
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(adminPassword), bcrypt.DefaultCost)
if err != nil {
return nil, fmt.Errorf("failed to hash password: %w", err)
}
// Create admin user
adminUser = &models.User{
Email: adminEmail,
DisplayName: "Admin",
PasswordHash: string(hashedPassword),
Role: models.RoleAdmin,
}
createdUser, err := s.DB.CreateUser(adminUser)
if err != nil {
return nil, fmt.Errorf("failed to create admin user: %w", err)
}
// Initialize workspace directory
err = s.Storage.InitializeUserWorkspace(createdUser.ID, createdUser.LastWorkspaceID)
if err != nil {
return nil, fmt.Errorf("failed to initialize admin workspace: %w", err)
}
log.Printf("Created admin user with ID: %d and default workspace with ID: %d", createdUser.ID, createdUser.LastWorkspaceID)
return adminUser, nil
}