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

@@ -42,13 +42,15 @@ var migrations = []Migration{
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}',
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) FOREIGN KEY (user_id) REFERENCES users (id)
); );
`,
},
{
Version: 2,
SQL: `
-- Create sessions table for authentication -- Create sessions table for authentication
CREATE TABLE IF NOT EXISTS sessions ( CREATE TABLE IF NOT EXISTS sessions (
id TEXT PRIMARY KEY, id TEXT PRIMARY KEY,
@@ -59,26 +61,19 @@ var migrations = []Migration{
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
); );
-- Add show_hidden_files field to workspaces
ALTER TABLE workspaces ADD COLUMN show_hidden_files BOOLEAN NOT NULL DEFAULT 0;
-- 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);
-- 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 system_settings table for application settings
CREATE TABLE IF NOT EXISTS system_settings ( CREATE TABLE IF NOT EXISTS system_settings (
key TEXT PRIMARY KEY, key TEXT PRIMARY KEY,
value TEXT NOT NULL, value TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_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);
`,
}, },
} }

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,8 +5,10 @@ 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"
) )
@@ -16,6 +18,8 @@ type Config struct {
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

@@ -60,6 +60,8 @@ func TestWorkspaceHandlers_Integration(t *testing.T) {
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) {
@@ -168,6 +172,8 @@ func TestWorkspaceHandlers_Integration(t *testing.T) {
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
}