diff --git a/app/src/components/settings/workspace/GitSettings.jsx b/app/src/components/settings/workspace/GitSettings.jsx index 9d83a42..1c5867c 100644 --- a/app/src/components/settings/workspace/GitSettings.jsx +++ b/app/src/components/settings/workspace/GitSettings.jsx @@ -16,13 +16,15 @@ const GitSettings = ({ gitToken, gitAutoCommit, gitCommitMsgTemplate, + gitCommitName, + gitCommitEmail, onInputChange, }) => { return ( - Enable Git + Enable Git Repository @@ -41,6 +43,7 @@ const GitSettings = ({ onInputChange('gitUrl', event.currentTarget.value) } @@ -50,11 +53,12 @@ const GitSettings = ({ - Git Username + Username onInputChange('gitUser', event.currentTarget.value) } @@ -64,11 +68,12 @@ const GitSettings = ({ - Git Token + Access Token onInputChange('gitToken', event.currentTarget.value) } @@ -78,7 +83,7 @@ const GitSettings = ({ - Auto Commit + Commit on Save @@ -98,6 +103,7 @@ const GitSettings = ({ onInputChange('gitCommitMsgTemplate', event.currentTarget.value) } @@ -105,6 +111,36 @@ const GitSettings = ({ placeholder="Enter commit message template" /> + + + Commit Author + + + + onInputChange('gitCommitName', event.currentTarget.value) + } + disabled={!gitEnabled} + placeholder="Enter commit author name." + /> + + + + Commit Author Email + + + + onInputChange('gitCommitEmail', event.currentTarget.value) + } + disabled={!gitEnabled} + placeholder="Enter commit author email." + /> + ); diff --git a/app/src/components/settings/workspace/WorkspaceSettings.jsx b/app/src/components/settings/workspace/WorkspaceSettings.jsx index 703d503..34bc7b8 100644 --- a/app/src/components/settings/workspace/WorkspaceSettings.jsx +++ b/app/src/components/settings/workspace/WorkspaceSettings.jsx @@ -74,6 +74,8 @@ const WorkspaceSettings = () => { gitToken: currentWorkspace.gitToken, gitAutoCommit: currentWorkspace.gitAutoCommit, gitCommitMsgTemplate: currentWorkspace.gitCommitMsgTemplate, + gitCommitName: currentWorkspace.gitCommitName, + gitCommitEmail: currentWorkspace.gitCommitEmail, }; dispatch({ type: 'INIT_SETTINGS', payload: settings }); } @@ -204,6 +206,8 @@ const WorkspaceSettings = () => { gitToken={state.localSettings.gitToken} gitAutoCommit={state.localSettings.gitAutoCommit} gitCommitMsgTemplate={state.localSettings.gitCommitMsgTemplate} + gitCommitName={state.localSettings.gitCommitName} + gitCommitEmail={state.localSettings.gitCommitEmail} onInputChange={handleInputChange} /> diff --git a/server/internal/app/app.go b/server/internal/app/app.go index 02730d5..75f1c04 100644 --- a/server/internal/app/app.go +++ b/server/internal/app/app.go @@ -2,6 +2,7 @@ package app import ( + "database/sql" "fmt" "log" "net/http" @@ -12,12 +13,14 @@ import ( "github.com/go-chi/cors" "github.com/go-chi/httprate" "github.com/unrolled/secure" + "golang.org/x/crypto/bcrypt" "novamd/internal/api" "novamd/internal/auth" "novamd/internal/config" "novamd/internal/db" "novamd/internal/handlers" + "novamd/internal/models" "novamd/internal/secrets" "novamd/internal/storage" ) @@ -47,6 +50,12 @@ func NewServer(cfg *config.Config) (*Server, error) { // Initialize filesystem 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 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 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 + +} diff --git a/server/internal/db/migrations.go b/server/internal/db/migrations.go index f59b20f..a5d1586 100644 --- a/server/internal/db/migrations.go +++ b/server/internal/db/migrations.go @@ -15,70 +15,65 @@ var migrations = []Migration{ { Version: 1, SQL: ` - -- Create users table - CREATE TABLE IF NOT EXISTS users ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - email TEXT NOT NULL UNIQUE, - display_name TEXT, - password_hash TEXT NOT NULL, - role TEXT NOT NULL CHECK(role IN ('admin', 'editor', 'viewer')), - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - last_workspace_id INTEGER - ); + -- Create users table + CREATE TABLE IF NOT EXISTS users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + email TEXT NOT NULL UNIQUE, + display_name TEXT, + password_hash TEXT NOT NULL, + role TEXT NOT NULL CHECK(role IN ('admin', 'editor', 'viewer')), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + last_workspace_id INTEGER + ); - -- Create workspaces table with integrated settings - CREATE TABLE IF NOT EXISTS workspaces ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - user_id INTEGER NOT NULL, - name TEXT NOT NULL, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - last_opened_file_path TEXT, - -- Settings fields - theme TEXT NOT NULL DEFAULT 'light' CHECK(theme IN ('light', 'dark')), - auto_save BOOLEAN NOT NULL DEFAULT 0, - git_enabled BOOLEAN NOT NULL DEFAULT 0, - git_url TEXT, - git_user TEXT, - git_token TEXT, - git_auto_commit BOOLEAN NOT NULL DEFAULT 0, - git_commit_msg_template TEXT DEFAULT '${action} ${filename}', - FOREIGN KEY (user_id) REFERENCES users (id) - ); - `, - }, - { - Version: 2, - SQL: ` - -- 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 - ); + -- Create workspaces table with integrated settings + CREATE TABLE IF NOT EXISTS workspaces ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL, + name TEXT NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + last_opened_file_path TEXT, + -- Settings fields + theme TEXT NOT NULL DEFAULT 'light' CHECK(theme IN ('light', 'dark')), + auto_save BOOLEAN NOT NULL DEFAULT 0, + git_enabled BOOLEAN NOT NULL DEFAULT 0, + git_url TEXT, + git_user TEXT, + git_token TEXT, + git_auto_commit BOOLEAN NOT NULL DEFAULT 0, + git_commit_msg_template TEXT DEFAULT '${action} ${filename}', + 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), + updated_at TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES users (id) + ); - -- Add show_hidden_files field to workspaces - ALTER TABLE workspaces ADD COLUMN show_hidden_files BOOLEAN NOT NULL DEFAULT 0; + -- 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 indexes for performance - CREATE INDEX idx_sessions_user_id ON sessions(user_id); - CREATE INDEX idx_sessions_expires_at ON sessions(expires_at); - 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 + ); - -- Add audit fields to workspaces - ALTER TABLE workspaces ADD COLUMN created_by INTEGER REFERENCES users(id); - ALTER TABLE workspaces ADD COLUMN updated_by INTEGER REFERENCES users(id); - ALTER TABLE workspaces ADD COLUMN updated_at TIMESTAMP; - - -- 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 idx_sessions_user_id ON sessions(user_id); + CREATE INDEX idx_sessions_expires_at ON sessions(expires_at); + CREATE INDEX idx_sessions_refresh_token ON sessions(refresh_token); + `, }, } diff --git a/server/internal/db/migrations_test.go b/server/internal/db/migrations_test.go index 0b966c0..7272793 100644 --- a/server/internal/db/migrations_test.go +++ b/server/internal/db/migrations_test.go @@ -27,8 +27,8 @@ func TestMigrate(t *testing.T) { t.Fatalf("failed to get migration version: %v", err) } - if version != 2 { // Current number of migrations in production code - t.Errorf("expected migration version 2, got %d", version) + if version != 1 { // Current number of migrations in production code + t.Errorf("expected migration version 1, got %d", version) } // 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) } - if count != 2 { - t.Errorf("expected 2 migration entries, got %d", count) + if count != 1 { + 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) } - if count != 2 { - t.Errorf("expected 2 migration entries, got %d", count) + if count != 1 { + 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) } - if version != 2 { - t.Errorf("expected migration version to remain at 2, got %d", version) + if version != 1 { + t.Errorf("expected migration version to remain at 1, got %d", version) } }) } diff --git a/server/internal/db/users.go b/server/internal/db/users.go index 618dd3d..ecb2da1 100644 --- a/server/internal/db/users.go +++ b/server/internal/db/users.go @@ -68,12 +68,14 @@ func (db *database) createWorkspaceTx(tx *sql.Tx, workspace *models.Workspace) e user_id, name, theme, auto_save, show_hidden_files, git_enabled, git_url, git_user, git_token, - git_auto_commit, git_commit_msg_template - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + git_auto_commit, git_commit_msg_template, + git_commit_name, git_commit_email + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, workspace.UserID, workspace.Name, workspace.Theme, workspace.AutoSave, workspace.ShowHiddenFiles, workspace.GitEnabled, workspace.GitURL, workspace.GitUser, workspace.GitToken, workspace.GitAutoCommit, workspace.GitCommitMsgTemplate, + workspace.GitCommitName, workspace.GitCommitEmail, ) if err != nil { return err diff --git a/server/internal/db/workspaces.go b/server/internal/db/workspaces.go index efbbab4..667cf98 100644 --- a/server/internal/db/workspaces.go +++ b/server/internal/db/workspaces.go @@ -23,11 +23,12 @@ func (db *database) CreateWorkspace(workspace *models.Workspace) error { INSERT INTO workspaces ( user_id, name, theme, auto_save, show_hidden_files, git_enabled, git_url, git_user, git_token, - git_auto_commit, git_commit_msg_template - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + git_auto_commit, git_commit_msg_template, + git_commit_name, git_commit_email + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, workspace.UserID, workspace.Name, workspace.Theme, workspace.AutoSave, workspace.ShowHiddenFiles, workspace.GitEnabled, workspace.GitURL, workspace.GitUser, encryptedToken, - workspace.GitAutoCommit, workspace.GitCommitMsgTemplate, + workspace.GitAutoCommit, workspace.GitCommitMsgTemplate, workspace.GitCommitName, workspace.GitCommitEmail, ) if err != nil { return err @@ -51,7 +52,8 @@ func (db *database) GetWorkspaceByID(id int) (*models.Workspace, error) { id, user_id, name, created_at, theme, auto_save, show_hidden_files, 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 WHERE id = ?`, id, @@ -59,7 +61,7 @@ func (db *database) GetWorkspaceByID(id int) (*models.Workspace, error) { &workspace.ID, &workspace.UserID, &workspace.Name, &workspace.CreatedAt, &workspace.Theme, &workspace.AutoSave, &workspace.ShowHiddenFiles, &workspace.GitEnabled, &workspace.GitURL, &workspace.GitUser, &encryptedToken, - &workspace.GitAutoCommit, &workspace.GitCommitMsgTemplate, + &workspace.GitAutoCommit, &workspace.GitCommitMsgTemplate, &workspace.GitCommitName, &workspace.GitCommitEmail, ) if err != nil { return nil, err @@ -84,7 +86,8 @@ func (db *database) GetWorkspaceByName(userID int, workspaceName string) (*model id, user_id, name, created_at, theme, auto_save, show_hidden_files, 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 WHERE user_id = ? AND name = ?`, userID, workspaceName, @@ -93,6 +96,7 @@ func (db *database) GetWorkspaceByName(userID int, workspaceName string) (*model &workspace.Theme, &workspace.AutoSave, &workspace.ShowHiddenFiles, &workspace.GitEnabled, &workspace.GitURL, &workspace.GitUser, &encryptedToken, &workspace.GitAutoCommit, &workspace.GitCommitMsgTemplate, + &workspace.GitCommitName, &workspace.GitCommitEmail, ) if err != nil { return nil, err @@ -127,7 +131,9 @@ func (db *database) UpdateWorkspace(workspace *models.Workspace) error { git_user = ?, git_token = ?, git_auto_commit = ?, - git_commit_msg_template = ? + git_commit_msg_template = ?, + git_commit_name = ?, + git_commit_email = ? WHERE id = ? AND user_id = ?`, workspace.Name, workspace.Theme, @@ -139,6 +145,8 @@ func (db *database) UpdateWorkspace(workspace *models.Workspace) error { encryptedToken, workspace.GitAutoCommit, workspace.GitCommitMsgTemplate, + workspace.GitCommitName, + workspace.GitCommitEmail, workspace.ID, workspace.UserID, ) @@ -152,7 +160,8 @@ func (db *database) GetWorkspacesByUserID(userID int) ([]*models.Workspace, erro id, user_id, name, created_at, theme, auto_save, show_hidden_files, 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 WHERE user_id = ?`, userID, @@ -171,6 +180,7 @@ func (db *database) GetWorkspacesByUserID(userID int) ([]*models.Workspace, erro &workspace.Theme, &workspace.AutoSave, &workspace.ShowHiddenFiles, &workspace.GitEnabled, &workspace.GitURL, &workspace.GitUser, &encryptedToken, &workspace.GitAutoCommit, &workspace.GitCommitMsgTemplate, + &workspace.GitCommitName, &workspace.GitCommitEmail, ) if err != nil { return nil, err @@ -201,7 +211,9 @@ func (db *database) UpdateWorkspaceSettings(workspace *models.Workspace) error { git_user = ?, git_token = ?, git_auto_commit = ?, - git_commit_msg_template = ? + git_commit_msg_template = ?, + git_commit_name = ?, + git_commit_email = ? WHERE id = ?`, workspace.Theme, workspace.AutoSave, @@ -212,6 +224,8 @@ func (db *database) UpdateWorkspaceSettings(workspace *models.Workspace) error { workspace.GitToken, workspace.GitAutoCommit, workspace.GitCommitMsgTemplate, + workspace.GitCommitName, + workspace.GitCommitEmail, workspace.ID, ) return err @@ -261,7 +275,8 @@ func (db *database) GetAllWorkspaces() ([]*models.Workspace, error) { id, user_id, name, created_at, theme, auto_save, show_hidden_files, 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`, ) if err != nil { @@ -278,6 +293,7 @@ func (db *database) GetAllWorkspaces() ([]*models.Workspace, error) { &workspace.Theme, &workspace.AutoSave, &workspace.ShowHiddenFiles, &workspace.GitEnabled, &workspace.GitURL, &workspace.GitUser, &encryptedToken, &workspace.GitAutoCommit, &workspace.GitCommitMsgTemplate, + &workspace.GitCommitName, &workspace.GitCommitEmail, ) if err != nil { return nil, err diff --git a/server/internal/db/workspaces_test.go b/server/internal/db/workspaces_test.go index aa23163..722c7a1 100644 --- a/server/internal/db/workspaces_test.go +++ b/server/internal/db/workspaces_test.go @@ -69,6 +69,8 @@ func TestWorkspaceOperations(t *testing.T) { GitToken: "secret-token", GitAutoCommit: true, GitCommitMsgTemplate: "${action} ${filename}", + GitCommitName: "Test User", + GitCommitEmail: "test@example.com", }, wantErr: false, }, @@ -244,6 +246,8 @@ func TestWorkspaceOperations(t *testing.T) { workspace.GitToken = "new-token" workspace.GitAutoCommit = true workspace.GitCommitMsgTemplate = "custom ${filename}" + workspace.GitCommitName = "Test User" + workspace.GitCommitEmail = "test@example.com" if err := database.UpdateWorkspace(workspace); err != nil { 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 { 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() { t.Error("CreatedAt should not be zero") } diff --git a/server/internal/git/client.go b/server/internal/git/client.go index 0040042..0b9cc9b 100644 --- a/server/internal/git/client.go +++ b/server/internal/git/client.go @@ -5,17 +5,21 @@ import ( "fmt" "os" "path/filepath" + "time" "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" ) // Config holds the configuration for a Git client type Config struct { - URL string - Username string - Token string - WorkDir string + URL string + Username string + Token string + WorkDir string + CommitName string + CommitEmail string } // Client defines the interface for Git operations @@ -34,13 +38,15 @@ type client struct { } // 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{ Config: Config{ - URL: url, - Username: username, - Token: token, - WorkDir: workDir, + URL: url, + Username: username, + Token: token, + 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) } - _, 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 { return fmt.Errorf("failed to commit changes: %w", err) } diff --git a/server/internal/handlers/integration_test.go b/server/internal/handlers/integration_test.go index 5c65f16..9b380e9 100644 --- a/server/internal/handlers/integration_test.go +++ b/server/internal/handlers/integration_test.go @@ -70,7 +70,7 @@ func setupTestHarness(t *testing.T) *testHarness { // Create storage with mock git client 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 }, } diff --git a/server/internal/handlers/workspace_handlers.go b/server/internal/handlers/workspace_handlers.go index dc4d94d..514c9ff 100644 --- a/server/internal/handlers/workspace_handlers.go +++ b/server/internal/handlers/workspace_handlers.go @@ -64,6 +64,8 @@ func (h *Handler) CreateWorkspace() http.HandlerFunc { workspace.GitURL, workspace.GitUser, workspace.GitToken, + workspace.GitCommitName, + workspace.GitCommitEmail, ); err != nil { http.Error(w, "Failed to setup git repo: "+err.Error(), http.StatusInternalServerError) return @@ -96,7 +98,9 @@ func gitSettingsChanged(new, old *models.Workspace) bool { if new.GitEnabled { return new.GitURL != old.GitURL || new.GitUser != old.GitUser || - new.GitToken != old.GitToken + new.GitToken != old.GitToken || + new.GitCommitName != old.GitCommitName || + new.GitCommitEmail != old.GitCommitEmail } return false @@ -135,6 +139,8 @@ func (h *Handler) UpdateWorkspace() http.HandlerFunc { workspace.GitURL, workspace.GitUser, workspace.GitToken, + workspace.GitCommitName, + workspace.GitCommitEmail, ); err != nil { http.Error(w, "Failed to setup git repo: "+err.Error(), http.StatusInternalServerError) return diff --git a/server/internal/handlers/workspace_handlers_integration_test.go b/server/internal/handlers/workspace_handlers_integration_test.go index 724350c..0d44b73 100644 --- a/server/internal/handlers/workspace_handlers_integration_test.go +++ b/server/internal/handlers/workspace_handlers_integration_test.go @@ -54,12 +54,14 @@ func TestWorkspaceHandlers_Integration(t *testing.T) { t.Run("create with git settings", func(t *testing.T) { workspace := &models.Workspace{ - Name: "Git Workspace", - GitEnabled: true, - GitURL: "https://github.com/test/repo.git", - GitUser: "testuser", - GitToken: "testtoken", - GitAutoCommit: true, + Name: "Git Workspace", + GitEnabled: true, + GitURL: "https://github.com/test/repo.git", + GitUser: "testuser", + GitToken: "testtoken", + GitAutoCommit: true, + GitCommitName: "Test User", + GitCommitEmail: "test@example.com", } 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.GitToken, created.GitToken) 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) { @@ -161,13 +165,15 @@ func TestWorkspaceHandlers_Integration(t *testing.T) { t.Run("enable git", func(t *testing.T) { update := &models.Workspace{ - Name: workspace.Name, - Theme: "dark", - GitEnabled: true, - GitURL: "https://github.com/test/repo.git", - GitUser: "testuser", - GitToken: "testtoken", - GitAutoCommit: true, + Name: workspace.Name, + Theme: "dark", + GitEnabled: true, + GitURL: "https://github.com/test/repo.git", + GitUser: "testuser", + GitToken: "testtoken", + GitAutoCommit: true, + GitCommitName: "Test User", + GitCommitEmail: "test@example.com", } 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.GitUser, updated.GitUser) 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 assert.True(t, h.MockGit.IsInitialized()) diff --git a/server/internal/models/workspace.go b/server/internal/models/workspace.go index 584155a..6589957 100644 --- a/server/internal/models/workspace.go +++ b/server/internal/models/workspace.go @@ -22,6 +22,8 @@ type Workspace struct { GitToken string `json:"gitToken" validate:"required_if=GitEnabled true"` GitAutoCommit bool `json:"gitAutoCommit"` GitCommitMsgTemplate string `json:"gitCommitMsgTemplate"` + GitCommitName string `json:"gitCommitName"` + GitCommitEmail string `json:"gitCommitEmail" validate:"omitempty,required_if=GitEnabled true,email"` } // Validate validates the workspace struct diff --git a/server/internal/storage/filesystem_test.go b/server/internal/storage/filesystem_test.go index 238fc52..a109198 100644 --- a/server/internal/storage/filesystem_test.go +++ b/server/internal/storage/filesystem_test.go @@ -59,6 +59,7 @@ type mockFS struct { StatError error } +//revive:disable:unexported-return func NewMockFS() *mockFS { return &mockFS{ ReadCalls: make(map[string]int), diff --git a/server/internal/storage/git.go b/server/internal/storage/git.go index 49d564b..13a32d0 100644 --- a/server/internal/storage/git.go +++ b/server/internal/storage/git.go @@ -7,7 +7,7 @@ import ( // RepositoryManager defines the interface for managing Git repositories. 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) StageCommitAndPush(userID, workspaceID int, message string) 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. // 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) if _, ok := s.GitRepos[userID]; !ok { 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() } diff --git a/server/internal/storage/git_test.go b/server/internal/storage/git_test.go index b18ce35..a849ff5 100644 --- a/server/internal/storage/git_test.go +++ b/server/internal/storage/git_test.go @@ -55,6 +55,7 @@ func TestSetupGitRepo(t *testing.T) { gitURL string gitUser string gitToken string + commitEmail string mockErr error wantErr bool }{ @@ -65,6 +66,7 @@ func TestSetupGitRepo(t *testing.T) { gitURL: "https://github.com/user/repo", gitUser: "user", gitToken: "token", + commitEmail: "test@example.com", mockErr: nil, wantErr: false, }, @@ -75,6 +77,7 @@ func TestSetupGitRepo(t *testing.T) { gitURL: "https://github.com/user/repo", gitUser: "user", gitToken: "token", + commitEmail: "test@example.com", mockErr: errors.New("git initialization failed"), wantErr: true, }, @@ -86,7 +89,7 @@ func TestSetupGitRepo(t *testing.T) { mockClient := &MockGitClient{ReturnError: tc.mockErr} // Create a client factory that returns our configured mock - mockClientFactory := func(_, _, _, _ string) git.Client { + mockClientFactory := func(_, _, _, _, _, _ string) git.Client { return mockClient } @@ -96,7 +99,7 @@ func TestSetupGitRepo(t *testing.T) { }) // 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 err == nil { @@ -131,7 +134,7 @@ func TestGitOperations(t *testing.T) { mockFS := NewMockFS() s := storage.NewServiceWithOptions("test-root", storage.Options{ 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) { @@ -203,7 +206,7 @@ func TestDisableGitRepo(t *testing.T) { mockFS := NewMockFS() s := storage.NewServiceWithOptions("test-root", storage.Options{ Fs: mockFS, - NewGitClient: func(_, _, _, _ string) git.Client { return &MockGitClient{} }, + NewGitClient: func(_, _, _, _, _, _ string) git.Client { return &MockGitClient{} }, }) testCases := []struct { diff --git a/server/internal/storage/service.go b/server/internal/storage/service.go index 0516b5f..07e6b1e 100644 --- a/server/internal/storage/service.go +++ b/server/internal/storage/service.go @@ -14,7 +14,7 @@ type Manager interface { // Service represents the file system structure. type Service struct { fs fileSystem - newGitClient func(url, user, token, path string) git.Client + newGitClient func(url, user, token, path, commitName, commitEmail string) git.Client RootDir string 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. type Options struct { 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. diff --git a/server/internal/user/user.go b/server/internal/user/user.go deleted file mode 100644 index 20383ca..0000000 --- a/server/internal/user/user.go +++ /dev/null @@ -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 -}