From e1a49266cebb8b1d0c36fd4593bc83fd7701be95 Mon Sep 17 00:00:00 2001 From: Aidan Traboulay Date: Sun, 15 Dec 2024 03:04:09 -0400 Subject: [PATCH 01/24] purge server types --- backend/internal/server/types.go | 206 ------------------------------- 1 file changed, 206 deletions(-) diff --git a/backend/internal/server/types.go b/backend/internal/server/types.go index 13b34c9..e69de29 100644 --- a/backend/internal/server/types.go +++ b/backend/internal/server/types.go @@ -1,206 +0,0 @@ -package server - -import ( - "time" - - "KonferCA/SPUR/db" -) - -// TODO: Reorder types -// What is the criteria of the ordering? - Juan - -type DatabaseInfo struct { - Connected bool `json:"connected"` - LatencyMs float64 `json:"latency_ms"` - PostgresVersion string `json:"postgres_version,omitempty"` - Error string `json:"error,omitempty"` -} - -type SystemInfo struct { - Version string `json:"version"` - GoVersion string `json:"go_version"` - NumGoRoutine int `json:"num_goroutines"` - MemoryUsage float64 `json:"memory_usage"` -} - -type HealthReport struct { - Status string `json:"status"` - Timestamp time.Time `json:"timestamp"` - Database DatabaseInfo `json:"database"` - System SystemInfo `json:"system"` -} - -type CreateCompanyRequest struct { - OwnerUserID string `json:"owner_user_id" validate:"required,uuid"` - Name string `json:"name" validate:"required"` - Description *string `json:"description"` -} - -type CreateResourceRequestRequest struct { - CompanyID string `json:"company_id" validate:"required,uuid"` - ResourceType string `json:"resource_type" validate:"required"` - Description *string `json:"description"` - Status string `json:"status" validate:"required"` -} - -type SignupRequest struct { - Email string `json:"email" validate:"required,email"` - Password string `json:"password" validate:"required,min=8"` - Role db.UserRole `json:"role" validate:"required,valid_user_role,non_admin_role"` -} - -type SigninRequest struct { - Email string `json:"email" validate:"required,email"` - Password string `json:"password" validate:"required"` -} - -type AuthResponse struct { - AccessToken string `json:"access_token"` - RefreshToken string `json:"refresh_token"` - User User `json:"user"` -} - -type User struct { - ID string `json:"id"` - Email string `json:"email"` - FirstName *string `json:"first_name"` - LastName *string `json:"last_name"` - Role db.UserRole `json:"role"` - WalletAddress *string `json:"wallet_address,omitempty"` - EmailVerified bool `json:"email_verified"` -} - -type CreateCompanyFinancialsRequest struct { - FinancialYear int32 `json:"financial_year" validate:"required"` - Revenue float64 `json:"revenue" validate:"required"` - Expenses float64 `json:"expenses" validate:"required"` - Profit float64 `json:"profit" validate:"required"` - Sales float64 `json:"sales" validate:"required"` - AmountRaised float64 `json:"amount_raised" validate:"required"` - ARR float64 `json:"arr" validate:"required"` - GrantsReceived float64 `json:"grants_received" validate:"required"` -} - -type CreateEmployeeRequest struct { - CompanyID string `json:"company_id" validate:"required,uuid"` - Name string `json:"name" validate:"required"` - Email string `json:"email" validate:"required,email"` - Role string `json:"role" validate:"required"` - Bio *string `json:"bio"` -} - -type UpdateEmployeeRequest struct { - Name string `json:"name" validate:"required"` - Role string `json:"role" validate:"required"` - Bio *string `json:"bio"` -} - -type CreateCompanyDocumentRequest struct { - CompanyID string `json:"company_id" validate:"required,uuid"` - DocumentType string `json:"document_type" validate:"required"` - FileURL string `json:"file_url" validate:"required,url"` -} - -type UpdateCompanyDocumentRequest struct { - DocumentType string `json:"document_type" validate:"required"` - FileURL string `json:"file_url" validate:"required,url"` -} - -type CreateQuestionRequest struct { - QuestionText string `json:"question_text" validate:"required"` -} - -type CreateCompanyAnswerRequest struct { - QuestionID string `json:"question_id" validate:"required,uuid"` - AnswerText string `json:"answer_text" validate:"required"` -} - -type UpdateCompanyAnswerRequest struct { - AnswerText string `json:"answer_text" validate:"required"` -} - -type TeamMember struct { - ID string `json:"id"` - Name string `json:"name"` - Role string `json:"role"` - Avatar string `json:"avatar,omitempty"` -} - -type ProjectFile struct { - FileType string `json:"file_type" validate:"required"` - FileURL string `json:"file_url" validate:"required,url,s3_url"` -} - -type ProjectLink struct { - LinkType string `json:"link_type" validate:"required"` - URL string `json:"url" validate:"required,url"` -} - -type UpdateProjectRequest struct { - Title string `json:"title" validate:"required"` - Description string `json:"description"` - Status string `json:"status" validate:"required"` -} - -type CreateProjectCommentRequest struct { - UserID string `json:"user_id" validate:"required"` - Comment string `json:"comment" validate:"required"` -} - -type AddProjectTagRequest struct { - TagID string `json:"tag_id" validate:"required,uuid"` -} - -type CreateTagRequest struct { - Name string `json:"name" validate:"required"` -} - -type CreateFundingTransactionRequest struct { - ProjectID string `json:"project_id" validate:"required,uuid"` - Amount string `json:"amount" validate:"required"` - Currency string `json:"currency" validate:"required,len=3"` - TransactionHash string `json:"transaction_hash" validate:"required"` - FromWalletAddress string `json:"from_wallet_address" validate:"required"` - ToWalletAddress string `json:"to_wallet_address" validate:"required"` - Status string `json:"status" validate:"required,oneof=PENDING COMPLETED FAILED"` -} - -type UpdateFundingTransactionStatusRequest struct { - Status string `json:"status" validate:"required,oneof=PENDING COMPLETED FAILED"` -} - -type CreateMeetingRequest struct { - ProjectID string `json:"project_id" validate:"required,uuid"` - ScheduledByUserID string `json:"scheduled_by_user_id" validate:"required,uuid"` - StartTime string `json:"start_time" validate:"required,datetime=2006-01-02T15:04:05.000Z"` - EndTime string `json:"end_time" validate:"required,datetime=2006-01-02T15:04:05.000Z"` - MeetingURL *string `json:"meeting_url" validate:"omitempty,url"` - Location *string `json:"location"` - Notes *string `json:"notes"` -} - -type UpdateMeetingRequest struct { - StartTime string `json:"start_time" validate:"required,datetime=2006-01-02T15:04:05.000Z"` - EndTime string `json:"end_time" validate:"required,datetime=2006-01-02T15:04:05.000Z"` - MeetingURL *string `json:"meeting_url" validate:"omitempty,url"` - Location *string `json:"location"` - Notes *string `json:"notes"` -} - -type EmailVerifiedStatusResponse struct { - Verified bool `json:"verified"` -} - -type ResendVerificationEmailRequest struct { - Email string `json:"email" validate:"required,email"` -} - -type CreateProjectFileRequest struct { - FileType string `json:"file_type" validate:"required"` - FileURL string `json:"file_url" validate:"required,url,s3_url"` -} - -type CreateProjectLinkRequest struct { - LinkType string `json:"link_type" validate:"required"` - URL string `json:"url" validate:"required,url"` -} From 08b40637195d3db84bc296f3a35af2444da6ec8a Mon Sep 17 00:00:00 2001 From: Aidan Traboulay Date: Sun, 15 Dec 2024 03:04:50 -0400 Subject: [PATCH 02/24] create new unified schema --- backend/.sqlc/migrations/schema.sql | 127 ++++++++++++++++++++++++++++ 1 file changed, 127 insertions(+) create mode 100644 backend/.sqlc/migrations/schema.sql diff --git a/backend/.sqlc/migrations/schema.sql b/backend/.sqlc/migrations/schema.sql new file mode 100644 index 0000000..ff7f722 --- /dev/null +++ b/backend/.sqlc/migrations/schema.sql @@ -0,0 +1,127 @@ +CREATE TYPE user_role AS ENUM ( + 'admin', + 'startup_owner', + 'investor' +); + +CREATE TYPE project_status AS ENUM ( + 'draft', + 'pending', + 'verified', + 'declined' +); + +CREATE TABLE IF NOT EXISTS users ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + email varchar UNIQUE NOT NULL, + password char(256) NOT NULL, + role user_role NOT NULL, + email_verified boolean NOT NULL DEFAULT false, + created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + token_salt bytea UNIQUE NOT NULL DEFAULT gen_random_bytes(32) +); + +CREATE TABLE IF NOT EXISTS verify_email_tokens ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE, + created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + expires_at timestamp NOT NULL +); + +CREATE TABLE IF NOT EXISTS companies ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + owner_id uuid NOT NULL REFERENCES users(id), + name varchar NOT NULL, + wallet_address varchar, + linkedin_url varchar NOT NULL, + created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS team_members ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + company_id uuid NOT NULL REFERENCES companies(id) ON DELETE CASCADE, + first_name varchar NOT NULL, + last_name varchar NOT NULL, + title varchar NOT NULL, + bio varchar NOT NULL, + linkedin_url varchar NOT NULL, + is_account_owner boolean NOT NULL DEFAULT false, + created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS projects ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + company_id uuid NOT NULL REFERENCES companies(id), + title varchar NOT NULL, + description varchar, + status project_status NOT NULL DEFAULT 'draft', + created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS project_questions ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + question varchar NOT NULL, + section varchar NOT NULL DEFAULT 'overall', + created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS project_answers ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + project_id uuid NOT NULL REFERENCES projects(id) ON DELETE CASCADE, + question_id uuid NOT NULL REFERENCES project_questions(id), + answer varchar NOT NULL DEFAULT '', + created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + UNIQUE(project_id, question_id) +); + +CREATE TABLE IF NOT EXISTS project_documents ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + project_id uuid NOT NULL REFERENCES projects(id) ON DELETE CASCADE, + name varchar NOT NULL, + url varchar NOT NULL, + section varchar NOT NULL DEFAULT 'overall', + created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS project_comments ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + project_id uuid NOT NULL REFERENCES projects(id) ON DELETE CASCADE, + target_id uuid NOT NULL, + comment uuid NOT NULL, + commenter_id uuid NOT NULL REFERENCES users(id), + created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS transactions ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + project_id uuid NOT NULL REFERENCES projects(id), + company_id uuid NOT NULL REFERENCES companies(id), + tx_hash varchar NOT NULL, + block_number bigint NOT NULL, + from_address varchar NOT NULL, + to_address varchar NOT NULL, + value_amount decimal(65,18) NOT NULL, + currency_symbol varchar NOT NULL, + gas_price decimal(65,18), + gas_used bigint, + total_fee decimal(65,18), + status boolean NOT NULL, + nonce bigint NOT NULL, + created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX idx_users_email ON users(email); +CREATE INDEX idx_companies_owner ON companies(owner_id); +CREATE INDEX idx_projects_company ON projects(company_id); +CREATE INDEX idx_project_answers_project ON project_answers(project_id); +CREATE INDEX idx_transactions_project ON transactions(project_id); +CREATE INDEX idx_transactions_company ON transactions(company_id); +CREATE INDEX idx_team_members_company ON team_members(company_id); \ No newline at end of file From ef08f8e5e64efcb8f18b12da8b006d4aeea9e1b7 Mon Sep 17 00:00:00 2001 From: Aidan Traboulay Date: Sun, 15 Dec 2024 03:05:01 -0400 Subject: [PATCH 03/24] add interface types --- backend/internal/interfaces/types.go | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 backend/internal/interfaces/types.go diff --git a/backend/internal/interfaces/types.go b/backend/internal/interfaces/types.go new file mode 100644 index 0000000..e69de29 From 86d6ca9e391b3ea7e4f212a135cf117d7c5a43b8 Mon Sep 17 00:00:00 2001 From: Aidan Traboulay Date: Sun, 15 Dec 2024 03:05:18 -0400 Subject: [PATCH 04/24] add cors file --- backend/internal/server/cors.go | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 backend/internal/server/cors.go diff --git a/backend/internal/server/cors.go b/backend/internal/server/cors.go new file mode 100644 index 0000000..e69de29 From 0d54ac80b543c8bb7d1383fbd82b6fcf7606ff64 Mon Sep 17 00:00:00 2001 From: Aidan Traboulay Date: Sun, 15 Dec 2024 03:05:29 -0400 Subject: [PATCH 05/24] add middleware file --- backend/internal/server/middleware.go | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 backend/internal/server/middleware.go diff --git a/backend/internal/server/middleware.go b/backend/internal/server/middleware.go new file mode 100644 index 0000000..e69de29 From 95c564b2a1c9dcadc14afeae0e5d7d5cd4b51fa4 Mon Sep 17 00:00:00 2001 From: Aidan Traboulay Date: Sun, 15 Dec 2024 03:05:45 -0400 Subject: [PATCH 06/24] move tests to own dir --- backend/internal/tests/jwt_middleware_test.go | 132 ++++++++++++++++++ backend/internal/tests/jwt_test.go | 125 +++++++++++++++++ backend/internal/tests/req_validator_test.go | 108 ++++++++++++++ 3 files changed, 365 insertions(+) create mode 100644 backend/internal/tests/jwt_middleware_test.go create mode 100644 backend/internal/tests/jwt_test.go create mode 100644 backend/internal/tests/req_validator_test.go diff --git a/backend/internal/tests/jwt_middleware_test.go b/backend/internal/tests/jwt_middleware_test.go new file mode 100644 index 0000000..adbfb85 --- /dev/null +++ b/backend/internal/tests/jwt_middleware_test.go @@ -0,0 +1,132 @@ +package middleware + +import ( + "context" + "fmt" + "net/http" + "net/http/httptest" + "os" + "testing" + + "KonferCA/SPUR/db" + "KonferCA/SPUR/internal/jwt" + "github.com/google/uuid" + "github.com/jackc/pgx/v5/pgxpool" + "github.com/labstack/echo/v4" + "github.com/stretchr/testify/assert" +) + +func TestProtectAPIForAccessToken(t *testing.T) { + // setup test environment + os.Setenv("JWT_SECRET", "secret") + + // Connect to test database + ctx := context.Background() + dbURL := fmt.Sprintf("postgres://%s:%s@%s:%s/%s?sslmode=%s", + "postgres", + "postgres", + "localhost", + "5432", + "postgres", + "disable", + ) + + dbPool, err := pgxpool.New(ctx, dbURL) + if err != nil { + t.Fatalf("failed to connect to database: %v", err) + } + defer dbPool.Close() + + // Clean up any existing test user + _, err = dbPool.Exec(ctx, "DELETE FROM users WHERE email = $1", "test@example.com") + if err != nil { + t.Fatalf("failed to clean up test user: %v", err) + } + + // Create a test user directly in the database + userID := uuid.New().String() + _, err = dbPool.Exec(ctx, ` + INSERT INTO users (id, email, password_hash, first_name, last_name, role, token_salt) + VALUES ($1, $2, $3, $4, $5, 'startup_owner', gen_random_bytes(32)) + `, userID, "test@example.com", "hashedpassword", "Test", "User") + if err != nil { + t.Fatalf("failed to create test user: %v", err) + } + + // Create Echo instance with the DB connection + e := echo.New() + queries := db.New(dbPool) + middleware := ProtectAPI(jwt.ACCESS_TOKEN_TYPE, queries) + e.Use(middleware) + + e.GET("/protected", func(c echo.Context) error { + return c.String(http.StatusOK, "protected resource") + }) + + // Get test user data from the database + user, err := queries.GetUserByEmail(ctx, "test@example.com") + if err != nil { + t.Fatalf("failed to get test user: %v", err) + } + + // Get the user's salt + var salt []byte + err = dbPool.QueryRow(ctx, "SELECT token_salt FROM users WHERE id = $1", user.ID).Scan(&salt) + if err != nil { + t.Fatalf("failed to get user salt: %v", err) + } + + // generate valid tokens using the actual salt + accessToken, refreshToken, err := jwt.GenerateWithSalt(user.ID, user.Role, salt) + assert.Nil(t, err) + + tests := []struct { + name string + expectedCode int + token string + }{ + { + name: "Accept access token", + expectedCode: http.StatusOK, + token: accessToken, + }, + { + name: "Reject refresh token", + expectedCode: http.StatusUnauthorized, + token: refreshToken, + }, + { + name: "Reject invalid token format", + expectedCode: http.StatusUnauthorized, + token: "invalid-token", + }, + { + name: "Reject empty token", + expectedCode: http.StatusUnauthorized, + token: "", + }, + { + name: "Reject token with invalid signature", + expectedCode: http.StatusUnauthorized, + token: accessToken + "tampered", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + req := httptest.NewRequest(http.MethodGet, "/protected", nil) + rec := httptest.NewRecorder() + if test.token != "" { + req.Header.Set(echo.HeaderAuthorization, fmt.Sprintf("Bearer %s", test.token)) + } + e.ServeHTTP(rec, req) + assert.Equal(t, test.expectedCode, rec.Code) + }) + } + + // Clean up test user after test + _, err = dbPool.Exec(ctx, "DELETE FROM users WHERE email = $1", "test@example.com") + if err != nil { + t.Fatalf("failed to clean up test user: %v", err) + } +} \ No newline at end of file diff --git a/backend/internal/tests/jwt_test.go b/backend/internal/tests/jwt_test.go new file mode 100644 index 0000000..5b5a263 --- /dev/null +++ b/backend/internal/tests/jwt_test.go @@ -0,0 +1,125 @@ +package jwt + +import ( + "os" + "testing" + "time" + + "KonferCA/SPUR/db" + + "github.com/stretchr/testify/assert" +) + +func TestJWT(t *testing.T) { + // setup env + os.Setenv("JWT_SECRET", "secret") + os.Setenv("JWT_SECRET_VERIFY_EMAIL", "test-secret") + + userID := "some-user-id" + role := db.UserRole("user") + salt := []byte("test-salt") + + t.Run("token salt invalidation", func(t *testing.T) { + // Generate initial salt + initialSalt := []byte("initial-salt") + + // Generate tokens with initial salt + accessToken, refreshToken, err := GenerateWithSalt(userID, role, initialSalt) + assert.Nil(t, err) + assert.NotEmpty(t, accessToken) + assert.NotEmpty(t, refreshToken) + + // Verify tokens work with initial salt + claims, err := VerifyTokenWithSalt(accessToken, initialSalt) + assert.Nil(t, err) + assert.Equal(t, claims.UserID, userID) + assert.Equal(t, claims.Role, role) + assert.Equal(t, claims.TokenType, ACCESS_TOKEN_TYPE) + + // Change salt (simulating token invalidation) + newSalt := []byte("new-salt") + + // Old tokens should fail verification with new salt + _, err = VerifyTokenWithSalt(accessToken, newSalt) + assert.NotNil(t, err, "Token should be invalid with new salt") + + // Generate new tokens with new salt + newAccessToken, newRefreshToken, err := GenerateWithSalt(userID, role, newSalt) + assert.Nil(t, err) + assert.NotEmpty(t, newAccessToken) + assert.NotEmpty(t, newRefreshToken) + + // New tokens should work with new salt + claims, err = VerifyTokenWithSalt(newAccessToken, newSalt) + assert.Nil(t, err) + assert.Equal(t, claims.UserID, userID) + }) + + t.Run("two-step verification", func(t *testing.T) { + salt := []byte("test-salt") + + // Generate a token + accessToken, _, err := GenerateWithSalt(userID, role, salt) + assert.Nil(t, err) + + // Step 1: Parse claims without verification + unverifiedClaims, err := ParseUnverifiedClaims(accessToken) + assert.Nil(t, err) + assert.Equal(t, userID, unverifiedClaims.UserID) + + // Step 2: Verify with salt + verifiedClaims, err := VerifyTokenWithSalt(accessToken, salt) + assert.Nil(t, err) + assert.Equal(t, userID, verifiedClaims.UserID) + + // Try to verify with wrong salt + wrongSalt := []byte("wrong-salt") + _, err = VerifyTokenWithSalt(accessToken, wrongSalt) + assert.NotNil(t, err, "Token should be invalid with wrong salt") + }) + + t.Run("generate access token", func(t *testing.T) { + accessToken, _, err := GenerateWithSalt(userID, role, salt) + assert.Nil(t, err) + assert.NotEmpty(t, accessToken) + claims, err := VerifyTokenWithSalt(accessToken, salt) + assert.Nil(t, err) + assert.Equal(t, claims.UserID, userID) + assert.Equal(t, claims.Role, role) + assert.Equal(t, claims.TokenType, ACCESS_TOKEN_TYPE) + }) + + t.Run("generate refresh token", func(t *testing.T) { + _, refreshToken, err := GenerateWithSalt(userID, role, salt) + assert.Nil(t, err) + assert.NotEmpty(t, refreshToken) + claims, err := VerifyTokenWithSalt(refreshToken, salt) + assert.Nil(t, err) + assert.Equal(t, claims.UserID, userID) + assert.Equal(t, claims.Role, role) + assert.Equal(t, claims.TokenType, REFRESH_TOKEN_TYPE) + }) + + t.Run("verify email token", func(t *testing.T) { + email := "test@mail.com" + id := "some-id" + exp := time.Now().Add(time.Second * 5) + token, err := GenerateVerifyEmailToken(email, id, exp) + assert.Nil(t, err) + claims, err := VerifyEmailToken(token) + assert.Nil(t, err) + assert.Equal(t, claims.Email, email) + assert.Equal(t, claims.ID, id) + assert.Equal(t, claims.ExpiresAt.Unix(), exp.Unix()) + }) + + t.Run("deny expired verify email token", func(t *testing.T) { + email := "test@mail.com" + id := "some-id" + exp := time.Now().Add(-1 * 5 * time.Second) + token, err := GenerateVerifyEmailToken(email, id, exp) + assert.Nil(t, err) + _, err = VerifyEmailToken(token) + assert.NotNil(t, err) + }) +} diff --git a/backend/internal/tests/req_validator_test.go b/backend/internal/tests/req_validator_test.go new file mode 100644 index 0000000..b78d685 --- /dev/null +++ b/backend/internal/tests/req_validator_test.go @@ -0,0 +1,108 @@ +package middleware + +import ( + "bytes" + "encoding/json" + "net/http" + "net/http/httptest" + "reflect" + "testing" + + "github.com/labstack/echo/v4" + "github.com/stretchr/testify/assert" +) + +func TestRequestBodyValidator(t *testing.T) { + type testStruct struct { + TestField bool `json:"test_field" validate:"required"` + } + + e := echo.New() + e.Validator = NewRequestBodyValidator() + e.POST("/", func(c echo.Context) error { + // check that the request body is the correct interface + i, ok := c.Get(REQUEST_BODY_KEY).(*testStruct) + if !ok { + return echo.NewHTTPError(http.StatusInternalServerError) + } + + // echo back + return c.JSON(http.StatusOK, i) + }, ValidateRequestBody(reflect.TypeOf(testStruct{}))) + + tests := []struct { + name string + payload interface{} + expectedCode int + }{ + { + name: "Valid request body", + payload: testStruct{ + TestField: true, + }, + expectedCode: http.StatusOK, + }, + { + name: "Invalid request body - validation error", + payload: testStruct{ + // will fail required validation + TestField: false, + }, + // expecting 500 since the middleware its expected to return + // the original ValidationErrors from validator pkg + expectedCode: http.StatusInternalServerError, + }, + { + name: "Empty request body", + payload: nil, + // expecting 500 since the middleware its expected to return + // the original ValidationErrors from validator pkg + expectedCode: http.StatusInternalServerError, + }, + { + name: "Invalid JSON format", + payload: `{ + "test_field": invalid + }`, + expectedCode: http.StatusBadRequest, + }, + { + name: "Wrong type in JSON", + payload: map[string]interface{}{ + "test_field": "not a boolean", + }, + expectedCode: http.StatusBadRequest, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + var req *http.Request + + if tc.payload != nil { + var payload []byte + var err error + + // handle string payloads (for invalid JSON tests) + if strPayload, ok := tc.payload.(string); ok { + payload = []byte(strPayload) + } else { + payload, err = json.Marshal(tc.payload) + assert.NoError(t, err) + } + + req = httptest.NewRequest(http.MethodPost, "/", bytes.NewReader(payload)) + req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) + } else { + req = httptest.NewRequest(http.MethodPost, "/", nil) + req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) + } + + rec := httptest.NewRecorder() + e.ServeHTTP(rec, req) + + t.Log(rec.Body.String()) + assert.Equal(t, tc.expectedCode, rec.Code) + }) + } +} From 072509d1eb9d6dae30ca899939f9565543c82e7c Mon Sep 17 00:00:00 2001 From: Aidan Traboulay Date: Sun, 15 Dec 2024 03:06:27 -0400 Subject: [PATCH 07/24] add setup file (originally index) --- backend/internal/v1/setup.go | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 backend/internal/v1/setup.go diff --git a/backend/internal/v1/setup.go b/backend/internal/v1/setup.go new file mode 100644 index 0000000..e69de29 From 71d7c1c093573973d3aa73c4255d0901f5a1224c Mon Sep 17 00:00:00 2001 From: Aidan Traboulay Date: Sun, 15 Dec 2024 03:06:43 -0400 Subject: [PATCH 08/24] add v1 auth --- backend/internal/v1/v1_auth/auth.go | 0 backend/internal/v1/v1_auth/routes.go | 0 backend/internal/v1/v1_auth/types.go | 0 3 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 backend/internal/v1/v1_auth/auth.go create mode 100644 backend/internal/v1/v1_auth/routes.go create mode 100644 backend/internal/v1/v1_auth/types.go diff --git a/backend/internal/v1/v1_auth/auth.go b/backend/internal/v1/v1_auth/auth.go new file mode 100644 index 0000000..e69de29 diff --git a/backend/internal/v1/v1_auth/routes.go b/backend/internal/v1/v1_auth/routes.go new file mode 100644 index 0000000..e69de29 diff --git a/backend/internal/v1/v1_auth/types.go b/backend/internal/v1/v1_auth/types.go new file mode 100644 index 0000000..e69de29 From b562b472075101a865f331cc8b45b612c843ec95 Mon Sep 17 00:00:00 2001 From: Aidan Traboulay Date: Sun, 15 Dec 2024 03:06:56 -0400 Subject: [PATCH 09/24] add v1 common --- backend/internal/v1/v1_common/constants.go | 0 backend/internal/v1/v1_common/errors.go | 0 backend/internal/v1/v1_common/helpers.go | 0 backend/internal/v1/v1_common/response.go | 0 backend/internal/v1/v1_common/types.go | 0 5 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 backend/internal/v1/v1_common/constants.go create mode 100644 backend/internal/v1/v1_common/errors.go create mode 100644 backend/internal/v1/v1_common/helpers.go create mode 100644 backend/internal/v1/v1_common/response.go create mode 100644 backend/internal/v1/v1_common/types.go diff --git a/backend/internal/v1/v1_common/constants.go b/backend/internal/v1/v1_common/constants.go new file mode 100644 index 0000000..e69de29 diff --git a/backend/internal/v1/v1_common/errors.go b/backend/internal/v1/v1_common/errors.go new file mode 100644 index 0000000..e69de29 diff --git a/backend/internal/v1/v1_common/helpers.go b/backend/internal/v1/v1_common/helpers.go new file mode 100644 index 0000000..e69de29 diff --git a/backend/internal/v1/v1_common/response.go b/backend/internal/v1/v1_common/response.go new file mode 100644 index 0000000..e69de29 diff --git a/backend/internal/v1/v1_common/types.go b/backend/internal/v1/v1_common/types.go new file mode 100644 index 0000000..e69de29 From 599aa5865670a5bfb7b23f6ec5eb072d54aea453 Mon Sep 17 00:00:00 2001 From: Aidan Traboulay Date: Sun, 15 Dec 2024 03:07:07 -0400 Subject: [PATCH 10/24] add v1 companies --- backend/internal/v1/v1_companies/ companies.go | 0 backend/internal/v1/v1_companies/routes.go | 0 backend/internal/v1/v1_companies/types.go | 0 3 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 backend/internal/v1/v1_companies/ companies.go create mode 100644 backend/internal/v1/v1_companies/routes.go create mode 100644 backend/internal/v1/v1_companies/types.go diff --git a/backend/internal/v1/v1_companies/ companies.go b/backend/internal/v1/v1_companies/ companies.go new file mode 100644 index 0000000..e69de29 diff --git a/backend/internal/v1/v1_companies/routes.go b/backend/internal/v1/v1_companies/routes.go new file mode 100644 index 0000000..e69de29 diff --git a/backend/internal/v1/v1_companies/types.go b/backend/internal/v1/v1_companies/types.go new file mode 100644 index 0000000..e69de29 From 12f3d558209f326fab04f54dd6dd1f6a64fcd20a Mon Sep 17 00:00:00 2001 From: Aidan Traboulay Date: Sun, 15 Dec 2024 03:07:21 -0400 Subject: [PATCH 11/24] add v1 db health --- backend/internal/v1/v1_health/healtcheck.go | 0 backend/internal/v1/v1_health/routes.go | 0 backend/internal/v1/v1_health/types.go | 0 3 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 backend/internal/v1/v1_health/healtcheck.go create mode 100644 backend/internal/v1/v1_health/routes.go create mode 100644 backend/internal/v1/v1_health/types.go diff --git a/backend/internal/v1/v1_health/healtcheck.go b/backend/internal/v1/v1_health/healtcheck.go new file mode 100644 index 0000000..e69de29 diff --git a/backend/internal/v1/v1_health/routes.go b/backend/internal/v1/v1_health/routes.go new file mode 100644 index 0000000..e69de29 diff --git a/backend/internal/v1/v1_health/types.go b/backend/internal/v1/v1_health/types.go new file mode 100644 index 0000000..e69de29 From 0cff923e8863f063e59e04a108172ad7bef0621d Mon Sep 17 00:00:00 2001 From: Aidan Traboulay Date: Sun, 15 Dec 2024 03:07:30 -0400 Subject: [PATCH 12/24] add v1 projects --- backend/internal/v1/v1_projects/answers.go | 0 backend/internal/v1/v1_projects/comments.go | 0 backend/internal/v1/v1_projects/documents.go | 0 backend/internal/v1/v1_projects/projects.go | 0 backend/internal/v1/v1_projects/questions.go | 0 backend/internal/v1/v1_projects/routes.go | 0 backend/internal/v1/v1_projects/types.go | 0 7 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 backend/internal/v1/v1_projects/answers.go create mode 100644 backend/internal/v1/v1_projects/comments.go create mode 100644 backend/internal/v1/v1_projects/documents.go create mode 100644 backend/internal/v1/v1_projects/projects.go create mode 100644 backend/internal/v1/v1_projects/questions.go create mode 100644 backend/internal/v1/v1_projects/routes.go create mode 100644 backend/internal/v1/v1_projects/types.go diff --git a/backend/internal/v1/v1_projects/answers.go b/backend/internal/v1/v1_projects/answers.go new file mode 100644 index 0000000..e69de29 diff --git a/backend/internal/v1/v1_projects/comments.go b/backend/internal/v1/v1_projects/comments.go new file mode 100644 index 0000000..e69de29 diff --git a/backend/internal/v1/v1_projects/documents.go b/backend/internal/v1/v1_projects/documents.go new file mode 100644 index 0000000..e69de29 diff --git a/backend/internal/v1/v1_projects/projects.go b/backend/internal/v1/v1_projects/projects.go new file mode 100644 index 0000000..e69de29 diff --git a/backend/internal/v1/v1_projects/questions.go b/backend/internal/v1/v1_projects/questions.go new file mode 100644 index 0000000..e69de29 diff --git a/backend/internal/v1/v1_projects/routes.go b/backend/internal/v1/v1_projects/routes.go new file mode 100644 index 0000000..e69de29 diff --git a/backend/internal/v1/v1_projects/types.go b/backend/internal/v1/v1_projects/types.go new file mode 100644 index 0000000..e69de29 From b4bcbf4519d2f3e00bbecb59b457255bc415b1e3 Mon Sep 17 00:00:00 2001 From: Aidan Traboulay Date: Sun, 15 Dec 2024 03:07:38 -0400 Subject: [PATCH 13/24] add v1 teams --- backend/internal/v1/v1_teams/members.go | 0 backend/internal/v1/v1_teams/routes.go | 0 backend/internal/v1/v1_teams/types.go | 0 3 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 backend/internal/v1/v1_teams/members.go create mode 100644 backend/internal/v1/v1_teams/routes.go create mode 100644 backend/internal/v1/v1_teams/types.go diff --git a/backend/internal/v1/v1_teams/members.go b/backend/internal/v1/v1_teams/members.go new file mode 100644 index 0000000..e69de29 diff --git a/backend/internal/v1/v1_teams/routes.go b/backend/internal/v1/v1_teams/routes.go new file mode 100644 index 0000000..e69de29 diff --git a/backend/internal/v1/v1_teams/types.go b/backend/internal/v1/v1_teams/types.go new file mode 100644 index 0000000..e69de29 From 0d571d116451e311729f11e9de57cf002231cb5c Mon Sep 17 00:00:00 2001 From: Aidan Traboulay Date: Sun, 15 Dec 2024 03:07:48 -0400 Subject: [PATCH 14/24] add v1 transactions --- backend/internal/v1/v1_transactions/routes.go | 0 backend/internal/v1/v1_transactions/transactions.go | 0 backend/internal/v1/v1_transactions/types.go | 0 3 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 backend/internal/v1/v1_transactions/routes.go create mode 100644 backend/internal/v1/v1_transactions/transactions.go create mode 100644 backend/internal/v1/v1_transactions/types.go diff --git a/backend/internal/v1/v1_transactions/routes.go b/backend/internal/v1/v1_transactions/routes.go new file mode 100644 index 0000000..e69de29 diff --git a/backend/internal/v1/v1_transactions/transactions.go b/backend/internal/v1/v1_transactions/transactions.go new file mode 100644 index 0000000..e69de29 diff --git a/backend/internal/v1/v1_transactions/types.go b/backend/internal/v1/v1_transactions/types.go new file mode 100644 index 0000000..e69de29 From 509690dfe9eff086a83b95e24ebffe7421c51406 Mon Sep 17 00:00:00 2001 From: Aidan Traboulay Date: Sun, 15 Dec 2024 03:07:56 -0400 Subject: [PATCH 15/24] add v1 users --- backend/internal/v1/v1_users/routes.go | 0 backend/internal/v1/v1_users/types.go | 0 backend/internal/v1/v1_users/users.go | 0 3 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 backend/internal/v1/v1_users/routes.go create mode 100644 backend/internal/v1/v1_users/types.go create mode 100644 backend/internal/v1/v1_users/users.go diff --git a/backend/internal/v1/v1_users/routes.go b/backend/internal/v1/v1_users/routes.go new file mode 100644 index 0000000..e69de29 diff --git a/backend/internal/v1/v1_users/types.go b/backend/internal/v1/v1_users/types.go new file mode 100644 index 0000000..e69de29 diff --git a/backend/internal/v1/v1_users/users.go b/backend/internal/v1/v1_users/users.go new file mode 100644 index 0000000..e69de29 From 540868093b2b95254b132a44b90b6df893b75306 Mon Sep 17 00:00:00 2001 From: Aidan Traboulay Date: Sun, 15 Dec 2024 03:20:49 -0400 Subject: [PATCH 16/24] refactor main --- .../migrations/20241107232037_companies.sql | 19 - .../20241108045258_create_users_table.sql | 21 - .../20241108185615_resource_requests.sql | 20 - .../20241111230539_company_financials.sql | 26 - .../migrations/20241112025334_employees.sql | 21 - .../20241112030340_company_documents.sql | 18 - .../20241112031057_questions_and_answers.sql | 36 - .../migrations/20241114031507_create_tags.sql | 14 - ...14031508_projects_links_comments_files.sql | 64 -- .../20241114031509_project_questions.sql | 30 - .../20241114060947_funding_transactions.sql | 22 - .../migrations/20241115001354_meetings.sql | 24 - .../20241116191633_user_role_enum.sql | 17 - .../20241116192218_alter_users_role_col.sql | 29 - ...3212121_alter_users_add_email_verified.sql | 15 - ...85206_create_verify_email_tokens_table.sql | 16 - ...20241219000000_add_token_salt_to_users.sql | 19 - .../20244119000000_add_company_metadata.sql | 22 - backend/.sqlc/queries/company.sql | 39 - backend/.sqlc/queries/company_documents.sql | 36 - backend/.sqlc/queries/company_financials.sql | 69 -- .../queries/company_questions_answers.sql | 68 -- backend/.sqlc/queries/employee.sql | 42 -- .../.sqlc/queries/funding_transactions.sql | 38 - backend/.sqlc/queries/meetings.sql | 42 -- backend/.sqlc/queries/projects.sql | 200 ----- backend/.sqlc/queries/resource_requests.sql | 44 -- backend/.sqlc/queries/tags.sql | 19 - backend/.sqlc/queries/users.sql | 29 - backend/.sqlc/queries/verify_email_tokens.sql | 19 - backend/db/company.sql.go | 155 ---- backend/db/company_documents.sql.go | 171 ----- backend/db/company_financials.sql.go | 260 ------- backend/db/company_questions_answers.sql.go | 277 ------- backend/db/employee.sql.go | 213 ------ backend/db/funding_transactions.sql.go | 201 ----- backend/db/meetings.sql.go | 215 ------ backend/db/projects.sql.go | 699 ------------------ backend/db/resource_requests.sql.go | 184 ----- backend/db/tags.sql.go | 88 --- backend/db/users.sql.go | 131 ---- backend/db/verify_email_tokens.sql.go | 74 -- backend/internal/jwt/jwt_test.go | 125 ---- backend/internal/middleware/jwt_test.go | 132 ---- .../internal/middleware/req_validator_test.go | 108 --- backend/internal/server/auth.go | 406 ---------- backend/internal/server/auth_test.go | 315 -------- backend/internal/server/company.go | 113 --- backend/internal/server/company_documents.go | 148 ---- backend/internal/server/company_financials.go | 187 ----- .../server/company_questions_answers.go | 239 ------ backend/internal/server/employee.go | 145 ---- backend/internal/server/error_handler.go | 79 -- backend/internal/server/error_handler_test.go | 94 --- .../internal/server/funding_transactions.go | 162 ---- backend/internal/server/handler_helpers.go | 111 --- backend/internal/server/healthcheck.go | 70 -- backend/internal/server/healthcheck_test.go | 98 --- backend/internal/server/meetings.go | 194 ----- .../internal/server/project_comment_test.go | 261 ------- backend/internal/server/project_tag_test.go | 246 ------ backend/internal/server/projects.go | 652 ---------------- backend/internal/server/resource_request.go | 152 ---- backend/internal/server/startup.go | 50 -- backend/internal/server/static.go | 69 -- backend/internal/server/static_test.go | 69 -- backend/internal/server/storage.go | 102 --- backend/internal/server/tag.go | 74 -- backend/internal/server/tag_test.go | 130 ---- backend/internal/server/test_helpers.go | 15 - 70 files changed, 8292 deletions(-) delete mode 100644 backend/.sqlc/migrations/20241107232037_companies.sql delete mode 100644 backend/.sqlc/migrations/20241108045258_create_users_table.sql delete mode 100644 backend/.sqlc/migrations/20241108185615_resource_requests.sql delete mode 100644 backend/.sqlc/migrations/20241111230539_company_financials.sql delete mode 100644 backend/.sqlc/migrations/20241112025334_employees.sql delete mode 100644 backend/.sqlc/migrations/20241112030340_company_documents.sql delete mode 100644 backend/.sqlc/migrations/20241112031057_questions_and_answers.sql delete mode 100644 backend/.sqlc/migrations/20241114031507_create_tags.sql delete mode 100644 backend/.sqlc/migrations/20241114031508_projects_links_comments_files.sql delete mode 100644 backend/.sqlc/migrations/20241114031509_project_questions.sql delete mode 100644 backend/.sqlc/migrations/20241114060947_funding_transactions.sql delete mode 100644 backend/.sqlc/migrations/20241115001354_meetings.sql delete mode 100644 backend/.sqlc/migrations/20241116191633_user_role_enum.sql delete mode 100644 backend/.sqlc/migrations/20241116192218_alter_users_role_col.sql delete mode 100644 backend/.sqlc/migrations/20241203212121_alter_users_add_email_verified.sql delete mode 100644 backend/.sqlc/migrations/20241205185206_create_verify_email_tokens_table.sql delete mode 100644 backend/.sqlc/migrations/20241219000000_add_token_salt_to_users.sql delete mode 100644 backend/.sqlc/migrations/20244119000000_add_company_metadata.sql delete mode 100644 backend/.sqlc/queries/company.sql delete mode 100644 backend/.sqlc/queries/company_documents.sql delete mode 100644 backend/.sqlc/queries/company_financials.sql delete mode 100644 backend/.sqlc/queries/company_questions_answers.sql delete mode 100644 backend/.sqlc/queries/employee.sql delete mode 100644 backend/.sqlc/queries/funding_transactions.sql delete mode 100644 backend/.sqlc/queries/meetings.sql delete mode 100644 backend/.sqlc/queries/projects.sql delete mode 100644 backend/.sqlc/queries/resource_requests.sql delete mode 100644 backend/.sqlc/queries/tags.sql delete mode 100644 backend/.sqlc/queries/users.sql delete mode 100644 backend/.sqlc/queries/verify_email_tokens.sql delete mode 100644 backend/db/company.sql.go delete mode 100644 backend/db/company_documents.sql.go delete mode 100644 backend/db/company_financials.sql.go delete mode 100644 backend/db/company_questions_answers.sql.go delete mode 100644 backend/db/employee.sql.go delete mode 100644 backend/db/funding_transactions.sql.go delete mode 100644 backend/db/meetings.sql.go delete mode 100644 backend/db/projects.sql.go delete mode 100644 backend/db/resource_requests.sql.go delete mode 100644 backend/db/tags.sql.go delete mode 100644 backend/db/users.sql.go delete mode 100644 backend/db/verify_email_tokens.sql.go delete mode 100644 backend/internal/jwt/jwt_test.go delete mode 100644 backend/internal/middleware/jwt_test.go delete mode 100644 backend/internal/middleware/req_validator_test.go delete mode 100644 backend/internal/server/auth.go delete mode 100644 backend/internal/server/auth_test.go delete mode 100644 backend/internal/server/company.go delete mode 100644 backend/internal/server/company_documents.go delete mode 100644 backend/internal/server/company_financials.go delete mode 100644 backend/internal/server/company_questions_answers.go delete mode 100644 backend/internal/server/employee.go delete mode 100644 backend/internal/server/error_handler.go delete mode 100644 backend/internal/server/error_handler_test.go delete mode 100644 backend/internal/server/funding_transactions.go delete mode 100644 backend/internal/server/handler_helpers.go delete mode 100644 backend/internal/server/healthcheck.go delete mode 100644 backend/internal/server/healthcheck_test.go delete mode 100644 backend/internal/server/meetings.go delete mode 100644 backend/internal/server/project_comment_test.go delete mode 100644 backend/internal/server/project_tag_test.go delete mode 100644 backend/internal/server/projects.go delete mode 100644 backend/internal/server/resource_request.go delete mode 100644 backend/internal/server/startup.go delete mode 100644 backend/internal/server/static.go delete mode 100644 backend/internal/server/static_test.go delete mode 100644 backend/internal/server/storage.go delete mode 100644 backend/internal/server/tag.go delete mode 100644 backend/internal/server/tag_test.go delete mode 100644 backend/internal/server/test_helpers.go diff --git a/backend/.sqlc/migrations/20241107232037_companies.sql b/backend/.sqlc/migrations/20241107232037_companies.sql deleted file mode 100644 index 7d4e187..0000000 --- a/backend/.sqlc/migrations/20241107232037_companies.sql +++ /dev/null @@ -1,19 +0,0 @@ --- +goose Up --- +goose StatementBegin -CREATE TABLE companies ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, - owner_user_id UUID NOT NULL, - name VARCHAR(255) NOT NULL, - description TEXT, - is_verified BOOLEAN NOT NULL DEFAULT false, - created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP -); - -CREATE UNIQUE INDEX idx_startups_owner_name ON companies (owner_user_id, name); --- +goose StatementEnd - --- +goose Down --- +goose StatementBegin -DROP TABLE companies; --- +goose StatementEnd \ No newline at end of file diff --git a/backend/.sqlc/migrations/20241108045258_create_users_table.sql b/backend/.sqlc/migrations/20241108045258_create_users_table.sql deleted file mode 100644 index beb085f..0000000 --- a/backend/.sqlc/migrations/20241108045258_create_users_table.sql +++ /dev/null @@ -1,21 +0,0 @@ --- +goose Up --- +goose StatementBegin -CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; - -CREATE TABLE users ( - id UUID PRIMARY KEY DEFAULT uuid_generate_v4() NOT NULL, - email VARCHAR(255) UNIQUE NOT NULL, - password_hash VARCHAR(255) NOT NULL, - first_name VARCHAR(100), - last_name VARCHAR(100), - role VARCHAR(50) NOT NULL, - wallet_address VARCHAR(100), - created_at TIMESTAMP DEFAULT NOW(), - updated_at TIMESTAMP DEFAULT NOW() -); --- +goose StatementEnd - --- +goose Down --- +goose StatementBegin -DROP TABLE users; --- +goose StatementEnd diff --git a/backend/.sqlc/migrations/20241108185615_resource_requests.sql b/backend/.sqlc/migrations/20241108185615_resource_requests.sql deleted file mode 100644 index 8ab5f70..0000000 --- a/backend/.sqlc/migrations/20241108185615_resource_requests.sql +++ /dev/null @@ -1,20 +0,0 @@ --- +goose Up --- +goose StatementBegin -CREATE TABLE resource_requests ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, - company_id UUID NOT NULL REFERENCES companies(id) ON DELETE CASCADE, - resource_type VARCHAR(100) NOT NULL, - description TEXT, - status VARCHAR(50) NOT NULL DEFAULT 'pending', - created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP -); - -CREATE INDEX idx_resource_requests_company ON resource_requests(company_id); -CREATE INDEX idx_resource_requests_status ON resource_requests(status); --- +goose StatementEnd - --- +goose Down --- +goose StatementBegin -DROP TABLE resource_requests; --- +goose StatementEnd \ No newline at end of file diff --git a/backend/.sqlc/migrations/20241111230539_company_financials.sql b/backend/.sqlc/migrations/20241111230539_company_financials.sql deleted file mode 100644 index ab7eb05..0000000 --- a/backend/.sqlc/migrations/20241111230539_company_financials.sql +++ /dev/null @@ -1,26 +0,0 @@ --- +goose Up --- +goose StatementBegin -CREATE TABLE company_financials ( - id UUID PRIMARY KEY DEFAULT uuid_generate_v4() NOT NULL, - company_id UUID NOT NULL REFERENCES companies(id) ON DELETE CASCADE, - financial_year INTEGER NOT NULL, - revenue NUMERIC(15,2) NOT NULL, - expenses NUMERIC(15,2) NOT NULL, - profit NUMERIC(15,2) NOT NULL, - sales NUMERIC(15,2) NOT NULL, - amount_raised NUMERIC(15,2) NOT NULL, - arr NUMERIC(15,2) NOT NULL, - grants_received NUMERIC(15,2) NOT NULL, - created_at TIMESTAMP DEFAULT NOW(), - updated_at TIMESTAMP DEFAULT NOW(), - UNIQUE(company_id, financial_year) -); - -CREATE INDEX idx_company_financials_company_id ON company_financials(company_id); -CREATE INDEX idx_company_financials_year ON company_financials(financial_year); --- +goose StatementEnd - --- +goose Down --- +goose StatementBegin -DROP TABLE company_financials; --- +goose StatementEnd \ No newline at end of file diff --git a/backend/.sqlc/migrations/20241112025334_employees.sql b/backend/.sqlc/migrations/20241112025334_employees.sql deleted file mode 100644 index 406763f..0000000 --- a/backend/.sqlc/migrations/20241112025334_employees.sql +++ /dev/null @@ -1,21 +0,0 @@ --- +goose Up --- +goose StatementBegin -CREATE TABLE employees ( - id UUID PRIMARY KEY DEFAULT uuid_generate_v4() NOT NULL, - company_id UUID NOT NULL REFERENCES companies(id) ON DELETE CASCADE, - name VARCHAR(255) NOT NULL, - email VARCHAR(255) NOT NULL UNIQUE, - role VARCHAR(100) NOT NULL, - bio TEXT, - created_at TIMESTAMP DEFAULT NOW(), - updated_at TIMESTAMP DEFAULT NOW() -); - -CREATE INDEX idx_employees_company ON employees(company_id); -CREATE INDEX idx_employees_email ON employees(email); --- +goose StatementEnd - --- +goose Down --- +goose StatementBegin -DROP TABLE employees; --- +goose StatementEnd diff --git a/backend/.sqlc/migrations/20241112030340_company_documents.sql b/backend/.sqlc/migrations/20241112030340_company_documents.sql deleted file mode 100644 index d191257..0000000 --- a/backend/.sqlc/migrations/20241112030340_company_documents.sql +++ /dev/null @@ -1,18 +0,0 @@ --- +goose Up --- +goose StatementBegin -CREATE TABLE company_documents ( - id UUID PRIMARY KEY DEFAULT uuid_generate_v4() NOT NULL, - company_id UUID NOT NULL REFERENCES companies(id) ON DELETE CASCADE, - document_type VARCHAR(100) NOT NULL, - file_url TEXT NOT NULL, - created_at TIMESTAMP DEFAULT NOW(), - updated_at TIMESTAMP DEFAULT NOW() -); - -CREATE INDEX idx_company_documents_company ON company_documents(company_id); --- +goose StatementEnd - --- +goose Down --- +goose StatementBegin -DROP TABLE company_documents; --- +goose StatementEnd \ No newline at end of file diff --git a/backend/.sqlc/migrations/20241112031057_questions_and_answers.sql b/backend/.sqlc/migrations/20241112031057_questions_and_answers.sql deleted file mode 100644 index d42f30b..0000000 --- a/backend/.sqlc/migrations/20241112031057_questions_and_answers.sql +++ /dev/null @@ -1,36 +0,0 @@ --- +goose Up --- +goose StatementBegin -CREATE TABLE questions ( - id UUID PRIMARY KEY DEFAULT uuid_generate_v4() NOT NULL, - question_text TEXT NOT NULL, - created_at TIMESTAMP DEFAULT NOW(), - updated_at TIMESTAMP DEFAULT NOW(), - deleted_at TIMESTAMP -); - -CREATE TABLE company_question_answers ( - id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), - company_id UUID NOT NULL REFERENCES companies(id), - question_id UUID NOT NULL REFERENCES questions(id), - answer_text TEXT NOT NULL, - created_at TIMESTAMP DEFAULT NOW(), - updated_at TIMESTAMP DEFAULT NOW(), - deleted_at TIMESTAMP, - UNIQUE(company_id, question_id) -); - -CREATE INDEX idx_company_answers_company ON company_question_answers(company_id); -CREATE INDEX idx_company_answers_question ON company_question_answers(question_id); - -ALTER TABLE companies ADD COLUMN deleted_at TIMESTAMP; --- +goose StatementEnd - --- +goose Down --- +goose StatementBegin -DROP INDEX idx_company_answers_question; -DROP INDEX idx_company_answers_company; -DROP TABLE company_question_answers; -DROP TABLE questions; - -ALTER TABLE companies DROP COLUMN deleted_at; --- +goose StatementEnd \ No newline at end of file diff --git a/backend/.sqlc/migrations/20241114031507_create_tags.sql b/backend/.sqlc/migrations/20241114031507_create_tags.sql deleted file mode 100644 index 1f1495d..0000000 --- a/backend/.sqlc/migrations/20241114031507_create_tags.sql +++ /dev/null @@ -1,14 +0,0 @@ --- +goose Up --- +goose StatementBegin -CREATE TABLE tags ( - id UUID PRIMARY KEY DEFAULT uuid_generate_v4() NOT NULL, - name VARCHAR(100) NOT NULL UNIQUE, - created_at TIMESTAMP DEFAULT NOW(), - updated_at TIMESTAMP DEFAULT NOW() -); --- +goose StatementEnd - --- +goose Down --- +goose StatementBegin -DROP TABLE IF EXISTS tags CASCADE; --- +goose StatementEnd \ No newline at end of file diff --git a/backend/.sqlc/migrations/20241114031508_projects_links_comments_files.sql b/backend/.sqlc/migrations/20241114031508_projects_links_comments_files.sql deleted file mode 100644 index 6d31999..0000000 --- a/backend/.sqlc/migrations/20241114031508_projects_links_comments_files.sql +++ /dev/null @@ -1,64 +0,0 @@ --- +goose Up --- +goose StatementBegin -CREATE TABLE projects ( - id UUID PRIMARY KEY DEFAULT uuid_generate_v4() NOT NULL, - company_id UUID NOT NULL REFERENCES companies(id) ON DELETE CASCADE, - title VARCHAR(255) NOT NULL, - description TEXT, - status VARCHAR(50) NOT NULL, - created_at TIMESTAMP DEFAULT NOW(), - updated_at TIMESTAMP DEFAULT NOW() -); - -CREATE TABLE project_files ( - id UUID PRIMARY KEY DEFAULT uuid_generate_v4() NOT NULL, - project_id UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE, - file_type VARCHAR(100) NOT NULL, - file_url TEXT NOT NULL, - created_at TIMESTAMP DEFAULT NOW(), - updated_at TIMESTAMP DEFAULT NOW() -); - -CREATE TABLE project_comments ( - id UUID PRIMARY KEY DEFAULT uuid_generate_v4() NOT NULL, - project_id UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE, - user_id UUID NOT NULL REFERENCES users(id), - comment TEXT NOT NULL, - created_at TIMESTAMP DEFAULT NOW(), - updated_at TIMESTAMP DEFAULT NOW() -); - -CREATE TABLE project_links ( - id UUID PRIMARY KEY DEFAULT uuid_generate_v4() NOT NULL, - project_id UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE, - link_type VARCHAR(100) NOT NULL, - url TEXT NOT NULL, - created_at TIMESTAMP DEFAULT NOW(), - updated_at TIMESTAMP DEFAULT NOW() -); - -CREATE TABLE project_tags ( - id UUID PRIMARY KEY DEFAULT uuid_generate_v4() NOT NULL, - project_id UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE, - tag_id UUID NOT NULL REFERENCES tags(id), - created_at TIMESTAMP DEFAULT NOW(), - UNIQUE(project_id, tag_id) -); - -CREATE INDEX idx_projects_company ON projects(company_id); -CREATE INDEX idx_project_files_project ON project_files(project_id); -CREATE INDEX idx_project_comments_project ON project_comments(project_id); -CREATE INDEX idx_project_comments_user ON project_comments(user_id); -CREATE INDEX idx_project_links_project ON project_links(project_id); -CREATE INDEX idx_project_tags_project ON project_tags(project_id); -CREATE INDEX idx_project_tags_tag ON project_tags(tag_id); --- +goose StatementEnd - --- +goose Down --- +goose StatementBegin -DROP TABLE IF EXISTS project_links CASCADE; -DROP TABLE IF EXISTS project_comments CASCADE; -DROP TABLE IF EXISTS project_files CASCADE; -DROP TABLE IF EXISTS project_tags CASCADE; -DROP TABLE IF EXISTS projects CASCADE; --- +goose StatementEnd \ No newline at end of file diff --git a/backend/.sqlc/migrations/20241114031509_project_questions.sql b/backend/.sqlc/migrations/20241114031509_project_questions.sql deleted file mode 100644 index 1a61c40..0000000 --- a/backend/.sqlc/migrations/20241114031509_project_questions.sql +++ /dev/null @@ -1,30 +0,0 @@ --- +goose Up --- +goose StatementBegin -CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; - -CREATE TABLE project_sections ( - id UUID PRIMARY KEY DEFAULT uuid_generate_v4() NOT NULL, - project_id UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE, - title VARCHAR(255) NOT NULL, - created_at TIMESTAMP DEFAULT NOW(), - updated_at TIMESTAMP DEFAULT NOW() -); - -CREATE TABLE project_questions ( - id UUID PRIMARY KEY DEFAULT uuid_generate_v4() NOT NULL, - section_id UUID NOT NULL REFERENCES project_sections(id) ON DELETE CASCADE, - question_text TEXT NOT NULL, - answer_text TEXT NOT NULL, - created_at TIMESTAMP DEFAULT NOW(), - updated_at TIMESTAMP DEFAULT NOW() -); - -CREATE INDEX idx_project_sections_project ON project_sections(project_id); -CREATE INDEX idx_project_questions_section ON project_questions(section_id); --- +goose StatementEnd - --- +goose Down --- +goose StatementBegin -DROP TABLE project_questions; -DROP TABLE project_sections; --- +goose StatementEnd \ No newline at end of file diff --git a/backend/.sqlc/migrations/20241114060947_funding_transactions.sql b/backend/.sqlc/migrations/20241114060947_funding_transactions.sql deleted file mode 100644 index 66f18b3..0000000 --- a/backend/.sqlc/migrations/20241114060947_funding_transactions.sql +++ /dev/null @@ -1,22 +0,0 @@ --- +goose Up --- +goose StatementBegin -CREATE TABLE funding_transactions ( - id UUID PRIMARY KEY DEFAULT uuid_generate_v4() NOT NULL, - project_id UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE, - amount NUMERIC(15,2) NOT NULL, - currency VARCHAR(10) NOT NULL, - transaction_hash VARCHAR(255) NOT NULL, - from_wallet_address VARCHAR(255) NOT NULL, - to_wallet_address VARCHAR(255) NOT NULL, - status VARCHAR(50) NOT NULL, - created_at TIMESTAMP DEFAULT NOW(), - updated_at TIMESTAMP DEFAULT NOW() -); - -CREATE INDEX idx_funding_transactions_project ON funding_transactions(project_id); --- +goose StatementEnd - --- +goose Down --- +goose StatementBegin -DROP TABLE IF EXISTS funding_transactions; --- +goose StatementEnd \ No newline at end of file diff --git a/backend/.sqlc/migrations/20241115001354_meetings.sql b/backend/.sqlc/migrations/20241115001354_meetings.sql deleted file mode 100644 index e7e6ecc..0000000 --- a/backend/.sqlc/migrations/20241115001354_meetings.sql +++ /dev/null @@ -1,24 +0,0 @@ --- +goose Up --- +goose StatementBegin -CREATE TABLE meetings ( - id UUID PRIMARY KEY DEFAULT uuid_generate_v4() NOT NULL, - project_id UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE, - scheduled_by_user_id UUID NOT NULL REFERENCES users(id), - start_time TIMESTAMP NOT NULL, - end_time TIMESTAMP NOT NULL, - meeting_url TEXT, - location TEXT, - notes TEXT, - created_at TIMESTAMP DEFAULT NOW(), - updated_at TIMESTAMP DEFAULT NOW() -); - -CREATE INDEX idx_meetings_project ON meetings(project_id); -CREATE INDEX idx_meetings_scheduler ON meetings(scheduled_by_user_id); -CREATE INDEX idx_meetings_start_time ON meetings(start_time); --- +goose StatementEnd - --- +goose Down --- +goose StatementBegin -DROP TABLE IF EXISTS meetings CASCADE; --- +goose StatementEnd \ No newline at end of file diff --git a/backend/.sqlc/migrations/20241116191633_user_role_enum.sql b/backend/.sqlc/migrations/20241116191633_user_role_enum.sql deleted file mode 100644 index 5bd3ddb..0000000 --- a/backend/.sqlc/migrations/20241116191633_user_role_enum.sql +++ /dev/null @@ -1,17 +0,0 @@ --- +goose Up --- +goose StatementBegin -BEGIN; - -CREATE TYPE user_role AS ENUM ('admin', 'startup_owner', 'investor'); - -COMMIT; --- +goose StatementEnd - --- +goose Down --- +goose StatementBegin -BEGIN; - -DROP TYPE user_role; - -COMMIT; --- +goose StatementEnd diff --git a/backend/.sqlc/migrations/20241116192218_alter_users_role_col.sql b/backend/.sqlc/migrations/20241116192218_alter_users_role_col.sql deleted file mode 100644 index 7934b05..0000000 --- a/backend/.sqlc/migrations/20241116192218_alter_users_role_col.sql +++ /dev/null @@ -1,29 +0,0 @@ --- +goose Up --- +goose StatementBegin -BEGIN; - -ALTER TABLE users ADD COLUMN role_enum user_role NOT NULL; - -UPDATE users -SET role_enum = role::user_role; - -ALTER TABLE users DROP COLUMN role; -ALTER TABLE users RENAME COLUMN role_enum TO role; - -COMMIT; --- +goose StatementEnd - --- +goose Down --- +goose StatementBegin -BEGIN; - -ALTER TABLE users ADD COLUMN role_varchar VARCHAR(50) NOT NULL; - -UPDATE users -SET role_varchar = role::text; - -ALTER TABLE users DROP COLUMN role; -ALTER TABLE users RENAME COLUMN role_varchar TO role; - -COMMIT; --- +goose StatementEnd diff --git a/backend/.sqlc/migrations/20241203212121_alter_users_add_email_verified.sql b/backend/.sqlc/migrations/20241203212121_alter_users_add_email_verified.sql deleted file mode 100644 index 1eb6916..0000000 --- a/backend/.sqlc/migrations/20241203212121_alter_users_add_email_verified.sql +++ /dev/null @@ -1,15 +0,0 @@ --- +goose Up --- +goose StatementBegin - --- by default, goose runs in transactions - -ALTER TABLE users ADD COLUMN email_verified BOOLEAN NOT NULL DEFAULT false; - --- +goose StatementEnd - --- +goose Down --- +goose StatementBegin - -ALTER TABLE users DROP COLUMN email_verified; - --- +goose StatementEnd diff --git a/backend/.sqlc/migrations/20241205185206_create_verify_email_tokens_table.sql b/backend/.sqlc/migrations/20241205185206_create_verify_email_tokens_table.sql deleted file mode 100644 index 39982a3..0000000 --- a/backend/.sqlc/migrations/20241205185206_create_verify_email_tokens_table.sql +++ /dev/null @@ -1,16 +0,0 @@ --- +goose Up --- +goose StatementBegin -CREATE TABLE IF NOT EXISTS verify_email_tokens ( - -- id column will be used in the standard jwt id claims. - -- which can be used to identify/select the record in the db. - id UUID PRIMARY KEY DEFAULT uuid_generate_v4() NOT NULL, - email VARCHAR(255) UNIQUE NOT NULL, - expires_at TIMESTAMP NOT NULL, - created_at TIMESTAMP DEFAULT NOW() -); --- +goose StatementEnd - --- +goose Down --- +goose StatementBegin -DROP TABLE IF EXISTS verify_email_tokens; --- +goose StatementEnd diff --git a/backend/.sqlc/migrations/20241219000000_add_token_salt_to_users.sql b/backend/.sqlc/migrations/20241219000000_add_token_salt_to_users.sql deleted file mode 100644 index 012f7e2..0000000 --- a/backend/.sqlc/migrations/20241219000000_add_token_salt_to_users.sql +++ /dev/null @@ -1,19 +0,0 @@ --- +goose Up --- +goose StatementBegin -CREATE EXTENSION IF NOT EXISTS pgcrypto; - -ALTER TABLE users ADD COLUMN token_salt BYTEA; - --- backfill existing users with random salt -UPDATE users SET token_salt = gen_random_bytes(32); - --- make token_salt non-nullable and unique -ALTER TABLE users ALTER COLUMN token_salt SET NOT NULL; -ALTER TABLE users ADD CONSTRAINT users_token_salt_key UNIQUE (token_salt); --- +goose StatementEnd - --- +goose Down --- +goose StatementBegin -ALTER TABLE users DROP COLUMN token_salt; - --- +goose StatementEnd \ No newline at end of file diff --git a/backend/.sqlc/migrations/20244119000000_add_company_metadata.sql b/backend/.sqlc/migrations/20244119000000_add_company_metadata.sql deleted file mode 100644 index 0bf9b00..0000000 --- a/backend/.sqlc/migrations/20244119000000_add_company_metadata.sql +++ /dev/null @@ -1,22 +0,0 @@ --- +goose Up --- +goose StatementBegin -ALTER TABLE companies -ADD COLUMN industry VARCHAR(100), -ADD COLUMN company_stage VARCHAR(50), -ADD COLUMN founded_date DATE; - --- Create indexes for common queries -CREATE INDEX idx_companies_industry ON companies(industry); -CREATE INDEX idx_companies_company_stage ON companies(company_stage); --- +goose StatementEnd - --- +goose Down --- +goose StatementBegin -DROP INDEX idx_companies_company_stage; -DROP INDEX idx_companies_industry; - -ALTER TABLE companies -DROP COLUMN industry, -DROP COLUMN company_stage, -DROP COLUMN founded_date; --- +goose StatementEnd \ No newline at end of file diff --git a/backend/.sqlc/queries/company.sql b/backend/.sqlc/queries/company.sql deleted file mode 100644 index 531176c..0000000 --- a/backend/.sqlc/queries/company.sql +++ /dev/null @@ -1,39 +0,0 @@ --- name: CreateCompany :one -INSERT INTO companies ( - id, - owner_user_id, - name, - description, - is_verified, - created_at, - updated_at -) VALUES ( - gen_random_uuid(), - $1, - $2, - $3, - false, - CURRENT_TIMESTAMP, - CURRENT_TIMESTAMP -) -RETURNING *; - --- name: GetCompanyByID :one -SELECT * -FROM companies -WHERE id = $1 LIMIT 1; - --- name: GetCompanyByUser :one -SELECT * -FROM companies -WHERE owner_user_id = $1 LIMIT 1; - --- name: ListCompanies :many -SELECT * -FROM companies -ORDER BY updated_at DESC; - --- name: DeleteCompany :exec --- TODO: Add + use auth to ensure only company owners can delete -DELETE FROM companies -WHERE id = $1; diff --git a/backend/.sqlc/queries/company_documents.sql b/backend/.sqlc/queries/company_documents.sql deleted file mode 100644 index 6f99767..0000000 --- a/backend/.sqlc/queries/company_documents.sql +++ /dev/null @@ -1,36 +0,0 @@ --- name: CreateCompanyDocument :one -INSERT INTO company_documents ( - company_id, - document_type, - file_url -) VALUES ( - $1, $2, $3 -) -RETURNING *; - --- name: GetCompanyDocumentByID :one -SELECT * FROM company_documents -WHERE id = $1 LIMIT 1; - --- name: ListCompanyDocuments :many -SELECT * FROM company_documents -WHERE company_id = $1 -ORDER BY created_at DESC; - --- name: ListDocumentsByType :many -SELECT * FROM company_documents -WHERE company_id = $1 AND document_type = $2 -ORDER BY created_at DESC; - --- name: UpdateCompanyDocument :one -UPDATE company_documents -SET - document_type = $2, - file_url = $3, - updated_at = NOW() -WHERE id = $1 -RETURNING *; - --- name: DeleteCompanyDocument :exec -DELETE FROM company_documents -WHERE id = $1; \ No newline at end of file diff --git a/backend/.sqlc/queries/company_financials.sql b/backend/.sqlc/queries/company_financials.sql deleted file mode 100644 index 39943f6..0000000 --- a/backend/.sqlc/queries/company_financials.sql +++ /dev/null @@ -1,69 +0,0 @@ --- name: CreateCompanyFinancials :one -INSERT INTO company_financials ( - id, - company_id, - financial_year, - revenue, - expenses, - profit, - sales, - amount_raised, - arr, - grants_received, - created_at, - updated_at -) VALUES ( - gen_random_uuid(), - $1, - $2, - $3, - $4, - $5, - $6, - $7, - $8, - $9, - CURRENT_TIMESTAMP, - CURRENT_TIMESTAMP -) -RETURNING *; - --- name: GetCompanyFinancialsByYear :one -SELECT * -FROM company_financials -WHERE company_id = $1 -AND financial_year = $2 -LIMIT 1; - --- name: ListCompanyFinancials :many -SELECT * -FROM company_financials -WHERE company_id = $1 -ORDER BY financial_year DESC; - --- name: UpdateCompanyFinancials :one -UPDATE company_financials -SET - revenue = $3, - expenses = $4, - profit = $5, - sales = $6, - amount_raised = $7, - arr = $8, - grants_received = $9, - updated_at = CURRENT_TIMESTAMP -WHERE company_id = $1 -AND financial_year = $2 -RETURNING *; - --- name: DeleteCompanyFinancials :exec -DELETE FROM company_financials -WHERE company_id = $1 -AND financial_year = $2; - --- name: GetLatestCompanyFinancials :one -SELECT * -FROM company_financials -WHERE company_id = $1 -ORDER BY financial_year DESC -LIMIT 1; \ No newline at end of file diff --git a/backend/.sqlc/queries/company_questions_answers.sql b/backend/.sqlc/queries/company_questions_answers.sql deleted file mode 100644 index 225aa87..0000000 --- a/backend/.sqlc/queries/company_questions_answers.sql +++ /dev/null @@ -1,68 +0,0 @@ --- name: CreateQuestion :one -INSERT INTO questions ( - question_text -) VALUES ( - $1 -) -RETURNING *; - --- name: GetQuestion :one -SELECT * FROM questions -WHERE id = $1 AND deleted_at IS NULL -LIMIT 1; - --- name: ListQuestions :many -SELECT * FROM questions -WHERE deleted_at IS NULL -ORDER BY created_at DESC; - --- name: SoftDeleteQuestion :exec -UPDATE questions -SET deleted_at = NOW() -WHERE id = $1; - --- name: CreateCompanyAnswer :one -INSERT INTO company_question_answers ( - company_id, - question_id, - answer_text -) VALUES ( - $1, $2, $3 -) -RETURNING *; - --- name: GetCompanyAnswer :one -SELECT - cqa.*, - q.question_text -FROM company_question_answers cqa -JOIN questions q ON q.id = cqa.question_id -WHERE cqa.company_id = $1 AND cqa.question_id = $2 AND cqa.deleted_at IS NULL AND q.deleted_at IS NULL -LIMIT 1; - --- name: ListCompanyAnswers :many -SELECT - cqa.*, - q.question_text -FROM company_question_answers cqa -JOIN questions q ON q.id = cqa.question_id -WHERE cqa.company_id = $1 AND cqa.deleted_at IS NULL AND q.deleted_at IS NULL -ORDER BY cqa.created_at DESC; - --- name: UpdateCompanyAnswer :one -UPDATE company_question_answers -SET - answer_text = $3, - updated_at = NOW() -WHERE company_id = $1 AND question_id = $2 AND deleted_at IS NULL -RETURNING *; - --- name: SoftDeleteCompanyAnswer :exec -UPDATE company_question_answers -SET deleted_at = NOW() -WHERE company_id = $1 AND question_id = $2; - --- name: DeleteQuestion :exec -UPDATE questions -SET deleted_at = NOW() -WHERE id = $1 AND deleted_at IS NULL; \ No newline at end of file diff --git a/backend/.sqlc/queries/employee.sql b/backend/.sqlc/queries/employee.sql deleted file mode 100644 index 1a77b46..0000000 --- a/backend/.sqlc/queries/employee.sql +++ /dev/null @@ -1,42 +0,0 @@ --- name: CreateEmployee :one -INSERT INTO employees ( - company_id, - name, - email, - role, - bio -) VALUES ( - $1, $2, $3, $4, $5 -) -RETURNING *; - --- name: GetEmployeeByID :one -SELECT * FROM employees -WHERE id = $1 LIMIT 1; - --- name: GetEmployeeByEmail :one -SELECT * FROM employees -WHERE email = $1 LIMIT 1; - --- name: ListEmployees :many -SELECT * FROM employees -ORDER BY created_at DESC; - --- name: ListEmployeesByCompany :many -SELECT * FROM employees -WHERE company_id = $1 -ORDER BY created_at DESC; - --- name: UpdateEmployee :one -UPDATE employees -SET - name = $2, - role = $3, - bio = $4, - updated_at = NOW() -WHERE id = $1 -RETURNING *; - --- name: DeleteEmployee :exec -DELETE FROM employees -WHERE id = $1; \ No newline at end of file diff --git a/backend/.sqlc/queries/funding_transactions.sql b/backend/.sqlc/queries/funding_transactions.sql deleted file mode 100644 index 712a2b5..0000000 --- a/backend/.sqlc/queries/funding_transactions.sql +++ /dev/null @@ -1,38 +0,0 @@ --- name: CreateFundingTransaction :one -INSERT INTO funding_transactions ( - project_id, - amount, - currency, - transaction_hash, - from_wallet_address, - to_wallet_address, - status -) VALUES ( - $1, $2, $3, $4, $5, $6, $7 -) -RETURNING *; - --- name: GetFundingTransaction :one -SELECT * FROM funding_transactions -WHERE id = $1 LIMIT 1; - --- name: ListFundingTransactions :many -SELECT * FROM funding_transactions -ORDER BY created_at DESC; - --- name: ListProjectFundingTransactions :many -SELECT * FROM funding_transactions -WHERE project_id = $1 -ORDER BY created_at DESC; - --- name: UpdateFundingTransactionStatus :one -UPDATE funding_transactions -SET - status = $2, - updated_at = NOW() -WHERE id = $1 -RETURNING *; - --- name: DeleteFundingTransaction :exec -DELETE FROM funding_transactions -WHERE id = $1; \ No newline at end of file diff --git a/backend/.sqlc/queries/meetings.sql b/backend/.sqlc/queries/meetings.sql deleted file mode 100644 index cf5d91e..0000000 --- a/backend/.sqlc/queries/meetings.sql +++ /dev/null @@ -1,42 +0,0 @@ --- name: CreateMeeting :one -INSERT INTO meetings ( - project_id, - scheduled_by_user_id, - start_time, - end_time, - meeting_url, - location, - notes -) VALUES ( - $1, $2, $3, $4, $5, $6, $7 -) -RETURNING *; - --- name: GetMeeting :one -SELECT * FROM meetings -WHERE id = $1 LIMIT 1; - --- name: ListMeetings :many -SELECT * FROM meetings -ORDER BY start_time DESC; - --- name: ListProjectMeetings :many -SELECT * FROM meetings -WHERE project_id = $1 -ORDER BY start_time DESC; - --- name: UpdateMeeting :one -UPDATE meetings -SET - start_time = $2, - end_time = $3, - meeting_url = $4, - location = $5, - notes = $6, - updated_at = NOW() -WHERE id = $1 -RETURNING *; - --- name: DeleteMeeting :exec -DELETE FROM meetings -WHERE id = $1; \ No newline at end of file diff --git a/backend/.sqlc/queries/projects.sql b/backend/.sqlc/queries/projects.sql deleted file mode 100644 index 2e8c92d..0000000 --- a/backend/.sqlc/queries/projects.sql +++ /dev/null @@ -1,200 +0,0 @@ --- name: CreateProject :one -INSERT INTO projects ( - company_id, - title, - description, - status -) VALUES ( - $1, $2, $3, $4 -) -RETURNING *; - --- name: GetProject :one -SELECT * FROM projects -WHERE id = $1 LIMIT 1; - --- name: ListProjects :many -SELECT - p.*, - c.name as company_name, - c.industry as company_industry, - c.founded_date as company_founded_date, - c.company_stage as company_stage -FROM projects p -LEFT JOIN companies c ON p.company_id = c.id -ORDER BY p.created_at DESC; - --- name: ListProjectsByCompany :many -SELECT * FROM projects -WHERE company_id = $1 -ORDER BY created_at DESC; - --- name: UpdateProject :one -UPDATE projects -SET - title = $2, - description = $3, - status = $4, - updated_at = NOW() -WHERE id = $1 -RETURNING *; - --- name: DeleteProject :exec -DELETE FROM projects -WHERE id = $1; - --- name: CreateProjectFile :one -INSERT INTO project_files ( - project_id, - file_type, - file_url -) VALUES ( - $1, $2, $3 -) -RETURNING *; - --- name: ListProjectFiles :many -SELECT * FROM project_files -WHERE project_id = $1 -ORDER BY created_at DESC; - --- name: DeleteProjectFile :exec -DELETE FROM project_files -WHERE id = $1; - --- name: CreateProjectComment :one -INSERT INTO project_comments ( - project_id, - user_id, - comment -) VALUES ( - $1, $2, $3 -) -RETURNING *; - --- name: GetProjectComments :many -SELECT - pc.*, - u.first_name, - u.last_name, - u.email -FROM project_comments pc -JOIN users u ON u.id = pc.user_id -WHERE pc.project_id = $1 -ORDER BY pc.created_at DESC; - --- name: DeleteProjectComment :exec -DELETE FROM project_comments -WHERE id = $1; - --- name: CreateProjectLink :one -INSERT INTO project_links ( - project_id, - link_type, - url -) VALUES ( - $1, $2, $3 -) -RETURNING *; - --- name: ListProjectLinks :many -SELECT * FROM project_links -WHERE project_id = $1 -ORDER BY created_at DESC; - --- name: DeleteProjectLink :exec -DELETE FROM project_links -WHERE id = $1; - --- name: AddProjectTag :one -INSERT INTO project_tags ( - project_id, - tag_id -) VALUES ( - $1, $2 -) -RETURNING *; - --- name: ListProjectTags :many -SELECT - pt.*, - t.name as tag_name -FROM project_tags pt -JOIN tags t ON t.id = pt.tag_id -WHERE pt.project_id = $1 -ORDER BY t.name; - --- name: DeleteProjectTag :exec -DELETE FROM project_tags -WHERE project_id = $1 AND tag_id = $2; - --- name: DeleteAllProjectTags :exec -DELETE FROM project_tags -WHERE project_id = $1; - --- name: ListProjectWithDetails :one -SELECT - p.*, - c.name as company_name, - c.industry as company_industry, - c.founded_date as company_founded_date, - c.company_stage as company_stage, - COALESCE( - json_agg( - DISTINCT jsonb_build_object( - 'id', ps.id, - 'title', ps.title, - 'questions', ( - SELECT COALESCE( - json_agg( - jsonb_build_object( - 'question', pq.question_text, - 'answer', pq.answer_text - ) - ), - '[]'::json - ) - FROM project_questions pq - WHERE pq.section_id = ps.id - ) - ) - FILTER (WHERE ps.id IS NOT NULL) - ), - '[]'::json - ) as sections, - COALESCE( - json_agg( - DISTINCT jsonb_build_object( - 'id', pf.id, - 'name', pf.file_type, - 'url', pf.file_url - ) - FILTER (WHERE pf.id IS NOT NULL) - ), - '[]'::json - ) as documents -FROM projects p -LEFT JOIN companies c ON p.company_id = c.id -LEFT JOIN project_sections ps ON ps.project_id = p.id -LEFT JOIN project_files pf ON pf.project_id = p.id -WHERE p.id = $1 -GROUP BY p.id, c.id; - --- name: CreateProjectSection :one -INSERT INTO project_sections ( - project_id, - title -) VALUES ( - $1, $2 -) -RETURNING *; - --- name: CreateProjectQuestion :one -INSERT INTO project_questions ( - section_id, - question_text, - answer_text -) VALUES ( - $1, $2, $3 -) -RETURNING *; \ No newline at end of file diff --git a/backend/.sqlc/queries/resource_requests.sql b/backend/.sqlc/queries/resource_requests.sql deleted file mode 100644 index c798743..0000000 --- a/backend/.sqlc/queries/resource_requests.sql +++ /dev/null @@ -1,44 +0,0 @@ --- name: CreateResourceRequest :one -INSERT INTO resource_requests ( - id, - company_id, - resource_type, - description, - status, - created_at, - updated_at -) VALUES ( - gen_random_uuid(), - $1, - $2, - $3, - $4, - CURRENT_TIMESTAMP, - CURRENT_TIMESTAMP -) -RETURNING *; - --- name: GetResourceRequestByID :one -SELECT * FROM resource_requests -WHERE id = $1 LIMIT 1; - --- name: ListResourceRequests :many -SELECT * FROM resource_requests -ORDER BY updated_at DESC; - --- name: ListResourceRequestsByCompany :many -SELECT * FROM resource_requests -WHERE company_id = $1 -ORDER BY updated_at DESC; - --- name: DeleteResourceRequest :exec -DELETE FROM resource_requests -WHERE id = $1; - --- name: UpdateResourceRequestStatus :one -UPDATE resource_requests -SET - status = $2, - updated_at = CURRENT_TIMESTAMP -WHERE id = $1 -RETURNING *; \ No newline at end of file diff --git a/backend/.sqlc/queries/tags.sql b/backend/.sqlc/queries/tags.sql deleted file mode 100644 index 1aa330e..0000000 --- a/backend/.sqlc/queries/tags.sql +++ /dev/null @@ -1,19 +0,0 @@ --- name: CreateTag :one -INSERT INTO tags ( - name -) VALUES ( - $1 -) -RETURNING *; - --- name: GetTag :one -SELECT * FROM tags -WHERE id = $1 LIMIT 1; - --- name: ListTags :many -SELECT * FROM tags -ORDER BY name; - --- name: DeleteTag :exec -DELETE FROM tags -WHERE id = $1; \ No newline at end of file diff --git a/backend/.sqlc/queries/users.sql b/backend/.sqlc/queries/users.sql deleted file mode 100644 index 329c199..0000000 --- a/backend/.sqlc/queries/users.sql +++ /dev/null @@ -1,29 +0,0 @@ --- name: CreateUser :one -INSERT INTO users ( - email, - password_hash, - role, - token_salt -) VALUES ( - $1, $2, $3, gen_random_bytes(32) -) RETURNING *; - --- name: GetUserByEmail :one -SELECT * FROM users -WHERE email = $1 LIMIT 1; - --- name: GetUserByID :one -SELECT * FROM users -WHERE id = $1 LIMIT 1; - --- name: UpdateUserEmailVerifiedStatus :exec -UPDATE users SET email_verified = $1 -WHERE id = $2; - --- name: UpdateUserTokenSalt :exec -UPDATE users SET token_salt = gen_random_bytes(32) -WHERE id = $1; - --- name: GetUserTokenSalt :one -SELECT token_salt FROM users -WHERE id = $1; diff --git a/backend/.sqlc/queries/verify_email_tokens.sql b/backend/.sqlc/queries/verify_email_tokens.sql deleted file mode 100644 index ce1e54d..0000000 --- a/backend/.sqlc/queries/verify_email_tokens.sql +++ /dev/null @@ -1,19 +0,0 @@ --- name: CreateVerifyEmailToken :one -INSERT INTO verify_email_tokens ( - email, - expires_at -) VALUES ( - $1, $2 -) RETURNING *; - --- name: GetVerifyEmailTokenByID :one -SELECT * FROM verify_email_tokens -WHERE id = $1 LIMIT 1; - --- name: DeleteVerifyEmailTokenByID :exec -DELETE FROM verify_email_tokens -WHERE id = $1; - --- name: DeleteVerifyEmailTokenByEmail :exec -DELETE FROM verify_email_tokens -WHERE email = $1; diff --git a/backend/db/company.sql.go b/backend/db/company.sql.go deleted file mode 100644 index 455ae1b..0000000 --- a/backend/db/company.sql.go +++ /dev/null @@ -1,155 +0,0 @@ -// Code generated by sqlc. DO NOT EDIT. -// versions: -// sqlc v1.27.0 -// source: company.sql - -package db - -import ( - "context" -) - -const createCompany = `-- name: CreateCompany :one -INSERT INTO companies ( - id, - owner_user_id, - name, - description, - is_verified, - created_at, - updated_at -) VALUES ( - gen_random_uuid(), - $1, - $2, - $3, - false, - CURRENT_TIMESTAMP, - CURRENT_TIMESTAMP -) -RETURNING id, owner_user_id, name, description, is_verified, created_at, updated_at, deleted_at, industry, company_stage, founded_date -` - -type CreateCompanyParams struct { - OwnerUserID string - Name string - Description *string -} - -func (q *Queries) CreateCompany(ctx context.Context, arg CreateCompanyParams) (Company, error) { - row := q.db.QueryRow(ctx, createCompany, arg.OwnerUserID, arg.Name, arg.Description) - var i Company - err := row.Scan( - &i.ID, - &i.OwnerUserID, - &i.Name, - &i.Description, - &i.IsVerified, - &i.CreatedAt, - &i.UpdatedAt, - &i.DeletedAt, - &i.Industry, - &i.CompanyStage, - &i.FoundedDate, - ) - return i, err -} - -const deleteCompany = `-- name: DeleteCompany :exec -DELETE FROM companies -WHERE id = $1 -` - -// TODO: Add + use auth to ensure only company owners can delete -func (q *Queries) DeleteCompany(ctx context.Context, id string) error { - _, err := q.db.Exec(ctx, deleteCompany, id) - return err -} - -const getCompanyByID = `-- name: GetCompanyByID :one -SELECT id, owner_user_id, name, description, is_verified, created_at, updated_at, deleted_at, industry, company_stage, founded_date -FROM companies -WHERE id = $1 LIMIT 1 -` - -func (q *Queries) GetCompanyByID(ctx context.Context, id string) (Company, error) { - row := q.db.QueryRow(ctx, getCompanyByID, id) - var i Company - err := row.Scan( - &i.ID, - &i.OwnerUserID, - &i.Name, - &i.Description, - &i.IsVerified, - &i.CreatedAt, - &i.UpdatedAt, - &i.DeletedAt, - &i.Industry, - &i.CompanyStage, - &i.FoundedDate, - ) - return i, err -} - -const getCompanyByUser = `-- name: GetCompanyByUser :one -SELECT id, owner_user_id, name, description, is_verified, created_at, updated_at, deleted_at, industry, company_stage, founded_date -FROM companies -WHERE owner_user_id = $1 LIMIT 1 -` - -func (q *Queries) GetCompanyByUser(ctx context.Context, ownerUserID string) (Company, error) { - row := q.db.QueryRow(ctx, getCompanyByUser, ownerUserID) - var i Company - err := row.Scan( - &i.ID, - &i.OwnerUserID, - &i.Name, - &i.Description, - &i.IsVerified, - &i.CreatedAt, - &i.UpdatedAt, - &i.DeletedAt, - &i.Industry, - &i.CompanyStage, - &i.FoundedDate, - ) - return i, err -} - -const listCompanies = `-- name: ListCompanies :many -SELECT id, owner_user_id, name, description, is_verified, created_at, updated_at, deleted_at, industry, company_stage, founded_date -FROM companies -ORDER BY updated_at DESC -` - -func (q *Queries) ListCompanies(ctx context.Context) ([]Company, error) { - rows, err := q.db.Query(ctx, listCompanies) - if err != nil { - return nil, err - } - defer rows.Close() - var items []Company - for rows.Next() { - var i Company - if err := rows.Scan( - &i.ID, - &i.OwnerUserID, - &i.Name, - &i.Description, - &i.IsVerified, - &i.CreatedAt, - &i.UpdatedAt, - &i.DeletedAt, - &i.Industry, - &i.CompanyStage, - &i.FoundedDate, - ); err != nil { - return nil, err - } - items = append(items, i) - } - if err := rows.Err(); err != nil { - return nil, err - } - return items, nil -} diff --git a/backend/db/company_documents.sql.go b/backend/db/company_documents.sql.go deleted file mode 100644 index c3eb816..0000000 --- a/backend/db/company_documents.sql.go +++ /dev/null @@ -1,171 +0,0 @@ -// Code generated by sqlc. DO NOT EDIT. -// versions: -// sqlc v1.27.0 -// source: company_documents.sql - -package db - -import ( - "context" -) - -const createCompanyDocument = `-- name: CreateCompanyDocument :one -INSERT INTO company_documents ( - company_id, - document_type, - file_url -) VALUES ( - $1, $2, $3 -) -RETURNING id, company_id, document_type, file_url, created_at, updated_at -` - -type CreateCompanyDocumentParams struct { - CompanyID string - DocumentType string - FileUrl string -} - -func (q *Queries) CreateCompanyDocument(ctx context.Context, arg CreateCompanyDocumentParams) (CompanyDocument, error) { - row := q.db.QueryRow(ctx, createCompanyDocument, arg.CompanyID, arg.DocumentType, arg.FileUrl) - var i CompanyDocument - err := row.Scan( - &i.ID, - &i.CompanyID, - &i.DocumentType, - &i.FileUrl, - &i.CreatedAt, - &i.UpdatedAt, - ) - return i, err -} - -const deleteCompanyDocument = `-- name: DeleteCompanyDocument :exec -DELETE FROM company_documents -WHERE id = $1 -` - -func (q *Queries) DeleteCompanyDocument(ctx context.Context, id string) error { - _, err := q.db.Exec(ctx, deleteCompanyDocument, id) - return err -} - -const getCompanyDocumentByID = `-- name: GetCompanyDocumentByID :one -SELECT id, company_id, document_type, file_url, created_at, updated_at FROM company_documents -WHERE id = $1 LIMIT 1 -` - -func (q *Queries) GetCompanyDocumentByID(ctx context.Context, id string) (CompanyDocument, error) { - row := q.db.QueryRow(ctx, getCompanyDocumentByID, id) - var i CompanyDocument - err := row.Scan( - &i.ID, - &i.CompanyID, - &i.DocumentType, - &i.FileUrl, - &i.CreatedAt, - &i.UpdatedAt, - ) - return i, err -} - -const listCompanyDocuments = `-- name: ListCompanyDocuments :many -SELECT id, company_id, document_type, file_url, created_at, updated_at FROM company_documents -WHERE company_id = $1 -ORDER BY created_at DESC -` - -func (q *Queries) ListCompanyDocuments(ctx context.Context, companyID string) ([]CompanyDocument, error) { - rows, err := q.db.Query(ctx, listCompanyDocuments, companyID) - if err != nil { - return nil, err - } - defer rows.Close() - var items []CompanyDocument - for rows.Next() { - var i CompanyDocument - if err := rows.Scan( - &i.ID, - &i.CompanyID, - &i.DocumentType, - &i.FileUrl, - &i.CreatedAt, - &i.UpdatedAt, - ); err != nil { - return nil, err - } - items = append(items, i) - } - if err := rows.Err(); err != nil { - return nil, err - } - return items, nil -} - -const listDocumentsByType = `-- name: ListDocumentsByType :many -SELECT id, company_id, document_type, file_url, created_at, updated_at FROM company_documents -WHERE company_id = $1 AND document_type = $2 -ORDER BY created_at DESC -` - -type ListDocumentsByTypeParams struct { - CompanyID string - DocumentType string -} - -func (q *Queries) ListDocumentsByType(ctx context.Context, arg ListDocumentsByTypeParams) ([]CompanyDocument, error) { - rows, err := q.db.Query(ctx, listDocumentsByType, arg.CompanyID, arg.DocumentType) - if err != nil { - return nil, err - } - defer rows.Close() - var items []CompanyDocument - for rows.Next() { - var i CompanyDocument - if err := rows.Scan( - &i.ID, - &i.CompanyID, - &i.DocumentType, - &i.FileUrl, - &i.CreatedAt, - &i.UpdatedAt, - ); err != nil { - return nil, err - } - items = append(items, i) - } - if err := rows.Err(); err != nil { - return nil, err - } - return items, nil -} - -const updateCompanyDocument = `-- name: UpdateCompanyDocument :one -UPDATE company_documents -SET - document_type = $2, - file_url = $3, - updated_at = NOW() -WHERE id = $1 -RETURNING id, company_id, document_type, file_url, created_at, updated_at -` - -type UpdateCompanyDocumentParams struct { - ID string - DocumentType string - FileUrl string -} - -func (q *Queries) UpdateCompanyDocument(ctx context.Context, arg UpdateCompanyDocumentParams) (CompanyDocument, error) { - row := q.db.QueryRow(ctx, updateCompanyDocument, arg.ID, arg.DocumentType, arg.FileUrl) - var i CompanyDocument - err := row.Scan( - &i.ID, - &i.CompanyID, - &i.DocumentType, - &i.FileUrl, - &i.CreatedAt, - &i.UpdatedAt, - ) - return i, err -} diff --git a/backend/db/company_financials.sql.go b/backend/db/company_financials.sql.go deleted file mode 100644 index c2d70d9..0000000 --- a/backend/db/company_financials.sql.go +++ /dev/null @@ -1,260 +0,0 @@ -// Code generated by sqlc. DO NOT EDIT. -// versions: -// sqlc v1.27.0 -// source: company_financials.sql - -package db - -import ( - "context" - - "github.com/jackc/pgx/v5/pgtype" -) - -const createCompanyFinancials = `-- name: CreateCompanyFinancials :one -INSERT INTO company_financials ( - id, - company_id, - financial_year, - revenue, - expenses, - profit, - sales, - amount_raised, - arr, - grants_received, - created_at, - updated_at -) VALUES ( - gen_random_uuid(), - $1, - $2, - $3, - $4, - $5, - $6, - $7, - $8, - $9, - CURRENT_TIMESTAMP, - CURRENT_TIMESTAMP -) -RETURNING id, company_id, financial_year, revenue, expenses, profit, sales, amount_raised, arr, grants_received, created_at, updated_at -` - -type CreateCompanyFinancialsParams struct { - CompanyID string - FinancialYear int32 - Revenue pgtype.Numeric - Expenses pgtype.Numeric - Profit pgtype.Numeric - Sales pgtype.Numeric - AmountRaised pgtype.Numeric - Arr pgtype.Numeric - GrantsReceived pgtype.Numeric -} - -func (q *Queries) CreateCompanyFinancials(ctx context.Context, arg CreateCompanyFinancialsParams) (CompanyFinancial, error) { - row := q.db.QueryRow(ctx, createCompanyFinancials, - arg.CompanyID, - arg.FinancialYear, - arg.Revenue, - arg.Expenses, - arg.Profit, - arg.Sales, - arg.AmountRaised, - arg.Arr, - arg.GrantsReceived, - ) - var i CompanyFinancial - err := row.Scan( - &i.ID, - &i.CompanyID, - &i.FinancialYear, - &i.Revenue, - &i.Expenses, - &i.Profit, - &i.Sales, - &i.AmountRaised, - &i.Arr, - &i.GrantsReceived, - &i.CreatedAt, - &i.UpdatedAt, - ) - return i, err -} - -const deleteCompanyFinancials = `-- name: DeleteCompanyFinancials :exec -DELETE FROM company_financials -WHERE company_id = $1 -AND financial_year = $2 -` - -type DeleteCompanyFinancialsParams struct { - CompanyID string - FinancialYear int32 -} - -func (q *Queries) DeleteCompanyFinancials(ctx context.Context, arg DeleteCompanyFinancialsParams) error { - _, err := q.db.Exec(ctx, deleteCompanyFinancials, arg.CompanyID, arg.FinancialYear) - return err -} - -const getCompanyFinancialsByYear = `-- name: GetCompanyFinancialsByYear :one -SELECT id, company_id, financial_year, revenue, expenses, profit, sales, amount_raised, arr, grants_received, created_at, updated_at -FROM company_financials -WHERE company_id = $1 -AND financial_year = $2 -LIMIT 1 -` - -type GetCompanyFinancialsByYearParams struct { - CompanyID string - FinancialYear int32 -} - -func (q *Queries) GetCompanyFinancialsByYear(ctx context.Context, arg GetCompanyFinancialsByYearParams) (CompanyFinancial, error) { - row := q.db.QueryRow(ctx, getCompanyFinancialsByYear, arg.CompanyID, arg.FinancialYear) - var i CompanyFinancial - err := row.Scan( - &i.ID, - &i.CompanyID, - &i.FinancialYear, - &i.Revenue, - &i.Expenses, - &i.Profit, - &i.Sales, - &i.AmountRaised, - &i.Arr, - &i.GrantsReceived, - &i.CreatedAt, - &i.UpdatedAt, - ) - return i, err -} - -const getLatestCompanyFinancials = `-- name: GetLatestCompanyFinancials :one -SELECT id, company_id, financial_year, revenue, expenses, profit, sales, amount_raised, arr, grants_received, created_at, updated_at -FROM company_financials -WHERE company_id = $1 -ORDER BY financial_year DESC -LIMIT 1 -` - -func (q *Queries) GetLatestCompanyFinancials(ctx context.Context, companyID string) (CompanyFinancial, error) { - row := q.db.QueryRow(ctx, getLatestCompanyFinancials, companyID) - var i CompanyFinancial - err := row.Scan( - &i.ID, - &i.CompanyID, - &i.FinancialYear, - &i.Revenue, - &i.Expenses, - &i.Profit, - &i.Sales, - &i.AmountRaised, - &i.Arr, - &i.GrantsReceived, - &i.CreatedAt, - &i.UpdatedAt, - ) - return i, err -} - -const listCompanyFinancials = `-- name: ListCompanyFinancials :many -SELECT id, company_id, financial_year, revenue, expenses, profit, sales, amount_raised, arr, grants_received, created_at, updated_at -FROM company_financials -WHERE company_id = $1 -ORDER BY financial_year DESC -` - -func (q *Queries) ListCompanyFinancials(ctx context.Context, companyID string) ([]CompanyFinancial, error) { - rows, err := q.db.Query(ctx, listCompanyFinancials, companyID) - if err != nil { - return nil, err - } - defer rows.Close() - var items []CompanyFinancial - for rows.Next() { - var i CompanyFinancial - if err := rows.Scan( - &i.ID, - &i.CompanyID, - &i.FinancialYear, - &i.Revenue, - &i.Expenses, - &i.Profit, - &i.Sales, - &i.AmountRaised, - &i.Arr, - &i.GrantsReceived, - &i.CreatedAt, - &i.UpdatedAt, - ); err != nil { - return nil, err - } - items = append(items, i) - } - if err := rows.Err(); err != nil { - return nil, err - } - return items, nil -} - -const updateCompanyFinancials = `-- name: UpdateCompanyFinancials :one -UPDATE company_financials -SET - revenue = $3, - expenses = $4, - profit = $5, - sales = $6, - amount_raised = $7, - arr = $8, - grants_received = $9, - updated_at = CURRENT_TIMESTAMP -WHERE company_id = $1 -AND financial_year = $2 -RETURNING id, company_id, financial_year, revenue, expenses, profit, sales, amount_raised, arr, grants_received, created_at, updated_at -` - -type UpdateCompanyFinancialsParams struct { - CompanyID string - FinancialYear int32 - Revenue pgtype.Numeric - Expenses pgtype.Numeric - Profit pgtype.Numeric - Sales pgtype.Numeric - AmountRaised pgtype.Numeric - Arr pgtype.Numeric - GrantsReceived pgtype.Numeric -} - -func (q *Queries) UpdateCompanyFinancials(ctx context.Context, arg UpdateCompanyFinancialsParams) (CompanyFinancial, error) { - row := q.db.QueryRow(ctx, updateCompanyFinancials, - arg.CompanyID, - arg.FinancialYear, - arg.Revenue, - arg.Expenses, - arg.Profit, - arg.Sales, - arg.AmountRaised, - arg.Arr, - arg.GrantsReceived, - ) - var i CompanyFinancial - err := row.Scan( - &i.ID, - &i.CompanyID, - &i.FinancialYear, - &i.Revenue, - &i.Expenses, - &i.Profit, - &i.Sales, - &i.AmountRaised, - &i.Arr, - &i.GrantsReceived, - &i.CreatedAt, - &i.UpdatedAt, - ) - return i, err -} diff --git a/backend/db/company_questions_answers.sql.go b/backend/db/company_questions_answers.sql.go deleted file mode 100644 index 7867fc5..0000000 --- a/backend/db/company_questions_answers.sql.go +++ /dev/null @@ -1,277 +0,0 @@ -// Code generated by sqlc. DO NOT EDIT. -// versions: -// sqlc v1.27.0 -// source: company_questions_answers.sql - -package db - -import ( - "context" - - "github.com/jackc/pgx/v5/pgtype" -) - -const createCompanyAnswer = `-- name: CreateCompanyAnswer :one -INSERT INTO company_question_answers ( - company_id, - question_id, - answer_text -) VALUES ( - $1, $2, $3 -) -RETURNING id, company_id, question_id, answer_text, created_at, updated_at, deleted_at -` - -type CreateCompanyAnswerParams struct { - CompanyID string - QuestionID string - AnswerText string -} - -func (q *Queries) CreateCompanyAnswer(ctx context.Context, arg CreateCompanyAnswerParams) (CompanyQuestionAnswer, error) { - row := q.db.QueryRow(ctx, createCompanyAnswer, arg.CompanyID, arg.QuestionID, arg.AnswerText) - var i CompanyQuestionAnswer - err := row.Scan( - &i.ID, - &i.CompanyID, - &i.QuestionID, - &i.AnswerText, - &i.CreatedAt, - &i.UpdatedAt, - &i.DeletedAt, - ) - return i, err -} - -const createQuestion = `-- name: CreateQuestion :one -INSERT INTO questions ( - question_text -) VALUES ( - $1 -) -RETURNING id, question_text, created_at, updated_at, deleted_at -` - -func (q *Queries) CreateQuestion(ctx context.Context, questionText string) (Question, error) { - row := q.db.QueryRow(ctx, createQuestion, questionText) - var i Question - err := row.Scan( - &i.ID, - &i.QuestionText, - &i.CreatedAt, - &i.UpdatedAt, - &i.DeletedAt, - ) - return i, err -} - -const deleteQuestion = `-- name: DeleteQuestion :exec -UPDATE questions -SET deleted_at = NOW() -WHERE id = $1 AND deleted_at IS NULL -` - -func (q *Queries) DeleteQuestion(ctx context.Context, id string) error { - _, err := q.db.Exec(ctx, deleteQuestion, id) - return err -} - -const getCompanyAnswer = `-- name: GetCompanyAnswer :one -SELECT - cqa.id, cqa.company_id, cqa.question_id, cqa.answer_text, cqa.created_at, cqa.updated_at, cqa.deleted_at, - q.question_text -FROM company_question_answers cqa -JOIN questions q ON q.id = cqa.question_id -WHERE cqa.company_id = $1 AND cqa.question_id = $2 AND cqa.deleted_at IS NULL AND q.deleted_at IS NULL -LIMIT 1 -` - -type GetCompanyAnswerParams struct { - CompanyID string - QuestionID string -} - -type GetCompanyAnswerRow struct { - ID string - CompanyID string - QuestionID string - AnswerText string - CreatedAt pgtype.Timestamp - UpdatedAt pgtype.Timestamp - DeletedAt pgtype.Timestamp - QuestionText string -} - -func (q *Queries) GetCompanyAnswer(ctx context.Context, arg GetCompanyAnswerParams) (GetCompanyAnswerRow, error) { - row := q.db.QueryRow(ctx, getCompanyAnswer, arg.CompanyID, arg.QuestionID) - var i GetCompanyAnswerRow - err := row.Scan( - &i.ID, - &i.CompanyID, - &i.QuestionID, - &i.AnswerText, - &i.CreatedAt, - &i.UpdatedAt, - &i.DeletedAt, - &i.QuestionText, - ) - return i, err -} - -const getQuestion = `-- name: GetQuestion :one -SELECT id, question_text, created_at, updated_at, deleted_at FROM questions -WHERE id = $1 AND deleted_at IS NULL -LIMIT 1 -` - -func (q *Queries) GetQuestion(ctx context.Context, id string) (Question, error) { - row := q.db.QueryRow(ctx, getQuestion, id) - var i Question - err := row.Scan( - &i.ID, - &i.QuestionText, - &i.CreatedAt, - &i.UpdatedAt, - &i.DeletedAt, - ) - return i, err -} - -const listCompanyAnswers = `-- name: ListCompanyAnswers :many -SELECT - cqa.id, cqa.company_id, cqa.question_id, cqa.answer_text, cqa.created_at, cqa.updated_at, cqa.deleted_at, - q.question_text -FROM company_question_answers cqa -JOIN questions q ON q.id = cqa.question_id -WHERE cqa.company_id = $1 AND cqa.deleted_at IS NULL AND q.deleted_at IS NULL -ORDER BY cqa.created_at DESC -` - -type ListCompanyAnswersRow struct { - ID string - CompanyID string - QuestionID string - AnswerText string - CreatedAt pgtype.Timestamp - UpdatedAt pgtype.Timestamp - DeletedAt pgtype.Timestamp - QuestionText string -} - -func (q *Queries) ListCompanyAnswers(ctx context.Context, companyID string) ([]ListCompanyAnswersRow, error) { - rows, err := q.db.Query(ctx, listCompanyAnswers, companyID) - if err != nil { - return nil, err - } - defer rows.Close() - var items []ListCompanyAnswersRow - for rows.Next() { - var i ListCompanyAnswersRow - if err := rows.Scan( - &i.ID, - &i.CompanyID, - &i.QuestionID, - &i.AnswerText, - &i.CreatedAt, - &i.UpdatedAt, - &i.DeletedAt, - &i.QuestionText, - ); err != nil { - return nil, err - } - items = append(items, i) - } - if err := rows.Err(); err != nil { - return nil, err - } - return items, nil -} - -const listQuestions = `-- name: ListQuestions :many -SELECT id, question_text, created_at, updated_at, deleted_at FROM questions -WHERE deleted_at IS NULL -ORDER BY created_at DESC -` - -func (q *Queries) ListQuestions(ctx context.Context) ([]Question, error) { - rows, err := q.db.Query(ctx, listQuestions) - if err != nil { - return nil, err - } - defer rows.Close() - var items []Question - for rows.Next() { - var i Question - if err := rows.Scan( - &i.ID, - &i.QuestionText, - &i.CreatedAt, - &i.UpdatedAt, - &i.DeletedAt, - ); err != nil { - return nil, err - } - items = append(items, i) - } - if err := rows.Err(); err != nil { - return nil, err - } - return items, nil -} - -const softDeleteCompanyAnswer = `-- name: SoftDeleteCompanyAnswer :exec -UPDATE company_question_answers -SET deleted_at = NOW() -WHERE company_id = $1 AND question_id = $2 -` - -type SoftDeleteCompanyAnswerParams struct { - CompanyID string - QuestionID string -} - -func (q *Queries) SoftDeleteCompanyAnswer(ctx context.Context, arg SoftDeleteCompanyAnswerParams) error { - _, err := q.db.Exec(ctx, softDeleteCompanyAnswer, arg.CompanyID, arg.QuestionID) - return err -} - -const softDeleteQuestion = `-- name: SoftDeleteQuestion :exec -UPDATE questions -SET deleted_at = NOW() -WHERE id = $1 -` - -func (q *Queries) SoftDeleteQuestion(ctx context.Context, id string) error { - _, err := q.db.Exec(ctx, softDeleteQuestion, id) - return err -} - -const updateCompanyAnswer = `-- name: UpdateCompanyAnswer :one -UPDATE company_question_answers -SET - answer_text = $3, - updated_at = NOW() -WHERE company_id = $1 AND question_id = $2 AND deleted_at IS NULL -RETURNING id, company_id, question_id, answer_text, created_at, updated_at, deleted_at -` - -type UpdateCompanyAnswerParams struct { - CompanyID string - QuestionID string - AnswerText string -} - -func (q *Queries) UpdateCompanyAnswer(ctx context.Context, arg UpdateCompanyAnswerParams) (CompanyQuestionAnswer, error) { - row := q.db.QueryRow(ctx, updateCompanyAnswer, arg.CompanyID, arg.QuestionID, arg.AnswerText) - var i CompanyQuestionAnswer - err := row.Scan( - &i.ID, - &i.CompanyID, - &i.QuestionID, - &i.AnswerText, - &i.CreatedAt, - &i.UpdatedAt, - &i.DeletedAt, - ) - return i, err -} diff --git a/backend/db/employee.sql.go b/backend/db/employee.sql.go deleted file mode 100644 index 911298e..0000000 --- a/backend/db/employee.sql.go +++ /dev/null @@ -1,213 +0,0 @@ -// Code generated by sqlc. DO NOT EDIT. -// versions: -// sqlc v1.27.0 -// source: employee.sql - -package db - -import ( - "context" -) - -const createEmployee = `-- name: CreateEmployee :one -INSERT INTO employees ( - company_id, - name, - email, - role, - bio -) VALUES ( - $1, $2, $3, $4, $5 -) -RETURNING id, company_id, name, email, role, bio, created_at, updated_at -` - -type CreateEmployeeParams struct { - CompanyID string - Name string - Email string - Role string - Bio *string -} - -func (q *Queries) CreateEmployee(ctx context.Context, arg CreateEmployeeParams) (Employee, error) { - row := q.db.QueryRow(ctx, createEmployee, - arg.CompanyID, - arg.Name, - arg.Email, - arg.Role, - arg.Bio, - ) - var i Employee - err := row.Scan( - &i.ID, - &i.CompanyID, - &i.Name, - &i.Email, - &i.Role, - &i.Bio, - &i.CreatedAt, - &i.UpdatedAt, - ) - return i, err -} - -const deleteEmployee = `-- name: DeleteEmployee :exec -DELETE FROM employees -WHERE id = $1 -` - -func (q *Queries) DeleteEmployee(ctx context.Context, id string) error { - _, err := q.db.Exec(ctx, deleteEmployee, id) - return err -} - -const getEmployeeByEmail = `-- name: GetEmployeeByEmail :one -SELECT id, company_id, name, email, role, bio, created_at, updated_at FROM employees -WHERE email = $1 LIMIT 1 -` - -func (q *Queries) GetEmployeeByEmail(ctx context.Context, email string) (Employee, error) { - row := q.db.QueryRow(ctx, getEmployeeByEmail, email) - var i Employee - err := row.Scan( - &i.ID, - &i.CompanyID, - &i.Name, - &i.Email, - &i.Role, - &i.Bio, - &i.CreatedAt, - &i.UpdatedAt, - ) - return i, err -} - -const getEmployeeByID = `-- name: GetEmployeeByID :one -SELECT id, company_id, name, email, role, bio, created_at, updated_at FROM employees -WHERE id = $1 LIMIT 1 -` - -func (q *Queries) GetEmployeeByID(ctx context.Context, id string) (Employee, error) { - row := q.db.QueryRow(ctx, getEmployeeByID, id) - var i Employee - err := row.Scan( - &i.ID, - &i.CompanyID, - &i.Name, - &i.Email, - &i.Role, - &i.Bio, - &i.CreatedAt, - &i.UpdatedAt, - ) - return i, err -} - -const listEmployees = `-- name: ListEmployees :many -SELECT id, company_id, name, email, role, bio, created_at, updated_at FROM employees -ORDER BY created_at DESC -` - -func (q *Queries) ListEmployees(ctx context.Context) ([]Employee, error) { - rows, err := q.db.Query(ctx, listEmployees) - if err != nil { - return nil, err - } - defer rows.Close() - var items []Employee - for rows.Next() { - var i Employee - if err := rows.Scan( - &i.ID, - &i.CompanyID, - &i.Name, - &i.Email, - &i.Role, - &i.Bio, - &i.CreatedAt, - &i.UpdatedAt, - ); err != nil { - return nil, err - } - items = append(items, i) - } - if err := rows.Err(); err != nil { - return nil, err - } - return items, nil -} - -const listEmployeesByCompany = `-- name: ListEmployeesByCompany :many -SELECT id, company_id, name, email, role, bio, created_at, updated_at FROM employees -WHERE company_id = $1 -ORDER BY created_at DESC -` - -func (q *Queries) ListEmployeesByCompany(ctx context.Context, companyID string) ([]Employee, error) { - rows, err := q.db.Query(ctx, listEmployeesByCompany, companyID) - if err != nil { - return nil, err - } - defer rows.Close() - var items []Employee - for rows.Next() { - var i Employee - if err := rows.Scan( - &i.ID, - &i.CompanyID, - &i.Name, - &i.Email, - &i.Role, - &i.Bio, - &i.CreatedAt, - &i.UpdatedAt, - ); err != nil { - return nil, err - } - items = append(items, i) - } - if err := rows.Err(); err != nil { - return nil, err - } - return items, nil -} - -const updateEmployee = `-- name: UpdateEmployee :one -UPDATE employees -SET - name = $2, - role = $3, - bio = $4, - updated_at = NOW() -WHERE id = $1 -RETURNING id, company_id, name, email, role, bio, created_at, updated_at -` - -type UpdateEmployeeParams struct { - ID string - Name string - Role string - Bio *string -} - -func (q *Queries) UpdateEmployee(ctx context.Context, arg UpdateEmployeeParams) (Employee, error) { - row := q.db.QueryRow(ctx, updateEmployee, - arg.ID, - arg.Name, - arg.Role, - arg.Bio, - ) - var i Employee - err := row.Scan( - &i.ID, - &i.CompanyID, - &i.Name, - &i.Email, - &i.Role, - &i.Bio, - &i.CreatedAt, - &i.UpdatedAt, - ) - return i, err -} diff --git a/backend/db/funding_transactions.sql.go b/backend/db/funding_transactions.sql.go deleted file mode 100644 index b1723d5..0000000 --- a/backend/db/funding_transactions.sql.go +++ /dev/null @@ -1,201 +0,0 @@ -// Code generated by sqlc. DO NOT EDIT. -// versions: -// sqlc v1.27.0 -// source: funding_transactions.sql - -package db - -import ( - "context" - - "github.com/jackc/pgx/v5/pgtype" -) - -const createFundingTransaction = `-- name: CreateFundingTransaction :one -INSERT INTO funding_transactions ( - project_id, - amount, - currency, - transaction_hash, - from_wallet_address, - to_wallet_address, - status -) VALUES ( - $1, $2, $3, $4, $5, $6, $7 -) -RETURNING id, project_id, amount, currency, transaction_hash, from_wallet_address, to_wallet_address, status, created_at, updated_at -` - -type CreateFundingTransactionParams struct { - ProjectID string - Amount pgtype.Numeric - Currency string - TransactionHash string - FromWalletAddress string - ToWalletAddress string - Status string -} - -func (q *Queries) CreateFundingTransaction(ctx context.Context, arg CreateFundingTransactionParams) (FundingTransaction, error) { - row := q.db.QueryRow(ctx, createFundingTransaction, - arg.ProjectID, - arg.Amount, - arg.Currency, - arg.TransactionHash, - arg.FromWalletAddress, - arg.ToWalletAddress, - arg.Status, - ) - var i FundingTransaction - err := row.Scan( - &i.ID, - &i.ProjectID, - &i.Amount, - &i.Currency, - &i.TransactionHash, - &i.FromWalletAddress, - &i.ToWalletAddress, - &i.Status, - &i.CreatedAt, - &i.UpdatedAt, - ) - return i, err -} - -const deleteFundingTransaction = `-- name: DeleteFundingTransaction :exec -DELETE FROM funding_transactions -WHERE id = $1 -` - -func (q *Queries) DeleteFundingTransaction(ctx context.Context, id string) error { - _, err := q.db.Exec(ctx, deleteFundingTransaction, id) - return err -} - -const getFundingTransaction = `-- name: GetFundingTransaction :one -SELECT id, project_id, amount, currency, transaction_hash, from_wallet_address, to_wallet_address, status, created_at, updated_at FROM funding_transactions -WHERE id = $1 LIMIT 1 -` - -func (q *Queries) GetFundingTransaction(ctx context.Context, id string) (FundingTransaction, error) { - row := q.db.QueryRow(ctx, getFundingTransaction, id) - var i FundingTransaction - err := row.Scan( - &i.ID, - &i.ProjectID, - &i.Amount, - &i.Currency, - &i.TransactionHash, - &i.FromWalletAddress, - &i.ToWalletAddress, - &i.Status, - &i.CreatedAt, - &i.UpdatedAt, - ) - return i, err -} - -const listFundingTransactions = `-- name: ListFundingTransactions :many -SELECT id, project_id, amount, currency, transaction_hash, from_wallet_address, to_wallet_address, status, created_at, updated_at FROM funding_transactions -ORDER BY created_at DESC -` - -func (q *Queries) ListFundingTransactions(ctx context.Context) ([]FundingTransaction, error) { - rows, err := q.db.Query(ctx, listFundingTransactions) - if err != nil { - return nil, err - } - defer rows.Close() - var items []FundingTransaction - for rows.Next() { - var i FundingTransaction - if err := rows.Scan( - &i.ID, - &i.ProjectID, - &i.Amount, - &i.Currency, - &i.TransactionHash, - &i.FromWalletAddress, - &i.ToWalletAddress, - &i.Status, - &i.CreatedAt, - &i.UpdatedAt, - ); err != nil { - return nil, err - } - items = append(items, i) - } - if err := rows.Err(); err != nil { - return nil, err - } - return items, nil -} - -const listProjectFundingTransactions = `-- name: ListProjectFundingTransactions :many -SELECT id, project_id, amount, currency, transaction_hash, from_wallet_address, to_wallet_address, status, created_at, updated_at FROM funding_transactions -WHERE project_id = $1 -ORDER BY created_at DESC -` - -func (q *Queries) ListProjectFundingTransactions(ctx context.Context, projectID string) ([]FundingTransaction, error) { - rows, err := q.db.Query(ctx, listProjectFundingTransactions, projectID) - if err != nil { - return nil, err - } - defer rows.Close() - var items []FundingTransaction - for rows.Next() { - var i FundingTransaction - if err := rows.Scan( - &i.ID, - &i.ProjectID, - &i.Amount, - &i.Currency, - &i.TransactionHash, - &i.FromWalletAddress, - &i.ToWalletAddress, - &i.Status, - &i.CreatedAt, - &i.UpdatedAt, - ); err != nil { - return nil, err - } - items = append(items, i) - } - if err := rows.Err(); err != nil { - return nil, err - } - return items, nil -} - -const updateFundingTransactionStatus = `-- name: UpdateFundingTransactionStatus :one -UPDATE funding_transactions -SET - status = $2, - updated_at = NOW() -WHERE id = $1 -RETURNING id, project_id, amount, currency, transaction_hash, from_wallet_address, to_wallet_address, status, created_at, updated_at -` - -type UpdateFundingTransactionStatusParams struct { - ID string - Status string -} - -func (q *Queries) UpdateFundingTransactionStatus(ctx context.Context, arg UpdateFundingTransactionStatusParams) (FundingTransaction, error) { - row := q.db.QueryRow(ctx, updateFundingTransactionStatus, arg.ID, arg.Status) - var i FundingTransaction - err := row.Scan( - &i.ID, - &i.ProjectID, - &i.Amount, - &i.Currency, - &i.TransactionHash, - &i.FromWalletAddress, - &i.ToWalletAddress, - &i.Status, - &i.CreatedAt, - &i.UpdatedAt, - ) - return i, err -} diff --git a/backend/db/meetings.sql.go b/backend/db/meetings.sql.go deleted file mode 100644 index 0e72a96..0000000 --- a/backend/db/meetings.sql.go +++ /dev/null @@ -1,215 +0,0 @@ -// Code generated by sqlc. DO NOT EDIT. -// versions: -// sqlc v1.27.0 -// source: meetings.sql - -package db - -import ( - "context" - "time" -) - -const createMeeting = `-- name: CreateMeeting :one -INSERT INTO meetings ( - project_id, - scheduled_by_user_id, - start_time, - end_time, - meeting_url, - location, - notes -) VALUES ( - $1, $2, $3, $4, $5, $6, $7 -) -RETURNING id, project_id, scheduled_by_user_id, start_time, end_time, meeting_url, location, notes, created_at, updated_at -` - -type CreateMeetingParams struct { - ProjectID string - ScheduledByUserID string - StartTime time.Time - EndTime time.Time - MeetingUrl *string - Location *string - Notes *string -} - -func (q *Queries) CreateMeeting(ctx context.Context, arg CreateMeetingParams) (Meeting, error) { - row := q.db.QueryRow(ctx, createMeeting, - arg.ProjectID, - arg.ScheduledByUserID, - arg.StartTime, - arg.EndTime, - arg.MeetingUrl, - arg.Location, - arg.Notes, - ) - var i Meeting - err := row.Scan( - &i.ID, - &i.ProjectID, - &i.ScheduledByUserID, - &i.StartTime, - &i.EndTime, - &i.MeetingUrl, - &i.Location, - &i.Notes, - &i.CreatedAt, - &i.UpdatedAt, - ) - return i, err -} - -const deleteMeeting = `-- name: DeleteMeeting :exec -DELETE FROM meetings -WHERE id = $1 -` - -func (q *Queries) DeleteMeeting(ctx context.Context, id string) error { - _, err := q.db.Exec(ctx, deleteMeeting, id) - return err -} - -const getMeeting = `-- name: GetMeeting :one -SELECT id, project_id, scheduled_by_user_id, start_time, end_time, meeting_url, location, notes, created_at, updated_at FROM meetings -WHERE id = $1 LIMIT 1 -` - -func (q *Queries) GetMeeting(ctx context.Context, id string) (Meeting, error) { - row := q.db.QueryRow(ctx, getMeeting, id) - var i Meeting - err := row.Scan( - &i.ID, - &i.ProjectID, - &i.ScheduledByUserID, - &i.StartTime, - &i.EndTime, - &i.MeetingUrl, - &i.Location, - &i.Notes, - &i.CreatedAt, - &i.UpdatedAt, - ) - return i, err -} - -const listMeetings = `-- name: ListMeetings :many -SELECT id, project_id, scheduled_by_user_id, start_time, end_time, meeting_url, location, notes, created_at, updated_at FROM meetings -ORDER BY start_time DESC -` - -func (q *Queries) ListMeetings(ctx context.Context) ([]Meeting, error) { - rows, err := q.db.Query(ctx, listMeetings) - if err != nil { - return nil, err - } - defer rows.Close() - var items []Meeting - for rows.Next() { - var i Meeting - if err := rows.Scan( - &i.ID, - &i.ProjectID, - &i.ScheduledByUserID, - &i.StartTime, - &i.EndTime, - &i.MeetingUrl, - &i.Location, - &i.Notes, - &i.CreatedAt, - &i.UpdatedAt, - ); err != nil { - return nil, err - } - items = append(items, i) - } - if err := rows.Err(); err != nil { - return nil, err - } - return items, nil -} - -const listProjectMeetings = `-- name: ListProjectMeetings :many -SELECT id, project_id, scheduled_by_user_id, start_time, end_time, meeting_url, location, notes, created_at, updated_at FROM meetings -WHERE project_id = $1 -ORDER BY start_time DESC -` - -func (q *Queries) ListProjectMeetings(ctx context.Context, projectID string) ([]Meeting, error) { - rows, err := q.db.Query(ctx, listProjectMeetings, projectID) - if err != nil { - return nil, err - } - defer rows.Close() - var items []Meeting - for rows.Next() { - var i Meeting - if err := rows.Scan( - &i.ID, - &i.ProjectID, - &i.ScheduledByUserID, - &i.StartTime, - &i.EndTime, - &i.MeetingUrl, - &i.Location, - &i.Notes, - &i.CreatedAt, - &i.UpdatedAt, - ); err != nil { - return nil, err - } - items = append(items, i) - } - if err := rows.Err(); err != nil { - return nil, err - } - return items, nil -} - -const updateMeeting = `-- name: UpdateMeeting :one -UPDATE meetings -SET - start_time = $2, - end_time = $3, - meeting_url = $4, - location = $5, - notes = $6, - updated_at = NOW() -WHERE id = $1 -RETURNING id, project_id, scheduled_by_user_id, start_time, end_time, meeting_url, location, notes, created_at, updated_at -` - -type UpdateMeetingParams struct { - ID string - StartTime time.Time - EndTime time.Time - MeetingUrl *string - Location *string - Notes *string -} - -func (q *Queries) UpdateMeeting(ctx context.Context, arg UpdateMeetingParams) (Meeting, error) { - row := q.db.QueryRow(ctx, updateMeeting, - arg.ID, - arg.StartTime, - arg.EndTime, - arg.MeetingUrl, - arg.Location, - arg.Notes, - ) - var i Meeting - err := row.Scan( - &i.ID, - &i.ProjectID, - &i.ScheduledByUserID, - &i.StartTime, - &i.EndTime, - &i.MeetingUrl, - &i.Location, - &i.Notes, - &i.CreatedAt, - &i.UpdatedAt, - ) - return i, err -} diff --git a/backend/db/projects.sql.go b/backend/db/projects.sql.go deleted file mode 100644 index 78896ca..0000000 --- a/backend/db/projects.sql.go +++ /dev/null @@ -1,699 +0,0 @@ -// Code generated by sqlc. DO NOT EDIT. -// versions: -// sqlc v1.27.0 -// source: projects.sql - -package db - -import ( - "context" - - "github.com/jackc/pgx/v5/pgtype" -) - -const addProjectTag = `-- name: AddProjectTag :one -INSERT INTO project_tags ( - project_id, - tag_id -) VALUES ( - $1, $2 -) -RETURNING id, project_id, tag_id, created_at -` - -type AddProjectTagParams struct { - ProjectID string - TagID string -} - -func (q *Queries) AddProjectTag(ctx context.Context, arg AddProjectTagParams) (ProjectTag, error) { - row := q.db.QueryRow(ctx, addProjectTag, arg.ProjectID, arg.TagID) - var i ProjectTag - err := row.Scan( - &i.ID, - &i.ProjectID, - &i.TagID, - &i.CreatedAt, - ) - return i, err -} - -const createProject = `-- name: CreateProject :one -INSERT INTO projects ( - company_id, - title, - description, - status -) VALUES ( - $1, $2, $3, $4 -) -RETURNING id, company_id, title, description, status, created_at, updated_at -` - -type CreateProjectParams struct { - CompanyID string - Title string - Description *string - Status string -} - -func (q *Queries) CreateProject(ctx context.Context, arg CreateProjectParams) (Project, error) { - row := q.db.QueryRow(ctx, createProject, - arg.CompanyID, - arg.Title, - arg.Description, - arg.Status, - ) - var i Project - err := row.Scan( - &i.ID, - &i.CompanyID, - &i.Title, - &i.Description, - &i.Status, - &i.CreatedAt, - &i.UpdatedAt, - ) - return i, err -} - -const createProjectComment = `-- name: CreateProjectComment :one -INSERT INTO project_comments ( - project_id, - user_id, - comment -) VALUES ( - $1, $2, $3 -) -RETURNING id, project_id, user_id, comment, created_at, updated_at -` - -type CreateProjectCommentParams struct { - ProjectID string - UserID string - Comment string -} - -func (q *Queries) CreateProjectComment(ctx context.Context, arg CreateProjectCommentParams) (ProjectComment, error) { - row := q.db.QueryRow(ctx, createProjectComment, arg.ProjectID, arg.UserID, arg.Comment) - var i ProjectComment - err := row.Scan( - &i.ID, - &i.ProjectID, - &i.UserID, - &i.Comment, - &i.CreatedAt, - &i.UpdatedAt, - ) - return i, err -} - -const createProjectFile = `-- name: CreateProjectFile :one -INSERT INTO project_files ( - project_id, - file_type, - file_url -) VALUES ( - $1, $2, $3 -) -RETURNING id, project_id, file_type, file_url, created_at, updated_at -` - -type CreateProjectFileParams struct { - ProjectID string - FileType string - FileUrl string -} - -func (q *Queries) CreateProjectFile(ctx context.Context, arg CreateProjectFileParams) (ProjectFile, error) { - row := q.db.QueryRow(ctx, createProjectFile, arg.ProjectID, arg.FileType, arg.FileUrl) - var i ProjectFile - err := row.Scan( - &i.ID, - &i.ProjectID, - &i.FileType, - &i.FileUrl, - &i.CreatedAt, - &i.UpdatedAt, - ) - return i, err -} - -const createProjectLink = `-- name: CreateProjectLink :one -INSERT INTO project_links ( - project_id, - link_type, - url -) VALUES ( - $1, $2, $3 -) -RETURNING id, project_id, link_type, url, created_at, updated_at -` - -type CreateProjectLinkParams struct { - ProjectID string - LinkType string - Url string -} - -func (q *Queries) CreateProjectLink(ctx context.Context, arg CreateProjectLinkParams) (ProjectLink, error) { - row := q.db.QueryRow(ctx, createProjectLink, arg.ProjectID, arg.LinkType, arg.Url) - var i ProjectLink - err := row.Scan( - &i.ID, - &i.ProjectID, - &i.LinkType, - &i.Url, - &i.CreatedAt, - &i.UpdatedAt, - ) - return i, err -} - -const createProjectQuestion = `-- name: CreateProjectQuestion :one -INSERT INTO project_questions ( - section_id, - question_text, - answer_text -) VALUES ( - $1, $2, $3 -) -RETURNING id, section_id, question_text, answer_text, created_at, updated_at -` - -type CreateProjectQuestionParams struct { - SectionID string - QuestionText string - AnswerText string -} - -func (q *Queries) CreateProjectQuestion(ctx context.Context, arg CreateProjectQuestionParams) (ProjectQuestion, error) { - row := q.db.QueryRow(ctx, createProjectQuestion, arg.SectionID, arg.QuestionText, arg.AnswerText) - var i ProjectQuestion - err := row.Scan( - &i.ID, - &i.SectionID, - &i.QuestionText, - &i.AnswerText, - &i.CreatedAt, - &i.UpdatedAt, - ) - return i, err -} - -const createProjectSection = `-- name: CreateProjectSection :one -INSERT INTO project_sections ( - project_id, - title -) VALUES ( - $1, $2 -) -RETURNING id, project_id, title, created_at, updated_at -` - -type CreateProjectSectionParams struct { - ProjectID string - Title string -} - -func (q *Queries) CreateProjectSection(ctx context.Context, arg CreateProjectSectionParams) (ProjectSection, error) { - row := q.db.QueryRow(ctx, createProjectSection, arg.ProjectID, arg.Title) - var i ProjectSection - err := row.Scan( - &i.ID, - &i.ProjectID, - &i.Title, - &i.CreatedAt, - &i.UpdatedAt, - ) - return i, err -} - -const deleteAllProjectTags = `-- name: DeleteAllProjectTags :exec -DELETE FROM project_tags -WHERE project_id = $1 -` - -func (q *Queries) DeleteAllProjectTags(ctx context.Context, projectID string) error { - _, err := q.db.Exec(ctx, deleteAllProjectTags, projectID) - return err -} - -const deleteProject = `-- name: DeleteProject :exec -DELETE FROM projects -WHERE id = $1 -` - -func (q *Queries) DeleteProject(ctx context.Context, id string) error { - _, err := q.db.Exec(ctx, deleteProject, id) - return err -} - -const deleteProjectComment = `-- name: DeleteProjectComment :exec -DELETE FROM project_comments -WHERE id = $1 -` - -func (q *Queries) DeleteProjectComment(ctx context.Context, id string) error { - _, err := q.db.Exec(ctx, deleteProjectComment, id) - return err -} - -const deleteProjectFile = `-- name: DeleteProjectFile :exec -DELETE FROM project_files -WHERE id = $1 -` - -func (q *Queries) DeleteProjectFile(ctx context.Context, id string) error { - _, err := q.db.Exec(ctx, deleteProjectFile, id) - return err -} - -const deleteProjectLink = `-- name: DeleteProjectLink :exec -DELETE FROM project_links -WHERE id = $1 -` - -func (q *Queries) DeleteProjectLink(ctx context.Context, id string) error { - _, err := q.db.Exec(ctx, deleteProjectLink, id) - return err -} - -const deleteProjectTag = `-- name: DeleteProjectTag :exec -DELETE FROM project_tags -WHERE project_id = $1 AND tag_id = $2 -` - -type DeleteProjectTagParams struct { - ProjectID string - TagID string -} - -func (q *Queries) DeleteProjectTag(ctx context.Context, arg DeleteProjectTagParams) error { - _, err := q.db.Exec(ctx, deleteProjectTag, arg.ProjectID, arg.TagID) - return err -} - -const getProject = `-- name: GetProject :one -SELECT id, company_id, title, description, status, created_at, updated_at FROM projects -WHERE id = $1 LIMIT 1 -` - -func (q *Queries) GetProject(ctx context.Context, id string) (Project, error) { - row := q.db.QueryRow(ctx, getProject, id) - var i Project - err := row.Scan( - &i.ID, - &i.CompanyID, - &i.Title, - &i.Description, - &i.Status, - &i.CreatedAt, - &i.UpdatedAt, - ) - return i, err -} - -const getProjectComments = `-- name: GetProjectComments :many -SELECT - pc.id, pc.project_id, pc.user_id, pc.comment, pc.created_at, pc.updated_at, - u.first_name, - u.last_name, - u.email -FROM project_comments pc -JOIN users u ON u.id = pc.user_id -WHERE pc.project_id = $1 -ORDER BY pc.created_at DESC -` - -type GetProjectCommentsRow struct { - ID string - ProjectID string - UserID string - Comment string - CreatedAt pgtype.Timestamp - UpdatedAt pgtype.Timestamp - FirstName *string - LastName *string - Email string -} - -func (q *Queries) GetProjectComments(ctx context.Context, projectID string) ([]GetProjectCommentsRow, error) { - rows, err := q.db.Query(ctx, getProjectComments, projectID) - if err != nil { - return nil, err - } - defer rows.Close() - var items []GetProjectCommentsRow - for rows.Next() { - var i GetProjectCommentsRow - if err := rows.Scan( - &i.ID, - &i.ProjectID, - &i.UserID, - &i.Comment, - &i.CreatedAt, - &i.UpdatedAt, - &i.FirstName, - &i.LastName, - &i.Email, - ); err != nil { - return nil, err - } - items = append(items, i) - } - if err := rows.Err(); err != nil { - return nil, err - } - return items, nil -} - -const listProjectFiles = `-- name: ListProjectFiles :many -SELECT id, project_id, file_type, file_url, created_at, updated_at FROM project_files -WHERE project_id = $1 -ORDER BY created_at DESC -` - -func (q *Queries) ListProjectFiles(ctx context.Context, projectID string) ([]ProjectFile, error) { - rows, err := q.db.Query(ctx, listProjectFiles, projectID) - if err != nil { - return nil, err - } - defer rows.Close() - var items []ProjectFile - for rows.Next() { - var i ProjectFile - if err := rows.Scan( - &i.ID, - &i.ProjectID, - &i.FileType, - &i.FileUrl, - &i.CreatedAt, - &i.UpdatedAt, - ); err != nil { - return nil, err - } - items = append(items, i) - } - if err := rows.Err(); err != nil { - return nil, err - } - return items, nil -} - -const listProjectLinks = `-- name: ListProjectLinks :many -SELECT id, project_id, link_type, url, created_at, updated_at FROM project_links -WHERE project_id = $1 -ORDER BY created_at DESC -` - -func (q *Queries) ListProjectLinks(ctx context.Context, projectID string) ([]ProjectLink, error) { - rows, err := q.db.Query(ctx, listProjectLinks, projectID) - if err != nil { - return nil, err - } - defer rows.Close() - var items []ProjectLink - for rows.Next() { - var i ProjectLink - if err := rows.Scan( - &i.ID, - &i.ProjectID, - &i.LinkType, - &i.Url, - &i.CreatedAt, - &i.UpdatedAt, - ); err != nil { - return nil, err - } - items = append(items, i) - } - if err := rows.Err(); err != nil { - return nil, err - } - return items, nil -} - -const listProjectTags = `-- name: ListProjectTags :many -SELECT - pt.id, pt.project_id, pt.tag_id, pt.created_at, - t.name as tag_name -FROM project_tags pt -JOIN tags t ON t.id = pt.tag_id -WHERE pt.project_id = $1 -ORDER BY t.name -` - -type ListProjectTagsRow struct { - ID string - ProjectID string - TagID string - CreatedAt pgtype.Timestamp - TagName string -} - -func (q *Queries) ListProjectTags(ctx context.Context, projectID string) ([]ListProjectTagsRow, error) { - rows, err := q.db.Query(ctx, listProjectTags, projectID) - if err != nil { - return nil, err - } - defer rows.Close() - var items []ListProjectTagsRow - for rows.Next() { - var i ListProjectTagsRow - if err := rows.Scan( - &i.ID, - &i.ProjectID, - &i.TagID, - &i.CreatedAt, - &i.TagName, - ); err != nil { - return nil, err - } - items = append(items, i) - } - if err := rows.Err(); err != nil { - return nil, err - } - return items, nil -} - -const listProjectWithDetails = `-- name: ListProjectWithDetails :one -SELECT - p.id, p.company_id, p.title, p.description, p.status, p.created_at, p.updated_at, - c.name as company_name, - c.industry as company_industry, - c.founded_date as company_founded_date, - c.company_stage as company_stage, - COALESCE( - ( - SELECT json_agg( - json_build_object( - 'id', ps.id, - 'title', ps.title, - 'questions', ( - SELECT COALESCE( - json_agg( - json_build_object( - 'question', pq.question_text, - 'answer', pq.answer_text - ) - ), - '[]'::json - ) - FROM project_questions pq - WHERE pq.section_id = ps.id - ) - ) - ) - FROM project_sections ps - WHERE ps.project_id = p.id - ), - '[]'::json - ) as sections, - COALESCE( - ( - SELECT json_agg( - json_build_object( - 'id', pf.id, - 'name', pf.file_type, - 'url', pf.file_url - ) - ) - FROM project_files pf - WHERE pf.project_id = p.id - ), - '[]'::json - ) as documents -FROM projects p -LEFT JOIN companies c ON p.company_id = c.id -WHERE p.id = $1 -GROUP BY p.id, c.id -` - -type ListProjectWithDetailsRow struct { - ID string - CompanyID string - Title string - Description *string - Status string - CreatedAt pgtype.Timestamp - UpdatedAt pgtype.Timestamp - CompanyName *string - CompanyIndustry *string - CompanyFoundedDate pgtype.Date - CompanyStage *string - Sections interface{} - Documents interface{} -} - -func (q *Queries) ListProjectWithDetails(ctx context.Context, id string) (ListProjectWithDetailsRow, error) { - row := q.db.QueryRow(ctx, listProjectWithDetails, id) - var i ListProjectWithDetailsRow - err := row.Scan( - &i.ID, - &i.CompanyID, - &i.Title, - &i.Description, - &i.Status, - &i.CreatedAt, - &i.UpdatedAt, - &i.CompanyName, - &i.CompanyIndustry, - &i.CompanyFoundedDate, - &i.CompanyStage, - &i.Sections, - &i.Documents, - ) - return i, err -} - -const listProjects = `-- name: ListProjects :many -SELECT - p.id, p.company_id, p.title, p.description, p.status, p.created_at, p.updated_at, - c.name as company_name, - c.industry as company_industry, - c.founded_date as company_founded_date, - c.company_stage as company_stage -FROM projects p -LEFT JOIN companies c ON p.company_id = c.id -ORDER BY p.created_at DESC -` - -type ListProjectsRow struct { - ID string - CompanyID string - Title string - Description *string - Status string - CreatedAt pgtype.Timestamp - UpdatedAt pgtype.Timestamp - CompanyName *string - CompanyIndustry *string - CompanyFoundedDate pgtype.Date - CompanyStage *string -} - -func (q *Queries) ListProjects(ctx context.Context) ([]ListProjectsRow, error) { - rows, err := q.db.Query(ctx, listProjects) - if err != nil { - return nil, err - } - defer rows.Close() - var items []ListProjectsRow - for rows.Next() { - var i ListProjectsRow - if err := rows.Scan( - &i.ID, - &i.CompanyID, - &i.Title, - &i.Description, - &i.Status, - &i.CreatedAt, - &i.UpdatedAt, - &i.CompanyName, - &i.CompanyIndustry, - &i.CompanyFoundedDate, - &i.CompanyStage, - ); err != nil { - return nil, err - } - items = append(items, i) - } - if err := rows.Err(); err != nil { - return nil, err - } - return items, nil -} - -const listProjectsByCompany = `-- name: ListProjectsByCompany :many -SELECT id, company_id, title, description, status, created_at, updated_at FROM projects -WHERE company_id = $1 -ORDER BY created_at DESC -` - -func (q *Queries) ListProjectsByCompany(ctx context.Context, companyID string) ([]Project, error) { - rows, err := q.db.Query(ctx, listProjectsByCompany, companyID) - if err != nil { - return nil, err - } - defer rows.Close() - var items []Project - for rows.Next() { - var i Project - if err := rows.Scan( - &i.ID, - &i.CompanyID, - &i.Title, - &i.Description, - &i.Status, - &i.CreatedAt, - &i.UpdatedAt, - ); err != nil { - return nil, err - } - items = append(items, i) - } - if err := rows.Err(); err != nil { - return nil, err - } - return items, nil -} - -const updateProject = `-- name: UpdateProject :one -UPDATE projects -SET - title = $2, - description = $3, - status = $4, - updated_at = NOW() -WHERE id = $1 -RETURNING id, company_id, title, description, status, created_at, updated_at -` - -type UpdateProjectParams struct { - ID string - Title string - Description *string - Status string -} - -func (q *Queries) UpdateProject(ctx context.Context, arg UpdateProjectParams) (Project, error) { - row := q.db.QueryRow(ctx, updateProject, - arg.ID, - arg.Title, - arg.Description, - arg.Status, - ) - var i Project - err := row.Scan( - &i.ID, - &i.CompanyID, - &i.Title, - &i.Description, - &i.Status, - &i.CreatedAt, - &i.UpdatedAt, - ) - return i, err -} diff --git a/backend/db/resource_requests.sql.go b/backend/db/resource_requests.sql.go deleted file mode 100644 index 722f5f0..0000000 --- a/backend/db/resource_requests.sql.go +++ /dev/null @@ -1,184 +0,0 @@ -// Code generated by sqlc. DO NOT EDIT. -// versions: -// sqlc v1.27.0 -// source: resource_requests.sql - -package db - -import ( - "context" -) - -const createResourceRequest = `-- name: CreateResourceRequest :one -INSERT INTO resource_requests ( - id, - company_id, - resource_type, - description, - status, - created_at, - updated_at -) VALUES ( - gen_random_uuid(), - $1, - $2, - $3, - $4, - CURRENT_TIMESTAMP, - CURRENT_TIMESTAMP -) -RETURNING id, company_id, resource_type, description, status, created_at, updated_at -` - -type CreateResourceRequestParams struct { - CompanyID string - ResourceType string - Description *string - Status string -} - -func (q *Queries) CreateResourceRequest(ctx context.Context, arg CreateResourceRequestParams) (ResourceRequest, error) { - row := q.db.QueryRow(ctx, createResourceRequest, - arg.CompanyID, - arg.ResourceType, - arg.Description, - arg.Status, - ) - var i ResourceRequest - err := row.Scan( - &i.ID, - &i.CompanyID, - &i.ResourceType, - &i.Description, - &i.Status, - &i.CreatedAt, - &i.UpdatedAt, - ) - return i, err -} - -const deleteResourceRequest = `-- name: DeleteResourceRequest :exec -DELETE FROM resource_requests -WHERE id = $1 -` - -func (q *Queries) DeleteResourceRequest(ctx context.Context, id string) error { - _, err := q.db.Exec(ctx, deleteResourceRequest, id) - return err -} - -const getResourceRequestByID = `-- name: GetResourceRequestByID :one -SELECT id, company_id, resource_type, description, status, created_at, updated_at FROM resource_requests -WHERE id = $1 LIMIT 1 -` - -func (q *Queries) GetResourceRequestByID(ctx context.Context, id string) (ResourceRequest, error) { - row := q.db.QueryRow(ctx, getResourceRequestByID, id) - var i ResourceRequest - err := row.Scan( - &i.ID, - &i.CompanyID, - &i.ResourceType, - &i.Description, - &i.Status, - &i.CreatedAt, - &i.UpdatedAt, - ) - return i, err -} - -const listResourceRequests = `-- name: ListResourceRequests :many -SELECT id, company_id, resource_type, description, status, created_at, updated_at FROM resource_requests -ORDER BY updated_at DESC -` - -func (q *Queries) ListResourceRequests(ctx context.Context) ([]ResourceRequest, error) { - rows, err := q.db.Query(ctx, listResourceRequests) - if err != nil { - return nil, err - } - defer rows.Close() - var items []ResourceRequest - for rows.Next() { - var i ResourceRequest - if err := rows.Scan( - &i.ID, - &i.CompanyID, - &i.ResourceType, - &i.Description, - &i.Status, - &i.CreatedAt, - &i.UpdatedAt, - ); err != nil { - return nil, err - } - items = append(items, i) - } - if err := rows.Err(); err != nil { - return nil, err - } - return items, nil -} - -const listResourceRequestsByCompany = `-- name: ListResourceRequestsByCompany :many -SELECT id, company_id, resource_type, description, status, created_at, updated_at FROM resource_requests -WHERE company_id = $1 -ORDER BY updated_at DESC -` - -func (q *Queries) ListResourceRequestsByCompany(ctx context.Context, companyID string) ([]ResourceRequest, error) { - rows, err := q.db.Query(ctx, listResourceRequestsByCompany, companyID) - if err != nil { - return nil, err - } - defer rows.Close() - var items []ResourceRequest - for rows.Next() { - var i ResourceRequest - if err := rows.Scan( - &i.ID, - &i.CompanyID, - &i.ResourceType, - &i.Description, - &i.Status, - &i.CreatedAt, - &i.UpdatedAt, - ); err != nil { - return nil, err - } - items = append(items, i) - } - if err := rows.Err(); err != nil { - return nil, err - } - return items, nil -} - -const updateResourceRequestStatus = `-- name: UpdateResourceRequestStatus :one -UPDATE resource_requests -SET - status = $2, - updated_at = CURRENT_TIMESTAMP -WHERE id = $1 -RETURNING id, company_id, resource_type, description, status, created_at, updated_at -` - -type UpdateResourceRequestStatusParams struct { - ID string - Status string -} - -func (q *Queries) UpdateResourceRequestStatus(ctx context.Context, arg UpdateResourceRequestStatusParams) (ResourceRequest, error) { - row := q.db.QueryRow(ctx, updateResourceRequestStatus, arg.ID, arg.Status) - var i ResourceRequest - err := row.Scan( - &i.ID, - &i.CompanyID, - &i.ResourceType, - &i.Description, - &i.Status, - &i.CreatedAt, - &i.UpdatedAt, - ) - return i, err -} diff --git a/backend/db/tags.sql.go b/backend/db/tags.sql.go deleted file mode 100644 index c8e94b1..0000000 --- a/backend/db/tags.sql.go +++ /dev/null @@ -1,88 +0,0 @@ -// Code generated by sqlc. DO NOT EDIT. -// versions: -// sqlc v1.27.0 -// source: tags.sql - -package db - -import ( - "context" -) - -const createTag = `-- name: CreateTag :one -INSERT INTO tags ( - name -) VALUES ( - $1 -) -RETURNING id, name, created_at, updated_at -` - -func (q *Queries) CreateTag(ctx context.Context, name string) (Tag, error) { - row := q.db.QueryRow(ctx, createTag, name) - var i Tag - err := row.Scan( - &i.ID, - &i.Name, - &i.CreatedAt, - &i.UpdatedAt, - ) - return i, err -} - -const deleteTag = `-- name: DeleteTag :exec -DELETE FROM tags -WHERE id = $1 -` - -func (q *Queries) DeleteTag(ctx context.Context, id string) error { - _, err := q.db.Exec(ctx, deleteTag, id) - return err -} - -const getTag = `-- name: GetTag :one -SELECT id, name, created_at, updated_at FROM tags -WHERE id = $1 LIMIT 1 -` - -func (q *Queries) GetTag(ctx context.Context, id string) (Tag, error) { - row := q.db.QueryRow(ctx, getTag, id) - var i Tag - err := row.Scan( - &i.ID, - &i.Name, - &i.CreatedAt, - &i.UpdatedAt, - ) - return i, err -} - -const listTags = `-- name: ListTags :many -SELECT id, name, created_at, updated_at FROM tags -ORDER BY name -` - -func (q *Queries) ListTags(ctx context.Context) ([]Tag, error) { - rows, err := q.db.Query(ctx, listTags) - if err != nil { - return nil, err - } - defer rows.Close() - var items []Tag - for rows.Next() { - var i Tag - if err := rows.Scan( - &i.ID, - &i.Name, - &i.CreatedAt, - &i.UpdatedAt, - ); err != nil { - return nil, err - } - items = append(items, i) - } - if err := rows.Err(); err != nil { - return nil, err - } - return items, nil -} diff --git a/backend/db/users.sql.go b/backend/db/users.sql.go deleted file mode 100644 index 535fdbb..0000000 --- a/backend/db/users.sql.go +++ /dev/null @@ -1,131 +0,0 @@ -// Code generated by sqlc. DO NOT EDIT. -// versions: -// sqlc v1.27.0 -// source: users.sql - -package db - -import ( - "context" -) - -const createUser = `-- name: CreateUser :one -INSERT INTO users ( - email, - password_hash, - role, - token_salt -) VALUES ( - $1, $2, $3, gen_random_bytes(32) -) RETURNING id, email, password_hash, first_name, last_name, wallet_address, created_at, updated_at, role, email_verified, token_salt -` - -type CreateUserParams struct { - Email string - PasswordHash string - Role UserRole -} - -func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (User, error) { - row := q.db.QueryRow(ctx, createUser, arg.Email, arg.PasswordHash, arg.Role) - var i User - err := row.Scan( - &i.ID, - &i.Email, - &i.PasswordHash, - &i.FirstName, - &i.LastName, - &i.WalletAddress, - &i.CreatedAt, - &i.UpdatedAt, - &i.Role, - &i.EmailVerified, - &i.TokenSalt, - ) - return i, err -} - -const getUserByEmail = `-- name: GetUserByEmail :one -SELECT id, email, password_hash, first_name, last_name, wallet_address, created_at, updated_at, role, email_verified, token_salt FROM users -WHERE email = $1 LIMIT 1 -` - -func (q *Queries) GetUserByEmail(ctx context.Context, email string) (User, error) { - row := q.db.QueryRow(ctx, getUserByEmail, email) - var i User - err := row.Scan( - &i.ID, - &i.Email, - &i.PasswordHash, - &i.FirstName, - &i.LastName, - &i.WalletAddress, - &i.CreatedAt, - &i.UpdatedAt, - &i.Role, - &i.EmailVerified, - &i.TokenSalt, - ) - return i, err -} - -const getUserByID = `-- name: GetUserByID :one -SELECT id, email, password_hash, first_name, last_name, wallet_address, created_at, updated_at, role, email_verified, token_salt FROM users -WHERE id = $1 LIMIT 1 -` - -func (q *Queries) GetUserByID(ctx context.Context, id string) (User, error) { - row := q.db.QueryRow(ctx, getUserByID, id) - var i User - err := row.Scan( - &i.ID, - &i.Email, - &i.PasswordHash, - &i.FirstName, - &i.LastName, - &i.WalletAddress, - &i.CreatedAt, - &i.UpdatedAt, - &i.Role, - &i.EmailVerified, - &i.TokenSalt, - ) - return i, err -} - -const getUserTokenSalt = `-- name: GetUserTokenSalt :one -SELECT token_salt FROM users -WHERE id = $1 -` - -func (q *Queries) GetUserTokenSalt(ctx context.Context, id string) ([]byte, error) { - row := q.db.QueryRow(ctx, getUserTokenSalt, id) - var token_salt []byte - err := row.Scan(&token_salt) - return token_salt, err -} - -const updateUserEmailVerifiedStatus = `-- name: UpdateUserEmailVerifiedStatus :exec -UPDATE users SET email_verified = $1 -WHERE id = $2 -` - -type UpdateUserEmailVerifiedStatusParams struct { - EmailVerified bool - ID string -} - -func (q *Queries) UpdateUserEmailVerifiedStatus(ctx context.Context, arg UpdateUserEmailVerifiedStatusParams) error { - _, err := q.db.Exec(ctx, updateUserEmailVerifiedStatus, arg.EmailVerified, arg.ID) - return err -} - -const updateUserTokenSalt = `-- name: UpdateUserTokenSalt :exec -UPDATE users SET token_salt = gen_random_bytes(32) -WHERE id = $1 -` - -func (q *Queries) UpdateUserTokenSalt(ctx context.Context, id string) error { - _, err := q.db.Exec(ctx, updateUserTokenSalt, id) - return err -} diff --git a/backend/db/verify_email_tokens.sql.go b/backend/db/verify_email_tokens.sql.go deleted file mode 100644 index dcbed72..0000000 --- a/backend/db/verify_email_tokens.sql.go +++ /dev/null @@ -1,74 +0,0 @@ -// Code generated by sqlc. DO NOT EDIT. -// versions: -// sqlc v1.27.0 -// source: verify_email_tokens.sql - -package db - -import ( - "context" - "time" -) - -const createVerifyEmailToken = `-- name: CreateVerifyEmailToken :one -INSERT INTO verify_email_tokens ( - email, - expires_at -) VALUES ( - $1, $2 -) RETURNING id, email, expires_at, created_at -` - -type CreateVerifyEmailTokenParams struct { - Email string - ExpiresAt time.Time -} - -func (q *Queries) CreateVerifyEmailToken(ctx context.Context, arg CreateVerifyEmailTokenParams) (VerifyEmailToken, error) { - row := q.db.QueryRow(ctx, createVerifyEmailToken, arg.Email, arg.ExpiresAt) - var i VerifyEmailToken - err := row.Scan( - &i.ID, - &i.Email, - &i.ExpiresAt, - &i.CreatedAt, - ) - return i, err -} - -const deleteVerifyEmailTokenByEmail = `-- name: DeleteVerifyEmailTokenByEmail :exec -DELETE FROM verify_email_tokens -WHERE email = $1 -` - -func (q *Queries) DeleteVerifyEmailTokenByEmail(ctx context.Context, email string) error { - _, err := q.db.Exec(ctx, deleteVerifyEmailTokenByEmail, email) - return err -} - -const deleteVerifyEmailTokenByID = `-- name: DeleteVerifyEmailTokenByID :exec -DELETE FROM verify_email_tokens -WHERE id = $1 -` - -func (q *Queries) DeleteVerifyEmailTokenByID(ctx context.Context, id string) error { - _, err := q.db.Exec(ctx, deleteVerifyEmailTokenByID, id) - return err -} - -const getVerifyEmailTokenByID = `-- name: GetVerifyEmailTokenByID :one -SELECT id, email, expires_at, created_at FROM verify_email_tokens -WHERE id = $1 LIMIT 1 -` - -func (q *Queries) GetVerifyEmailTokenByID(ctx context.Context, id string) (VerifyEmailToken, error) { - row := q.db.QueryRow(ctx, getVerifyEmailTokenByID, id) - var i VerifyEmailToken - err := row.Scan( - &i.ID, - &i.Email, - &i.ExpiresAt, - &i.CreatedAt, - ) - return i, err -} diff --git a/backend/internal/jwt/jwt_test.go b/backend/internal/jwt/jwt_test.go deleted file mode 100644 index 5b5a263..0000000 --- a/backend/internal/jwt/jwt_test.go +++ /dev/null @@ -1,125 +0,0 @@ -package jwt - -import ( - "os" - "testing" - "time" - - "KonferCA/SPUR/db" - - "github.com/stretchr/testify/assert" -) - -func TestJWT(t *testing.T) { - // setup env - os.Setenv("JWT_SECRET", "secret") - os.Setenv("JWT_SECRET_VERIFY_EMAIL", "test-secret") - - userID := "some-user-id" - role := db.UserRole("user") - salt := []byte("test-salt") - - t.Run("token salt invalidation", func(t *testing.T) { - // Generate initial salt - initialSalt := []byte("initial-salt") - - // Generate tokens with initial salt - accessToken, refreshToken, err := GenerateWithSalt(userID, role, initialSalt) - assert.Nil(t, err) - assert.NotEmpty(t, accessToken) - assert.NotEmpty(t, refreshToken) - - // Verify tokens work with initial salt - claims, err := VerifyTokenWithSalt(accessToken, initialSalt) - assert.Nil(t, err) - assert.Equal(t, claims.UserID, userID) - assert.Equal(t, claims.Role, role) - assert.Equal(t, claims.TokenType, ACCESS_TOKEN_TYPE) - - // Change salt (simulating token invalidation) - newSalt := []byte("new-salt") - - // Old tokens should fail verification with new salt - _, err = VerifyTokenWithSalt(accessToken, newSalt) - assert.NotNil(t, err, "Token should be invalid with new salt") - - // Generate new tokens with new salt - newAccessToken, newRefreshToken, err := GenerateWithSalt(userID, role, newSalt) - assert.Nil(t, err) - assert.NotEmpty(t, newAccessToken) - assert.NotEmpty(t, newRefreshToken) - - // New tokens should work with new salt - claims, err = VerifyTokenWithSalt(newAccessToken, newSalt) - assert.Nil(t, err) - assert.Equal(t, claims.UserID, userID) - }) - - t.Run("two-step verification", func(t *testing.T) { - salt := []byte("test-salt") - - // Generate a token - accessToken, _, err := GenerateWithSalt(userID, role, salt) - assert.Nil(t, err) - - // Step 1: Parse claims without verification - unverifiedClaims, err := ParseUnverifiedClaims(accessToken) - assert.Nil(t, err) - assert.Equal(t, userID, unverifiedClaims.UserID) - - // Step 2: Verify with salt - verifiedClaims, err := VerifyTokenWithSalt(accessToken, salt) - assert.Nil(t, err) - assert.Equal(t, userID, verifiedClaims.UserID) - - // Try to verify with wrong salt - wrongSalt := []byte("wrong-salt") - _, err = VerifyTokenWithSalt(accessToken, wrongSalt) - assert.NotNil(t, err, "Token should be invalid with wrong salt") - }) - - t.Run("generate access token", func(t *testing.T) { - accessToken, _, err := GenerateWithSalt(userID, role, salt) - assert.Nil(t, err) - assert.NotEmpty(t, accessToken) - claims, err := VerifyTokenWithSalt(accessToken, salt) - assert.Nil(t, err) - assert.Equal(t, claims.UserID, userID) - assert.Equal(t, claims.Role, role) - assert.Equal(t, claims.TokenType, ACCESS_TOKEN_TYPE) - }) - - t.Run("generate refresh token", func(t *testing.T) { - _, refreshToken, err := GenerateWithSalt(userID, role, salt) - assert.Nil(t, err) - assert.NotEmpty(t, refreshToken) - claims, err := VerifyTokenWithSalt(refreshToken, salt) - assert.Nil(t, err) - assert.Equal(t, claims.UserID, userID) - assert.Equal(t, claims.Role, role) - assert.Equal(t, claims.TokenType, REFRESH_TOKEN_TYPE) - }) - - t.Run("verify email token", func(t *testing.T) { - email := "test@mail.com" - id := "some-id" - exp := time.Now().Add(time.Second * 5) - token, err := GenerateVerifyEmailToken(email, id, exp) - assert.Nil(t, err) - claims, err := VerifyEmailToken(token) - assert.Nil(t, err) - assert.Equal(t, claims.Email, email) - assert.Equal(t, claims.ID, id) - assert.Equal(t, claims.ExpiresAt.Unix(), exp.Unix()) - }) - - t.Run("deny expired verify email token", func(t *testing.T) { - email := "test@mail.com" - id := "some-id" - exp := time.Now().Add(-1 * 5 * time.Second) - token, err := GenerateVerifyEmailToken(email, id, exp) - assert.Nil(t, err) - _, err = VerifyEmailToken(token) - assert.NotNil(t, err) - }) -} diff --git a/backend/internal/middleware/jwt_test.go b/backend/internal/middleware/jwt_test.go deleted file mode 100644 index adbfb85..0000000 --- a/backend/internal/middleware/jwt_test.go +++ /dev/null @@ -1,132 +0,0 @@ -package middleware - -import ( - "context" - "fmt" - "net/http" - "net/http/httptest" - "os" - "testing" - - "KonferCA/SPUR/db" - "KonferCA/SPUR/internal/jwt" - "github.com/google/uuid" - "github.com/jackc/pgx/v5/pgxpool" - "github.com/labstack/echo/v4" - "github.com/stretchr/testify/assert" -) - -func TestProtectAPIForAccessToken(t *testing.T) { - // setup test environment - os.Setenv("JWT_SECRET", "secret") - - // Connect to test database - ctx := context.Background() - dbURL := fmt.Sprintf("postgres://%s:%s@%s:%s/%s?sslmode=%s", - "postgres", - "postgres", - "localhost", - "5432", - "postgres", - "disable", - ) - - dbPool, err := pgxpool.New(ctx, dbURL) - if err != nil { - t.Fatalf("failed to connect to database: %v", err) - } - defer dbPool.Close() - - // Clean up any existing test user - _, err = dbPool.Exec(ctx, "DELETE FROM users WHERE email = $1", "test@example.com") - if err != nil { - t.Fatalf("failed to clean up test user: %v", err) - } - - // Create a test user directly in the database - userID := uuid.New().String() - _, err = dbPool.Exec(ctx, ` - INSERT INTO users (id, email, password_hash, first_name, last_name, role, token_salt) - VALUES ($1, $2, $3, $4, $5, 'startup_owner', gen_random_bytes(32)) - `, userID, "test@example.com", "hashedpassword", "Test", "User") - if err != nil { - t.Fatalf("failed to create test user: %v", err) - } - - // Create Echo instance with the DB connection - e := echo.New() - queries := db.New(dbPool) - middleware := ProtectAPI(jwt.ACCESS_TOKEN_TYPE, queries) - e.Use(middleware) - - e.GET("/protected", func(c echo.Context) error { - return c.String(http.StatusOK, "protected resource") - }) - - // Get test user data from the database - user, err := queries.GetUserByEmail(ctx, "test@example.com") - if err != nil { - t.Fatalf("failed to get test user: %v", err) - } - - // Get the user's salt - var salt []byte - err = dbPool.QueryRow(ctx, "SELECT token_salt FROM users WHERE id = $1", user.ID).Scan(&salt) - if err != nil { - t.Fatalf("failed to get user salt: %v", err) - } - - // generate valid tokens using the actual salt - accessToken, refreshToken, err := jwt.GenerateWithSalt(user.ID, user.Role, salt) - assert.Nil(t, err) - - tests := []struct { - name string - expectedCode int - token string - }{ - { - name: "Accept access token", - expectedCode: http.StatusOK, - token: accessToken, - }, - { - name: "Reject refresh token", - expectedCode: http.StatusUnauthorized, - token: refreshToken, - }, - { - name: "Reject invalid token format", - expectedCode: http.StatusUnauthorized, - token: "invalid-token", - }, - { - name: "Reject empty token", - expectedCode: http.StatusUnauthorized, - token: "", - }, - { - name: "Reject token with invalid signature", - expectedCode: http.StatusUnauthorized, - token: accessToken + "tampered", - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - req := httptest.NewRequest(http.MethodGet, "/protected", nil) - rec := httptest.NewRecorder() - if test.token != "" { - req.Header.Set(echo.HeaderAuthorization, fmt.Sprintf("Bearer %s", test.token)) - } - e.ServeHTTP(rec, req) - assert.Equal(t, test.expectedCode, rec.Code) - }) - } - - // Clean up test user after test - _, err = dbPool.Exec(ctx, "DELETE FROM users WHERE email = $1", "test@example.com") - if err != nil { - t.Fatalf("failed to clean up test user: %v", err) - } -} \ No newline at end of file diff --git a/backend/internal/middleware/req_validator_test.go b/backend/internal/middleware/req_validator_test.go deleted file mode 100644 index b78d685..0000000 --- a/backend/internal/middleware/req_validator_test.go +++ /dev/null @@ -1,108 +0,0 @@ -package middleware - -import ( - "bytes" - "encoding/json" - "net/http" - "net/http/httptest" - "reflect" - "testing" - - "github.com/labstack/echo/v4" - "github.com/stretchr/testify/assert" -) - -func TestRequestBodyValidator(t *testing.T) { - type testStruct struct { - TestField bool `json:"test_field" validate:"required"` - } - - e := echo.New() - e.Validator = NewRequestBodyValidator() - e.POST("/", func(c echo.Context) error { - // check that the request body is the correct interface - i, ok := c.Get(REQUEST_BODY_KEY).(*testStruct) - if !ok { - return echo.NewHTTPError(http.StatusInternalServerError) - } - - // echo back - return c.JSON(http.StatusOK, i) - }, ValidateRequestBody(reflect.TypeOf(testStruct{}))) - - tests := []struct { - name string - payload interface{} - expectedCode int - }{ - { - name: "Valid request body", - payload: testStruct{ - TestField: true, - }, - expectedCode: http.StatusOK, - }, - { - name: "Invalid request body - validation error", - payload: testStruct{ - // will fail required validation - TestField: false, - }, - // expecting 500 since the middleware its expected to return - // the original ValidationErrors from validator pkg - expectedCode: http.StatusInternalServerError, - }, - { - name: "Empty request body", - payload: nil, - // expecting 500 since the middleware its expected to return - // the original ValidationErrors from validator pkg - expectedCode: http.StatusInternalServerError, - }, - { - name: "Invalid JSON format", - payload: `{ - "test_field": invalid - }`, - expectedCode: http.StatusBadRequest, - }, - { - name: "Wrong type in JSON", - payload: map[string]interface{}{ - "test_field": "not a boolean", - }, - expectedCode: http.StatusBadRequest, - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - var req *http.Request - - if tc.payload != nil { - var payload []byte - var err error - - // handle string payloads (for invalid JSON tests) - if strPayload, ok := tc.payload.(string); ok { - payload = []byte(strPayload) - } else { - payload, err = json.Marshal(tc.payload) - assert.NoError(t, err) - } - - req = httptest.NewRequest(http.MethodPost, "/", bytes.NewReader(payload)) - req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) - } else { - req = httptest.NewRequest(http.MethodPost, "/", nil) - req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) - } - - rec := httptest.NewRecorder() - e.ServeHTTP(rec, req) - - t.Log(rec.Body.String()) - assert.Equal(t, tc.expectedCode, rec.Code) - }) - } -} diff --git a/backend/internal/server/auth.go b/backend/internal/server/auth.go deleted file mode 100644 index db27f21..0000000 --- a/backend/internal/server/auth.go +++ /dev/null @@ -1,406 +0,0 @@ -package server - -import ( - "context" - "net/http" - "reflect" - "time" - - "KonferCA/SPUR/db" - "KonferCA/SPUR/internal/jwt" - mw "KonferCA/SPUR/internal/middleware" - "KonferCA/SPUR/internal/service" - - "github.com/jackc/pgx/v5/pgtype" - "github.com/labstack/echo/v4" - "github.com/rs/zerolog/log" - "golang.org/x/crypto/bcrypt" -) - -type SignupResponse struct { - AccessToken string `json:"access_token"` - User User `json:"user"` -} - -type SigninResponse struct { - AccessToken string `json:"access_token"` - User User `json:"user"` -} - -func (s *Server) setupAuthRoutes() { - auth := s.apiV1.Group("/auth") - auth.Use(s.authLimiter.RateLimit()) // special rate limit for auth routes - auth.POST("/signup", s.handleSignup, mw.ValidateRequestBody(reflect.TypeOf(SignupRequest{}))) - auth.POST("/signin", s.handleSignin, mw.ValidateRequestBody(reflect.TypeOf(SigninRequest{}))) - auth.GET("/verify-email", s.handleVerifyEmail) - auth.GET("/ami-verified", s.handleEmailVerifiedStatus) - auth.POST("/resend-verification", s.handleResendVerificationEmail, mw.ValidateRequestBody(reflect.TypeOf(ResendVerificationEmailRequest{}))) - auth.POST("/refresh", s.handleRefreshToken) - auth.POST("/signout", s.handleSignout) -} - -func (s *Server) handleSignup(c echo.Context) error { - var req *SignupRequest - req, ok := c.Get(mw.REQUEST_BODY_KEY).(*SignupRequest) - if !ok { - return echo.NewHTTPError(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError)) - } - - if err := validateSignupRole(req.Role); err != nil { - return echo.NewHTTPError(http.StatusBadRequest, err.Error()) - } - - // Hash password - hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost) - if err != nil { - return echo.NewHTTPError(http.StatusInternalServerError, "failed to hash password") - } - - ctx := context.Background() - user, err := s.queries.CreateUser(ctx, db.CreateUserParams{ - Email: req.Email, - PasswordHash: string(hashedPassword), - Role: req.Role, - }) - if err != nil { - return echo.NewHTTPError(http.StatusConflict, "email already exists") - } - - accessToken, refreshToken, err := jwt.GenerateWithSalt(user.ID, user.Role, user.TokenSalt) - if err != nil { - return echo.NewHTTPError(http.StatusInternalServerError, "failed to generate tokens") - } - - // Set refresh token as HTTP-only cookie - cookie := new(http.Cookie) - cookie.Name = "refresh_token" - cookie.Value = refreshToken - cookie.HttpOnly = true - cookie.Secure = true // only send over HTTPS - cookie.SameSite = http.SameSiteStrictMode - cookie.Path = "/api/v1/auth" // only accessible by auth endpoints - cookie.MaxAge = 7 * 24 * 60 * 60 // 7 days in seconds - - c.SetCookie(cookie) - - // Send verification email asynchronously - go func() { - ctx, cancel := context.WithTimeout(context.Background(), time.Second*30) - defer cancel() - - exp := time.Now().Add(30 * time.Minute) - - token, err := s.queries.CreateVerifyEmailToken(ctx, db.CreateVerifyEmailTokenParams{ - Email: user.Email, - ExpiresAt: exp, - }) - if err != nil { - log.Error().Err(err).Msg("Failed to create verification token") - return - } - - // Generate JWT for email verification - tokenStr, err := jwt.GenerateVerifyEmailToken(token.Email, token.ID, exp) - if err != nil { - log.Error().Err(err).Msg("Failed to generate verification token") - return - } - - // Send verification email - if err := service.SendVerficationEmail(ctx, user.Email, tokenStr); err != nil { - log.Error().Err(err).Msg("Failed to send verification email") - return - } - - log.Info().Str("token_id", token.ID).Msg("Verification email sent") - }() - - return c.JSON(http.StatusCreated, SignupResponse{ - AccessToken: accessToken, - User: User{ - ID: user.ID, - Email: user.Email, - FirstName: user.FirstName, - LastName: user.LastName, - Role: user.Role, - WalletAddress: user.WalletAddress, - EmailVerified: user.EmailVerified, - }, - }) -} - -func (s *Server) handleSignin(c echo.Context) error { - req, ok := c.Get(mw.REQUEST_BODY_KEY).(*SigninRequest) - if !ok { - return echo.NewHTTPError(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError)) - } - - ctx := context.Background() - user, err := s.queries.GetUserByEmail(ctx, req.Email) - if err != nil { - return echo.NewHTTPError(http.StatusUnauthorized, "invalid credentials") - } - - if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(req.Password)); err != nil { - return echo.NewHTTPError(http.StatusUnauthorized, "invalid credentials") - } - - accessToken, refreshToken, err := jwt.GenerateWithSalt(user.ID, user.Role, user.TokenSalt) - if err != nil { - return echo.NewHTTPError(http.StatusInternalServerError, "failed to generate tokens") - } - - // Set refresh token as HTTP-only cookie - cookie := new(http.Cookie) - cookie.Name = "refresh_token" - cookie.Value = refreshToken - cookie.HttpOnly = true - cookie.Secure = true // only send over HTTPS - cookie.SameSite = http.SameSiteStrictMode - cookie.Path = "/api/v1/auth" // only accessible by auth endpoints - cookie.MaxAge = 7 * 24 * 60 * 60 // 7 days in seconds - - c.SetCookie(cookie) - - return c.JSON(http.StatusOK, SigninResponse{ - AccessToken: accessToken, - User: User{ - ID: user.ID, - Email: user.Email, - FirstName: user.FirstName, - LastName: user.LastName, - Role: user.Role, - WalletAddress: user.WalletAddress, - EmailVerified: user.EmailVerified, - }, - }) -} - -func (s *Server) handleVerifyEmail(c echo.Context) error { - // TODO: the returns should be a view instead of a normal json - // or at least redirect the user to the normal looking page - - tokenStr := c.QueryParam("token") - - if tokenStr == "" { - return echo.NewHTTPError(http.StatusBadRequest, "Missing token in url.") - } - - claims, err := jwt.VerifyEmailToken(tokenStr) - if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, "Invalid token. Please request a new verification email.") - } - - q := db.New(s.DBPool) - - // verify existance in the database - token, err := q.GetVerifyEmailTokenByID(c.Request().Context(), claims.ID) - if err != nil { - log.Error().Err(err).Msg("Failed to fetch verify email token from database.") - return echo.NewHTTPError(http.StatusBadRequest, "Unable to verify email. Please request a new verification email.") - } - - // match token claims email - if token.Email != claims.Email { - return echo.NewHTTPError(http.StatusBadRequest, "Invalid token. Please request a new verification email.") - } - - // find user - user, err := q.GetUserByEmail(c.Request().Context(), token.Email) - if err != nil { - log.Error().Err(err).Msg("Failed to fetch user from database.") - return echo.NewHTTPError(http.StatusBadRequest, "Unable to verify email. Please request a new verification email.") - } - - // begin a transaction to update user's email status and also delete email token. - // make sure that both actions are performed and no ambiguous state remains if something goes wrong. - tx, err := s.DBPool.Begin(c.Request().Context()) - if err != nil { - log.Error().Err(err).Msg("Failed to begin transaction to update user email verification status.") - return echo.NewHTTPError(http.StatusInternalServerError, "Unable to verify email. Please try again later.") - } - defer tx.Rollback(c.Request().Context()) - - qtx := q.WithTx(tx) - err = qtx.UpdateUserEmailVerifiedStatus(c.Request().Context(), db.UpdateUserEmailVerifiedStatusParams{ - EmailVerified: true, - ID: user.ID, - }) - if err != nil { - log.Error().Err(err).Msg("Failed to update user ") - return err - } - - err = qtx.DeleteVerifyEmailTokenByID(c.Request().Context(), token.ID) - if err != nil { - log.Error().Err(err).Msg("Failed to update user ") - return echo.NewHTTPError(http.StatusInternalServerError, "Unable to verify email. Please try again later.") - } - - err = tx.Commit(c.Request().Context()) - if err != nil { - log.Error().Err(err).Msg("Failed to commit transaction to update user email verification status.") - return echo.NewHTTPError(http.StatusInternalServerError, "Unable to verify email. Please try again later.") - } - - return c.JSON(http.StatusOK, map[string]bool{ - "success": true, - }) -} - -func (s *Server) handleResendVerificationEmail(c echo.Context) error { - req, ok := c.Get(mw.REQUEST_BODY_KEY).(*ResendVerificationEmailRequest) - if !ok { - return echo.NewHTTPError(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError)) - } - - ctx := c.Request().Context() - user, err := s.queries.GetUserByEmail(ctx, req.Email) - if err != nil { - return echo.NewHTTPError(http.StatusNotFound, "user not found") - } - - if user.EmailVerified { - return echo.NewHTTPError(http.StatusBadRequest, "email already verified") - } - - err = s.queries.DeleteVerifyEmailTokenByEmail(ctx, req.Email) - if err != nil { - log.Error().Err(err).Str("email", req.Email).Msg("Failed to delete existing verification tokens") - return echo.NewHTTPError(http.StatusInternalServerError, "failed to process request") - } - - exp := time.Now().Add(time.Minute * 30) - token, err := s.queries.CreateVerifyEmailToken(ctx, db.CreateVerifyEmailTokenParams{ - Email: req.Email, - ExpiresAt: exp, - }) - if err != nil { - log.Error().Err(err).Str("email", req.Email).Msg("Failed to verify email token in db") - return echo.NewHTTPError(http.StatusInternalServerError, "failed to create verification token") - } - - tokenStr, err := jwt.GenerateVerifyEmailToken(req.Email, token.ID, exp) - if err != nil { - log.Error().Err(err).Str("email", req.Email).Msg("Failed to generate signed verify email token") - return echo.NewHTTPError(http.StatusInternalServerError, "failed to generate verification token") - } - - err = service.SendVerficationEmail(ctx, req.Email, tokenStr) - if err != nil { - log.Error().Err(err).Str("email", req.Email).Msg("Failed to send verification email") - return echo.NewHTTPError(http.StatusInternalServerError, "failed to send verification email") - } - - return c.JSON(http.StatusOK, map[string]bool{ - "success": true, - }) -} - -/* -handleEmailVerifiedStatus checks for the email_verified column of the given email. -If the email does not exist in the users table, it returns false. The same goes -for any error encountered. -*/ -func (s *Server) handleEmailVerifiedStatus(c echo.Context) error { - email := c.QueryParam("email") - if email == "" { - return echo.NewHTTPError(http.StatusBadRequest, "Missing email in query param.") - } - - user, err := db.New(s.DBPool).GetUserByEmail(c.Request().Context(), email) - if err != nil { - log.Error().Err(err).Msg("Failed to fetch user when checking email verified status.") - return c.JSON(http.StatusOK, EmailVerifiedStatusResponse{Verified: false}) - } - - return c.JSON(http.StatusOK, EmailVerifiedStatusResponse{Verified: user.EmailVerified}) -} - -func (s *Server) handleRefreshToken(c echo.Context) error { - // Get refresh token from HTTP-only cookie - cookie, err := c.Cookie("refresh_token") - if err != nil { - return echo.NewHTTPError(http.StatusUnauthorized, "no refresh token provided") - } - - // Parse claims without verification to get userID - unverifiedClaims, err := jwt.ParseUnverifiedClaims(cookie.Value) - if err != nil { - return echo.NewHTTPError(http.StatusUnauthorized, "invalid token format") - } - - // Get user's salt from database - ctx := context.Background() - salt, err := s.queries.GetUserTokenSalt(ctx, unverifiedClaims.UserID) - if err != nil { - return echo.NewHTTPError(http.StatusUnauthorized, "invalid token") - } - - // Verify the refresh token with user's salt - claims, err := jwt.VerifyTokenWithSalt(cookie.Value, salt) - if err != nil { - return echo.NewHTTPError(http.StatusUnauthorized, "invalid token") - } - - // Verify it's actually a refresh token - if claims.TokenType != jwt.REFRESH_TOKEN_TYPE { - return echo.NewHTTPError(http.StatusUnauthorized, "invalid token type") - } - - // Update user's token salt to invalidate old tokens - if err := s.queries.UpdateUserTokenSalt(ctx, claims.UserID); err != nil { - return echo.NewHTTPError(http.StatusInternalServerError, "failed to rotate token salt") - } - - // Get the new salt - newSalt, err := s.queries.GetUserTokenSalt(ctx, claims.UserID) - if err != nil { - return echo.NewHTTPError(http.StatusInternalServerError, "failed to get new token salt") - } - - // Generate new tokens with the new salt - accessToken, refreshToken, err := jwt.GenerateWithSalt(claims.UserID, claims.Role, newSalt) - if err != nil { - return echo.NewHTTPError(http.StatusInternalServerError, "failed to generate tokens") - } - - // Set new refresh token cookie - refreshCookie := new(http.Cookie) - refreshCookie.Name = "refresh_token" - refreshCookie.Value = refreshToken - refreshCookie.HttpOnly = true - refreshCookie.Secure = true - refreshCookie.SameSite = http.SameSiteStrictMode - refreshCookie.Path = "/api/v1/auth" - refreshCookie.MaxAge = 7 * 24 * 60 * 60 // 7 days - - c.SetCookie(refreshCookie) - - return c.JSON(http.StatusOK, map[string]string{ - "access_token": accessToken, - }) -} - -func (s *Server) handleSignout(c echo.Context) error { - // Create an expired cookie to clear the refresh token - cookie := new(http.Cookie) - cookie.Name = "refresh_token" - cookie.Value = "" - cookie.HttpOnly = true - cookie.Secure = true - cookie.SameSite = http.SameSiteStrictMode - cookie.Path = "/api/v1/auth" - cookie.MaxAge = -1 // immediately expires the cookie - - c.SetCookie(cookie) - return c.NoContent(http.StatusOK) -} - -// helper function to convert pgtype.Text to *string -func getStringPtr(t pgtype.Text) *string { - if !t.Valid { - return nil - } - return &t.String -} diff --git a/backend/internal/server/auth_test.go b/backend/internal/server/auth_test.go deleted file mode 100644 index f6288ad..0000000 --- a/backend/internal/server/auth_test.go +++ /dev/null @@ -1,315 +0,0 @@ -package server - -import ( - "KonferCA/SPUR/db" - "KonferCA/SPUR/internal/jwt" - "bytes" - "context" - "encoding/json" - "fmt" - "net/http" - "net/http/httptest" - "os" - "testing" - "time" - - "github.com/stretchr/testify/assert" -) - -func TestAuth(t *testing.T) { - // setup test environment - os.Setenv("JWT_SECRET", "test-secret") - os.Setenv("DB_HOST", "localhost") - os.Setenv("DB_PORT", "5432") - os.Setenv("DB_USER", "postgres") - os.Setenv("DB_PASSWORD", "postgres") - os.Setenv("DB_NAME", "postgres") - os.Setenv("DB_SSLMODE", "disable") - - // create server - s, err := New(true) - if err != nil { - t.Fatalf("failed to create server: %v", err) - } - defer s.DBPool.Close() - - // clean up database before tests - ctx := context.Background() - _, err = s.DBPool.Exec(ctx, "DELETE FROM users WHERE email = $1", "test@example.com") - if err != nil { - t.Fatalf("failed to clean up database: %v", err) - } - - // test signup - t.Run("signup", func(t *testing.T) { - payload := SignupRequest{ - Email: "test@example.com", - Password: "password123", - Role: "startup_owner", - } - body, _ := json.Marshal(payload) - - req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/signup", bytes.NewReader(body)) - req.Header.Set("Content-Type", "application/json") - rec := httptest.NewRecorder() - - s.echoInstance.ServeHTTP(rec, req) - - assert.Equal(t, http.StatusCreated, rec.Code) - - var response SignupResponse - err := json.NewDecoder(rec.Body).Decode(&response) - assert.NoError(t, err) - assert.NotEmpty(t, response.AccessToken) - assert.Equal(t, payload.Email, response.User.Email) - - // Check refresh token cookie - cookies := rec.Result().Cookies() - var refreshCookie *http.Cookie - for _, cookie := range cookies { - if cookie.Name == "refresh_token" { - refreshCookie = cookie - break - } - } - assert.NotNil(t, refreshCookie, "Refresh token cookie should be set") - assert.True(t, refreshCookie.HttpOnly, "Cookie should be HTTP-only") - }) - - // test duplicate email - t.Run("duplicate email", func(t *testing.T) { - payload := SignupRequest{ - Email: "test@example.com", - Password: "password123", - Role: "startup_owner", - } - body, _ := json.Marshal(payload) - - req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/signup", bytes.NewReader(body)) - req.Header.Set("Content-Type", "application/json") - rec := httptest.NewRecorder() - - s.echoInstance.ServeHTTP(rec, req) - assert.Equal(t, http.StatusConflict, rec.Code) - }) - - // test signin - t.Run("signin", func(t *testing.T) { - payload := SigninRequest{ - Email: "test@example.com", - Password: "password123", - } - body, _ := json.Marshal(payload) - - req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/signin", bytes.NewReader(body)) - req.Header.Set("Content-Type", "application/json") - rec := httptest.NewRecorder() - - s.echoInstance.ServeHTTP(rec, req) - assert.Equal(t, http.StatusOK, rec.Code) - - var response SigninResponse - err := json.Unmarshal(rec.Body.Bytes(), &response) - assert.NoError(t, err) - assert.NotEmpty(t, response.AccessToken) - assert.Equal(t, payload.Email, response.User.Email) - - // Check refresh token cookie - cookies := rec.Result().Cookies() - var refreshCookie *http.Cookie - for _, cookie := range cookies { - if cookie.Name == "refresh_token" { - refreshCookie = cookie - break - } - } - assert.NotNil(t, refreshCookie, "Refresh token cookie should be set") - assert.True(t, refreshCookie.HttpOnly, "Cookie should be HTTP-only") - }) - - // test invalid credentials - t.Run("invalid credentials", func(t *testing.T) { - payload := SigninRequest{ - Email: "test@example.com", - Password: "wrongpassword", - } - body, _ := json.Marshal(payload) - - req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/signin", bytes.NewReader(body)) - req.Header.Set("Content-Type", "application/json") - rec := httptest.NewRecorder() - - s.echoInstance.ServeHTTP(rec, req) - assert.Equal(t, http.StatusUnauthorized, rec.Code) - }) - - t.Run("email verified status", func(t *testing.T) { - req := httptest.NewRequest(http.MethodGet, "/api/v1/auth/ami-verified?email=test@example.com", nil) - rec := httptest.NewRecorder() - s.echoInstance.ServeHTTP(rec, req) - assert.Equal(t, http.StatusOK, rec.Code) - - var response EmailVerifiedStatusResponse - err := json.Unmarshal(rec.Body.Bytes(), &response) - assert.Nil(t, err) - assert.False(t, response.Verified) - }) - - t.Run("verify email", func(t *testing.T) { - // taking a shortcut here - // make use of the already created user before this test - // we are going to directly fetch the user from the database here - // to generate a new email token and verify that email verified is set to true - ctx, cancel := context.WithTimeout(context.Background(), time.Second*30) - defer cancel() - q := db.New(s.DBPool) - - // since the signup test will trigger the creating of a new email token - // when registration sends a verification email, we delete it here - err := q.DeleteVerifyEmailTokenByEmail(ctx, "test@example.com") - assert.Nil(t, err) - - user, err := q.GetUserByEmail(ctx, "test@example.com") - assert.Nil(t, err) - assert.False(t, user.EmailVerified) - - exp := time.Now().Add(time.Second * 30) - token, err := q.CreateVerifyEmailToken(ctx, db.CreateVerifyEmailTokenParams{ - Email: "test@example.com", - ExpiresAt: exp, - }) - assert.Nil(t, err) - tokenStr, err := jwt.GenerateVerifyEmailToken(token.Email, token.ID, exp) - assert.Nil(t, err) - req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/api/v1/auth/verify-email?token=%s", tokenStr), nil) - rec := httptest.NewRecorder() - - s.echoInstance.ServeHTTP(rec, req) - assert.Equal(t, http.StatusOK, rec.Code) - - user, err = q.GetUserByEmail(ctx, "test@example.com") - assert.Nil(t, err) - assert.True(t, user.EmailVerified) - }) - - t.Run("email verified status - true", func(t *testing.T) { - req := httptest.NewRequest(http.MethodGet, "/api/v1/auth/ami-verified?email=test@example.com", nil) - rec := httptest.NewRecorder() - s.echoInstance.ServeHTTP(rec, req) - assert.Equal(t, http.StatusOK, rec.Code) - - var response EmailVerifiedStatusResponse - err := json.Unmarshal(rec.Body.Bytes(), &response) - assert.Nil(t, err) - assert.True(t, response.Verified) - }) - - t.Run("email verified status - missing email query param", func(t *testing.T) { - req := httptest.NewRequest(http.MethodGet, "/api/v1/auth/ami-verified", nil) - rec := httptest.NewRecorder() - s.echoInstance.ServeHTTP(rec, req) - assert.Equal(t, http.StatusBadRequest, rec.Code) - }) - - // test refresh token endpoint - t.Run("refresh token", func(t *testing.T) { - // First sign in to get a refresh token cookie - signinPayload := SigninRequest{ - Email: "test@example.com", - Password: "password123", - } - body, _ := json.Marshal(signinPayload) - - signinReq := httptest.NewRequest(http.MethodPost, "/api/v1/auth/signin", bytes.NewReader(body)) - signinReq.Header.Set("Content-Type", "application/json") - signinRec := httptest.NewRecorder() - - s.echoInstance.ServeHTTP(signinRec, signinReq) - assert.Equal(t, http.StatusOK, signinRec.Code) - - // Get the refresh token cookie - cookies := signinRec.Result().Cookies() - var refreshCookie *http.Cookie - for _, cookie := range cookies { - if cookie.Name == "refresh_token" { - refreshCookie = cookie - break - } - } - assert.NotNil(t, refreshCookie, "Refresh token cookie should be set") - assert.True(t, refreshCookie.HttpOnly, "Cookie should be HTTP-only") - assert.True(t, refreshCookie.Secure, "Cookie should be secure") - assert.Equal(t, http.SameSiteStrictMode, refreshCookie.SameSite, "Cookie should have strict same-site policy") - assert.Equal(t, "/api/v1/auth", refreshCookie.Path, "Cookie should be limited to auth endpoints") - - // Test refresh endpoint - refreshReq := httptest.NewRequest(http.MethodPost, "/api/v1/auth/refresh", nil) - refreshReq.AddCookie(refreshCookie) - refreshRec := httptest.NewRecorder() - - s.echoInstance.ServeHTTP(refreshRec, refreshReq) - assert.Equal(t, http.StatusOK, refreshRec.Code) - - var refreshResponse map[string]string - err := json.NewDecoder(refreshRec.Body).Decode(&refreshResponse) - assert.NoError(t, err) - assert.NotEmpty(t, refreshResponse["access_token"], "Should return new access token") - - // Verify new refresh token cookie is set - newCookies := refreshRec.Result().Cookies() - var newRefreshCookie *http.Cookie - for _, cookie := range newCookies { - if cookie.Name == "refresh_token" { - newRefreshCookie = cookie - break - } - } - assert.NotNil(t, newRefreshCookie, "New refresh token cookie should be set") - assert.NotEqual(t, refreshCookie.Value, newRefreshCookie.Value, "New refresh token should be different") - }) - - // test signout endpoint - t.Run("signout", func(t *testing.T) { - req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/signout", nil) - rec := httptest.NewRecorder() - - s.echoInstance.ServeHTTP(rec, req) - assert.Equal(t, http.StatusOK, rec.Code) - - // Check that refresh token cookie is cleared - cookies := rec.Result().Cookies() - var refreshCookie *http.Cookie - for _, cookie := range cookies { - if cookie.Name == "refresh_token" { - refreshCookie = cookie - break - } - } - assert.NotNil(t, refreshCookie, "Refresh token cookie should be present") - assert.Equal(t, "", refreshCookie.Value, "Cookie value should be empty") - assert.True(t, refreshCookie.MaxAge < 0, "Cookie should be expired") - }) - - // test refresh with invalid token - t.Run("refresh with invalid token", func(t *testing.T) { - invalidCookie := &http.Cookie{ - Name: "refresh_token", - Value: "invalid-token", - } - req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/refresh", nil) - req.AddCookie(invalidCookie) - rec := httptest.NewRecorder() - - s.echoInstance.ServeHTTP(rec, req) - assert.Equal(t, http.StatusUnauthorized, rec.Code) - }) - - // test refresh without token - t.Run("refresh without token", func(t *testing.T) { - req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/refresh", nil) - rec := httptest.NewRecorder() - - s.echoInstance.ServeHTTP(rec, req) - assert.Equal(t, http.StatusUnauthorized, rec.Code) - }) -} diff --git a/backend/internal/server/company.go b/backend/internal/server/company.go deleted file mode 100644 index 1ffc37f..0000000 --- a/backend/internal/server/company.go +++ /dev/null @@ -1,113 +0,0 @@ -package server - -import ( - "net/http" - - "KonferCA/SPUR/db" - "KonferCA/SPUR/internal/jwt" - "KonferCA/SPUR/internal/middleware" - mw "KonferCA/SPUR/internal/middleware" - - "github.com/labstack/echo/v4" - "github.com/rs/zerolog/log" -) - -func (s *Server) handleCreateCompany(c echo.Context) error { - ctx := c.Request().Context() - - var req *CreateCompanyRequest - req, ok := c.Get(mw.REQUEST_BODY_KEY).(*CreateCompanyRequest) - if !ok { - return echo.NewHTTPError(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError)) - } - - ownerUUID, err := validateUUID(req.OwnerUserID, "owner") - if err != nil { - return err - } - - queries := db.New(s.DBPool) - params := db.CreateCompanyParams{ - OwnerUserID: ownerUUID, - Name: req.Name, - Description: req.Description, - } - - company, err := queries.CreateCompany(ctx, params) - if err != nil { - return handleDBError(err, "create", "company") - } - - return c.JSON(http.StatusCreated, company) -} - -func (s *Server) handleGetUserCompany(c echo.Context) error { - ctx := c.Request().Context() - - claims, ok := c.Get(middleware.JWT_CLAIMS).(*jwt.JWTClaims) - if !ok { - return echo.NewHTTPError(http.StatusBadRequest, "Failed to type cast jwt claims") - } - - company, err := s.queries.GetCompanyByUser(ctx, claims.UserID) - if err != nil { - if isNoRowsError(err) { - return echo.NewHTTPError(http.StatusNotFound, "No company found") - } - log.Error().Err(err).Msg("Failed to get company by user") - return echo.NewHTTPError(http.StatusInternalServerError, "Failed to get company") - } - - return c.JSON(http.StatusOK, company) -} - -func (s *Server) handleGetCompany(c echo.Context) error { - ctx := c.Request().Context() - - companyID, err := validateUUID(c.Param("id"), "company") - if err != nil { - return err - } - - queries := db.New(s.DBPool) - company, err := queries.GetCompanyByID(ctx, companyID) - if err != nil { - return handleDBError(err, "fetch", "company") - } - - return c.JSON(http.StatusOK, company) -} - -func (s *Server) handleListCompanies(c echo.Context) error { - ctx := c.Request().Context() - - queries := db.New(s.DBPool) - companies, err := queries.ListCompanies(ctx) - if err != nil { - return handleDBError(err, "fetch", "companies") - } - - return c.JSON(http.StatusOK, companies) -} - -func (s *Server) handleDeleteCompany(c echo.Context) error { - ctx := c.Request().Context() - - companyID, err := validateUUID(c.Param("id"), "company") - if err != nil { - return err - } - - queries := db.New(s.DBPool) - _, err = queries.GetCompanyByID(ctx, companyID) - if err != nil { - return handleDBError(err, "verify", "company") - } - - err = queries.DeleteCompany(ctx, companyID) - if err != nil { - return handleDBError(err, "delete", "company") - } - - return c.NoContent(http.StatusNoContent) -} diff --git a/backend/internal/server/company_documents.go b/backend/internal/server/company_documents.go deleted file mode 100644 index 1e81ffb..0000000 --- a/backend/internal/server/company_documents.go +++ /dev/null @@ -1,148 +0,0 @@ -package server - -import ( - "net/http" - - "KonferCA/SPUR/db" - mw "KonferCA/SPUR/internal/middleware" - - "github.com/labstack/echo/v4" -) - -func (s *Server) handleCreateCompanyDocument(c echo.Context) error { - ctx := c.Request().Context() - - var req *CreateCompanyDocumentRequest - req, ok := c.Get(mw.REQUEST_BODY_KEY).(*CreateCompanyDocumentRequest) - if !ok { - return echo.NewHTTPError(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError)) - } - - companyID, err := validateUUID(req.CompanyID, "company") - if err != nil { - return err - } - - queries := db.New(s.DBPool) - _, err = queries.GetCompanyByID(ctx, companyID) - if err != nil { - return handleDBError(err, "verify", "company") - } - - params := db.CreateCompanyDocumentParams{ - CompanyID: companyID, - DocumentType: req.DocumentType, - FileUrl: req.FileURL, - } - - document, err := queries.CreateCompanyDocument(ctx, params) - if err != nil { - return handleDBError(err, "create", "company document") - } - - return c.JSON(http.StatusCreated, document) -} - -func (s *Server) handleGetCompanyDocument(c echo.Context) error { - ctx := c.Request().Context() - - documentID, err := validateUUID(c.Param("id"), "document") - if err != nil { - return err - } - - queries := db.New(s.DBPool) - document, err := queries.GetCompanyDocumentByID(ctx, documentID) - if err != nil { - return handleDBError(err, "fetch", "company document") - } - - return c.JSON(http.StatusOK, document) -} - -func (s *Server) handleListCompanyDocuments(c echo.Context) error { - ctx := c.Request().Context() - - companyID, err := validateUUID(c.Param("id"), "company") - if err != nil { - return err - } - - queries := db.New(s.DBPool) - documentType := c.QueryParam("document_type") - if documentType != "" { - params := db.ListDocumentsByTypeParams{ - CompanyID: companyID, - DocumentType: documentType, - } - - documents, err := queries.ListDocumentsByType(ctx, params) - if err != nil { - return handleDBError(err, "fetch", "company documents") - } - return c.JSON(http.StatusOK, documents) - } - - documents, err := queries.ListCompanyDocuments(ctx, companyID) - if err != nil { - return handleDBError(err, "fetch", "company documents") - } - - return c.JSON(http.StatusOK, documents) -} - -func (s *Server) handleUpdateCompanyDocument(c echo.Context) error { - ctx := c.Request().Context() - - documentID, err := validateUUID(c.Param("id"), "document") - if err != nil { - return err - } - - var req *UpdateCompanyDocumentRequest - req, ok := c.Get(mw.REQUEST_BODY_KEY).(*UpdateCompanyDocumentRequest) - if !ok { - return echo.NewHTTPError(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError)) - } - - queries := db.New(s.DBPool) - _, err = queries.GetCompanyDocumentByID(ctx, documentID) - if err != nil { - return handleDBError(err, "verify", "company document") - } - - params := db.UpdateCompanyDocumentParams{ - ID: documentID, - DocumentType: req.DocumentType, - FileUrl: req.FileURL, - } - - document, err := queries.UpdateCompanyDocument(ctx, params) - if err != nil { - return handleDBError(err, "update", "company document") - } - - return c.JSON(http.StatusOK, document) -} - -func (s *Server) handleDeleteCompanyDocument(c echo.Context) error { - ctx := c.Request().Context() - - documentID, err := validateUUID(c.Param("id"), "document") - if err != nil { - return err - } - - queries := db.New(s.DBPool) - _, err = queries.GetCompanyDocumentByID(ctx, documentID) - if err != nil { - return handleDBError(err, "verify", "company document") - } - - err = queries.DeleteCompanyDocument(ctx, documentID) - if err != nil { - return handleDBError(err, "delete", "company document") - } - - return c.NoContent(http.StatusNoContent) -} diff --git a/backend/internal/server/company_financials.go b/backend/internal/server/company_financials.go deleted file mode 100644 index 37c5c72..0000000 --- a/backend/internal/server/company_financials.go +++ /dev/null @@ -1,187 +0,0 @@ -package server - -import ( - "net/http" - "strconv" - - "KonferCA/SPUR/db" - mw "KonferCA/SPUR/internal/middleware" - - "github.com/labstack/echo/v4" -) - -func (s *Server) handleCreateCompanyFinancials(c echo.Context) error { - ctx := c.Request().Context() - - var req *CreateCompanyFinancialsRequest - req, ok := c.Get(mw.REQUEST_BODY_KEY).(*CreateCompanyFinancialsRequest) - if !ok { - return echo.NewHTTPError(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError)) - } - - companyID, err := validateUUID(c.Param("id"), "company") - if err != nil { - return err - } - - queries := db.New(s.DBPool) - _, err = queries.GetCompanyByID(ctx, companyID) - if err != nil { - return handleDBError(err, "verify", "company") - } - - params := db.CreateCompanyFinancialsParams{ - CompanyID: companyID, - FinancialYear: req.FinancialYear, - Revenue: numericFromFloat(req.Revenue), - Expenses: numericFromFloat(req.Expenses), - Profit: numericFromFloat(req.Profit), - Sales: numericFromFloat(req.Sales), - AmountRaised: numericFromFloat(req.AmountRaised), - Arr: numericFromFloat(req.ARR), - GrantsReceived: numericFromFloat(req.GrantsReceived), - } - - financials, err := queries.CreateCompanyFinancials(ctx, params) - if err != nil { - return handleDBError(err, "create", "company financials") - } - - return c.JSON(http.StatusCreated, financials) -} - -func (s *Server) handleGetCompanyFinancials(c echo.Context) error { - ctx := c.Request().Context() - - companyID, err := validateUUID(c.Param("id"), "company") - if err != nil { - return err - } - - queries := db.New(s.DBPool) - year := c.QueryParam("year") - if year != "" { - yearInt, err := strconv.ParseInt(year, 10, 32) - if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, "Invalid year format") - } - - params := db.GetCompanyFinancialsByYearParams{ - CompanyID: companyID, - FinancialYear: int32(yearInt), - } - - financials, err := queries.GetCompanyFinancialsByYear(ctx, params) - if err != nil { - return handleDBError(err, "fetch", "company financials") - } - - return c.JSON(http.StatusOK, financials) - } - - financials, err := queries.ListCompanyFinancials(ctx, companyID) - if err != nil { - return handleDBError(err, "fetch", "company financials") - } - - return c.JSON(http.StatusOK, financials) -} - -func (s *Server) handleUpdateCompanyFinancials(c echo.Context) error { - ctx := c.Request().Context() - - var req *CreateCompanyFinancialsRequest - req, ok := c.Get(mw.REQUEST_BODY_KEY).(*CreateCompanyFinancialsRequest) - if !ok { - return echo.NewHTTPError(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError)) - } - - companyID, err := validateUUID(c.Param("id"), "company") - if err != nil { - return err - } - - year := c.QueryParam("year") - if year == "" { - return echo.NewHTTPError(http.StatusBadRequest, "Year parameter is required") - } - - yearInt, err := strconv.ParseInt(year, 10, 32) - if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, "Invalid year format") - } - - queries := db.New(s.DBPool) - _, err = queries.GetCompanyByID(ctx, companyID) - if err != nil { - return handleDBError(err, "verify", "company") - } - - params := db.UpdateCompanyFinancialsParams{ - CompanyID: companyID, - FinancialYear: int32(yearInt), - Revenue: numericFromFloat(req.Revenue), - Expenses: numericFromFloat(req.Expenses), - Profit: numericFromFloat(req.Profit), - Sales: numericFromFloat(req.Sales), - AmountRaised: numericFromFloat(req.AmountRaised), - Arr: numericFromFloat(req.ARR), - GrantsReceived: numericFromFloat(req.GrantsReceived), - } - - financials, err := queries.UpdateCompanyFinancials(ctx, params) - if err != nil { - return handleDBError(err, "update", "company financials") - } - - return c.JSON(http.StatusOK, financials) -} - -func (s *Server) handleDeleteCompanyFinancials(c echo.Context) error { - ctx := c.Request().Context() - - companyID, err := validateUUID(c.Param("id"), "company") - if err != nil { - return err - } - - year := c.QueryParam("year") - if year == "" { - return echo.NewHTTPError(http.StatusBadRequest, "Year parameter is required") - } - - yearInt, err := strconv.ParseInt(year, 10, 32) - if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, "Invalid year format") - } - - queries := db.New(s.DBPool) - params := db.DeleteCompanyFinancialsParams{ - CompanyID: companyID, - FinancialYear: int32(yearInt), - } - - err = queries.DeleteCompanyFinancials(ctx, params) - if err != nil { - return handleDBError(err, "delete", "company financials") - } - - return c.NoContent(http.StatusNoContent) -} - -func (s *Server) handleGetLatestCompanyFinancials(c echo.Context) error { - ctx := c.Request().Context() - - companyID, err := validateUUID(c.Param("id"), "company") - if err != nil { - return err - } - - queries := db.New(s.DBPool) - financials, err := queries.GetLatestCompanyFinancials(ctx, companyID) - if err != nil { - return handleDBError(err, "fetch", "latest company financials") - } - - return c.JSON(http.StatusOK, financials) -} diff --git a/backend/internal/server/company_questions_answers.go b/backend/internal/server/company_questions_answers.go deleted file mode 100644 index 180ac15..0000000 --- a/backend/internal/server/company_questions_answers.go +++ /dev/null @@ -1,239 +0,0 @@ -package server - -import ( - "net/http" - - "KonferCA/SPUR/db" - mw "KonferCA/SPUR/internal/middleware" - - "github.com/labstack/echo/v4" -) - -func (s *Server) handleCreateQuestion(c echo.Context) error { - ctx := c.Request().Context() - - var req *CreateQuestionRequest - req, ok := c.Get(mw.REQUEST_BODY_KEY).(*CreateQuestionRequest) - if !ok { - return echo.NewHTTPError(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError)) - } - - queries := db.New(s.DBPool) - question, err := queries.CreateQuestion(ctx, req.QuestionText) - if err != nil { - return handleDBError(err, "create", "question") - } - - return c.JSON(http.StatusCreated, question) -} - -func (s *Server) handleGetQuestion(c echo.Context) error { - ctx := c.Request().Context() - - questionID, err := validateUUID(c.Param("id"), "question") - if err != nil { - return err - } - - queries := db.New(s.DBPool) - question, err := queries.GetQuestion(ctx, questionID) - if err != nil { - return handleDBError(err, "fetch", "question") - } - - return c.JSON(http.StatusOK, question) -} - -func (s *Server) handleListQuestions(c echo.Context) error { - ctx := c.Request().Context() - - queries := db.New(s.DBPool) - questions, err := queries.ListQuestions(ctx) - if err != nil { - return handleDBError(err, "fetch", "questions") - } - - return c.JSON(http.StatusOK, questions) -} - -func (s *Server) handleCreateCompanyAnswer(c echo.Context) error { - ctx := c.Request().Context() - - companyID, err := validateUUID(c.Param("id"), "company") - if err != nil { - return err - } - - var req *CreateCompanyAnswerRequest - req, ok := c.Get(mw.REQUEST_BODY_KEY).(*CreateCompanyAnswerRequest) - if !ok { - return echo.NewHTTPError(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError)) - } - - questionID, err := validateUUID(req.QuestionID, "question") - if err != nil { - return err - } - - queries := db.New(s.DBPool) - _, err = queries.GetCompanyByID(ctx, companyID) - if err != nil { - return handleDBError(err, "verify", "company") - } - - _, err = queries.GetQuestion(ctx, questionID) - if err != nil { - return handleDBError(err, "verify", "question") - } - - params := db.CreateCompanyAnswerParams{ - CompanyID: companyID, - QuestionID: questionID, - AnswerText: req.AnswerText, - } - - answer, err := queries.CreateCompanyAnswer(ctx, params) - if err != nil { - return handleDBError(err, "create", "company answer") - } - - return c.JSON(http.StatusCreated, answer) -} - -func (s *Server) handleGetCompanyAnswer(c echo.Context) error { - ctx := c.Request().Context() - - companyID, err := validateUUID(c.Param("company_id"), "company") - if err != nil { - return err - } - - questionID, err := validateUUID(c.Param("question_id"), "question") - if err != nil { - return err - } - - queries := db.New(s.DBPool) - params := db.GetCompanyAnswerParams{ - CompanyID: companyID, - QuestionID: questionID, - } - - answer, err := queries.GetCompanyAnswer(ctx, params) - if err != nil { - return handleDBError(err, "fetch", "company answer") - } - - return c.JSON(http.StatusOK, answer) -} - -func (s *Server) handleListCompanyAnswers(c echo.Context) error { - ctx := c.Request().Context() - - companyID, err := validateUUID(c.Param("id"), "company") - if err != nil { - return err - } - - queries := db.New(s.DBPool) - answers, err := queries.ListCompanyAnswers(ctx, companyID) - if err != nil { - return handleDBError(err, "fetch", "company answers") - } - - return c.JSON(http.StatusOK, answers) -} - -func (s *Server) handleUpdateCompanyAnswer(c echo.Context) error { - ctx := c.Request().Context() - - companyID, err := validateUUID(c.Param("company_id"), "company") - if err != nil { - return err - } - - questionID, err := validateUUID(c.Param("question_id"), "question") - if err != nil { - return err - } - - var req *UpdateCompanyAnswerRequest - req, ok := c.Get(mw.REQUEST_BODY_KEY).(*UpdateCompanyAnswerRequest) - if !ok { - return echo.NewHTTPError(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError)) - } - - queries := db.New(s.DBPool) - _, err = queries.GetCompanyAnswer(ctx, db.GetCompanyAnswerParams{ - CompanyID: companyID, - QuestionID: questionID, - }) - if err != nil { - return handleDBError(err, "verify", "company answer") - } - - params := db.UpdateCompanyAnswerParams{ - CompanyID: companyID, - QuestionID: questionID, - AnswerText: req.AnswerText, - } - - answer, err := queries.UpdateCompanyAnswer(ctx, params) - if err != nil { - return handleDBError(err, "update", "company answer") - } - - return c.JSON(http.StatusOK, answer) -} - -func (s *Server) handleDeleteCompanyAnswer(c echo.Context) error { - ctx := c.Request().Context() - - companyID, err := validateUUID(c.Param("company_id"), "company") - if err != nil { - return err - } - - questionID, err := validateUUID(c.Param("question_id"), "question") - if err != nil { - return err - } - - queries := db.New(s.DBPool) - _, err = queries.GetCompanyAnswer(ctx, db.GetCompanyAnswerParams{ - CompanyID: companyID, - QuestionID: questionID, - }) - if err != nil { - return handleDBError(err, "verify", "company answer") - } - - params := db.SoftDeleteCompanyAnswerParams{ - CompanyID: companyID, - QuestionID: questionID, - } - - err = queries.SoftDeleteCompanyAnswer(ctx, params) - if err != nil { - return handleDBError(err, "delete", "company answer") - } - - return c.NoContent(http.StatusNoContent) -} - -func (s *Server) handleDeleteQuestion(c echo.Context) error { - ctx := c.Request().Context() - - questionID, err := validateUUID(c.Param("id"), "question") - if err != nil { - return err - } - - queries := db.New(s.DBPool) - err = queries.SoftDeleteQuestion(ctx, questionID) - if err != nil { - return handleDBError(err, "delete", "question") - } - - return c.NoContent(http.StatusNoContent) -} diff --git a/backend/internal/server/employee.go b/backend/internal/server/employee.go deleted file mode 100644 index ce9332a..0000000 --- a/backend/internal/server/employee.go +++ /dev/null @@ -1,145 +0,0 @@ -package server - -import ( - "context" - "net/http" - - "KonferCA/SPUR/db" - mw "KonferCA/SPUR/internal/middleware" - "github.com/labstack/echo/v4" -) - -func (s *Server) handleCreateEmployee(c echo.Context) error { - var req *CreateEmployeeRequest - req, ok := c.Get(mw.REQUEST_BODY_KEY).(*CreateEmployeeRequest) - if !ok { - return echo.NewHTTPError(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError)) - } - - companyID, err := validateUUID(req.CompanyID, "company") - if err != nil { - return err - } - - queries := db.New(s.DBPool) - - _, err = queries.GetCompanyByID(context.Background(), companyID) - if err != nil { - return handleDBError(err, "verify", "company") - } - - _, err = queries.GetEmployeeByEmail(context.Background(), req.Email) - if err == nil { - return echo.NewHTTPError(http.StatusConflict, "Email already in use") - } - - params := db.CreateEmployeeParams{ - CompanyID: companyID, - Name: req.Name, - Email: req.Email, - Role: req.Role, - Bio: req.Bio, - } - - employee, err := queries.CreateEmployee(context.Background(), params) - if err != nil { - return handleDBError(err, "create", "employee") - } - - return c.JSON(http.StatusCreated, employee) -} - -func (s *Server) handleGetEmployee(c echo.Context) error { - employeeID, err := validateUUID(c.Param("id"), "employee") - if err != nil { - return err - } - - queries := db.New(s.DBPool) - employee, err := queries.GetEmployeeByID(context.Background(), employeeID) - if err != nil { - return handleDBError(err, "fetch", "employee") - } - - return c.JSON(http.StatusOK, employee) -} - -func (s *Server) handleListEmployees(c echo.Context) error { - queries := db.New(s.DBPool) - - companyID := c.QueryParam("company_id") - if companyID != "" { - companyUUID, err := validateUUID(companyID, "company") - if err != nil { - return err - } - - employees, err := queries.ListEmployeesByCompany(context.Background(), companyUUID) - if err != nil { - return handleDBError(err, "fetch", "employees") - } - return c.JSON(http.StatusOK, employees) - } - - employees, err := queries.ListEmployees(context.Background()) - if err != nil { - return handleDBError(err, "fetch", "employees") - } - - return c.JSON(http.StatusOK, employees) -} - -func (s *Server) handleUpdateEmployee(c echo.Context) error { - employeeID, err := validateUUID(c.Param("id"), "employee") - if err != nil { - return err - } - - var req *UpdateEmployeeRequest - req, ok := c.Get(mw.REQUEST_BODY_KEY).(*UpdateEmployeeRequest) - if !ok { - return echo.NewHTTPError(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError)) - } - - queries := db.New(s.DBPool) - - _, err = queries.GetEmployeeByID(context.Background(), employeeID) - if err != nil { - return handleDBError(err, "verify", "employee") - } - - params := db.UpdateEmployeeParams{ - ID: employeeID, - Name: req.Name, - Role: req.Role, - Bio: req.Bio, - } - - employee, err := queries.UpdateEmployee(context.Background(), params) - if err != nil { - return handleDBError(err, "update", "employee") - } - - return c.JSON(http.StatusOK, employee) -} - -func (s *Server) handleDeleteEmployee(c echo.Context) error { - employeeID, err := validateUUID(c.Param("id"), "employee") - if err != nil { - return err - } - - queries := db.New(s.DBPool) - - _, err = queries.GetEmployeeByID(context.Background(), employeeID) - if err != nil { - return handleDBError(err, "verify", "employee") - } - - err = queries.DeleteEmployee(context.Background(), employeeID) - if err != nil { - return handleDBError(err, "delete", "employee") - } - - return c.NoContent(http.StatusNoContent) -} diff --git a/backend/internal/server/error_handler.go b/backend/internal/server/error_handler.go deleted file mode 100644 index 2bd49cd..0000000 --- a/backend/internal/server/error_handler.go +++ /dev/null @@ -1,79 +0,0 @@ -package server - -import ( - "net/http" - - "github.com/go-playground/validator/v10" - "github.com/labstack/echo/v4" - "github.com/rs/zerolog/log" -) - -type ErrorResponse struct { - Status int `json:"status"` - Message string `json:"message"` - RequestID string `json:"request_id,omitempty"` - Errors []string `json:"errors,omitempty"` -} - -func globalErrorHandler(err error, c echo.Context) { - req := c.Request() - requestID := req.Header.Get(echo.HeaderXRequestID) - - // default error response - status := http.StatusInternalServerError - message := "internal server error" - var validationErrors []string - - // handle different error types - switch e := err.(type) { - case *echo.HTTPError: - status = e.Code - message = e.Message.(string) - - case validator.ValidationErrors: - // handle validation errors specially - status = http.StatusBadRequest - message = "validation failed" - validationErrors = make([]string, len(e)) - for i, err := range e { - validationErrors[i] = err.Error() - } - - case error: - message = e.Error() - } - - // log with more context - logger := log.Error() - if status < 500 { - logger = log.Warn() - } - - logger. - Err(err). - Str("request_id", requestID). - Str("method", req.Method). - Str("path", req.URL.Path). - Int("status", status). - Str("user_agent", req.UserAgent()). - Msg("request error") - - // return json response - if !c.Response().Committed { - response := ErrorResponse{ - Status: status, - Message: message, - RequestID: requestID, - } - if len(validationErrors) > 0 { - response.Errors = validationErrors - } - - if err := c.JSON(status, response); err != nil { - log.Error(). - Err(err). - Str("request_id", requestID). - Msg("failed to send error response") - } - } -} diff --git a/backend/internal/server/error_handler_test.go b/backend/internal/server/error_handler_test.go deleted file mode 100644 index 7544de9..0000000 --- a/backend/internal/server/error_handler_test.go +++ /dev/null @@ -1,94 +0,0 @@ -package server - -import ( - "net/http" - "net/http/httptest" - "testing" - - "github.com/go-playground/validator/v10" - "github.com/labstack/echo/v4" - "github.com/stretchr/testify/assert" -) - -func TestGlobalErrorHandler(t *testing.T) { - // setup - e := echo.New() - e.HTTPErrorHandler = globalErrorHandler - - // test cases - tests := []struct { - name string - handler echo.HandlerFunc - expectedStatus int - expectedBody string - }{ - { - name: "http error", - handler: func(c echo.Context) error { - return echo.NewHTTPError(http.StatusBadRequest, "bad request") - }, - expectedStatus: http.StatusBadRequest, - expectedBody: `{"status":400,"message":"bad request"}`, - }, - { - name: "generic error", - handler: func(c echo.Context) error { - return echo.NewHTTPError(http.StatusInternalServerError, "something went wrong") - }, - expectedStatus: http.StatusInternalServerError, - expectedBody: `{"status":500,"message":"something went wrong"}`, - }, - { - name: "validation error", - handler: func(c echo.Context) error { - type TestStruct struct { - Email string `validate:"required,email"` - Age int `validate:"required,gt=0"` - } - - v := validator.New() - err := v.Struct(TestStruct{ - Email: "invalid-email", - Age: -1, - }) - - return err - }, - expectedStatus: http.StatusBadRequest, - expectedBody: `{ - "status": 400, - "message": "validation failed", - "errors": [ - "Key: 'TestStruct.Email' Error:Field validation for 'Email' failed on the 'email' tag", - "Key: 'TestStruct.Age' Error:Field validation for 'Age' failed on the 'gt' tag" - ] - }`, - }, - { - name: "with request id", - handler: func(c echo.Context) error { - c.Request().Header.Set(echo.HeaderXRequestID, "test-123") - return echo.NewHTTPError(http.StatusBadRequest, "bad request") - }, - expectedStatus: http.StatusBadRequest, - expectedBody: `{"status":400,"message":"bad request","request_id":"test-123"}`, - }, - } - - // run tests - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - req := httptest.NewRequest(http.MethodGet, "/", nil) - rec := httptest.NewRecorder() - c := e.NewContext(req, rec) - - err := tt.handler(c) - if err != nil { - e.HTTPErrorHandler(err, c) - } - - assert.Equal(t, tt.expectedStatus, rec.Code) - assert.JSONEq(t, tt.expectedBody, rec.Body.String()) - }) - } -} diff --git a/backend/internal/server/funding_transactions.go b/backend/internal/server/funding_transactions.go deleted file mode 100644 index ab6fad0..0000000 --- a/backend/internal/server/funding_transactions.go +++ /dev/null @@ -1,162 +0,0 @@ -package server - -import ( - "fmt" - "net/http" - - "KonferCA/SPUR/db" - mw "KonferCA/SPUR/internal/middleware" - - "github.com/labstack/echo/v4" -) - -func (s *Server) handleCreateFundingTransaction(c echo.Context) error { - ctx := c.Request().Context() - - var req *CreateFundingTransactionRequest - req, ok := c.Get(mw.REQUEST_BODY_KEY).(*CreateFundingTransactionRequest) - if !ok { - return echo.NewHTTPError(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError)) - } - - projectID, err := validateUUID(req.ProjectID, "project") - if err != nil { - return err - } - - queries := db.New(s.DBPool) - - _, err = queries.GetProject(ctx, projectID) - if err != nil { - return handleDBError(err, "verify", "project") - } - - amount, err := validateNumeric(req.Amount) - if err != nil { - return err - } - - params := db.CreateFundingTransactionParams{ - ProjectID: projectID, - Amount: amount, - Currency: req.Currency, - TransactionHash: req.TransactionHash, - FromWalletAddress: req.FromWalletAddress, - ToWalletAddress: req.ToWalletAddress, - Status: req.Status, - } - - transaction, err := queries.CreateFundingTransaction(ctx, params) - if err != nil { - fmt.Printf("Error creating transaction: %v\n", err) - return handleDBError(err, "create", "funding transaction") - } - - return c.JSON(http.StatusCreated, transaction) -} - -func (s *Server) handleGetFundingTransaction(c echo.Context) error { - ctx := c.Request().Context() - - transactionID, err := validateUUID(c.Param("id"), "transaction") - if err != nil { - return err - } - - queries := db.New(s.DBPool) - transaction, err := queries.GetFundingTransaction(ctx, transactionID) - if err != nil { - if err.Error() == "no rows in result set" { - return echo.NewHTTPError(http.StatusNotFound, "funding transaction not found :(") - } - - return handleDBError(err, "fetch", "funding transaction") - } - - return c.JSON(http.StatusOK, transaction) -} - -func (s *Server) handleListFundingTransactions(c echo.Context) error { - ctx := c.Request().Context() - - queries := db.New(s.DBPool) - projectID := c.QueryParam("project_id") - - if projectID != "" { - projectUUID, err := validateUUID(projectID, "project") - if err != nil { - return err - } - - transactions, err := queries.ListProjectFundingTransactions(ctx, projectUUID) - if err != nil { - return handleDBError(err, "fetch", "funding transactions") - } - - return c.JSON(http.StatusOK, transactions) - } - - transactions, err := queries.ListFundingTransactions(ctx) - if err != nil { - return handleDBError(err, "fetch", "funding transactions") - } - - return c.JSON(http.StatusOK, transactions) -} - -func (s *Server) handleUpdateFundingTransactionStatus(c echo.Context) error { - ctx := c.Request().Context() - - transactionID, err := validateUUID(c.Param("id"), "transaction") - if err != nil { - return err - } - - var req *UpdateFundingTransactionStatusRequest - req, ok := c.Get(mw.REQUEST_BODY_KEY).(*UpdateFundingTransactionStatusRequest) - if !ok { - return echo.NewHTTPError(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError)) - } - - queries := db.New(s.DBPool) - - _, err = queries.GetFundingTransaction(ctx, transactionID) - if err != nil { - return handleDBError(err, "verify", "funding transaction") - } - - params := db.UpdateFundingTransactionStatusParams{ - ID: transactionID, - Status: req.Status, - } - - transaction, err := queries.UpdateFundingTransactionStatus(ctx, params) - if err != nil { - return handleDBError(err, "update", "funding transaction") - } - - return c.JSON(http.StatusOK, transaction) -} - -func (s *Server) handleDeleteFundingTransaction(c echo.Context) error { - ctx := c.Request().Context() - - transactionID, err := validateUUID(c.Param("id"), "transaction") - if err != nil { - return err - } - - queries := db.New(s.DBPool) - - _, err = queries.GetFundingTransaction(ctx, transactionID) - if err != nil { - return handleDBError(err, "verify", "funding transaction") - } - - err = queries.DeleteFundingTransaction(ctx, transactionID) - if err != nil { - return handleDBError(err, "delete", "funding transaction") - } - - return c.NoContent(http.StatusNoContent) -} diff --git a/backend/internal/server/handler_helpers.go b/backend/internal/server/handler_helpers.go deleted file mode 100644 index 3aa53fb..0000000 --- a/backend/internal/server/handler_helpers.go +++ /dev/null @@ -1,111 +0,0 @@ -package server - -import ( - "fmt" - "net/http" - "time" - - "KonferCA/SPUR/db" - "github.com/google/uuid" - "github.com/jackc/pgx/v5/pgtype" - "github.com/labstack/echo/v4" -) - -func validateBody(c echo.Context, requestBodyType interface{}) error { - if err := c.Bind(requestBodyType); err != nil { - return echo.NewHTTPError(http.StatusBadRequest, "Invalid request body :(") - } - if err := c.Validate(requestBodyType); err != nil { - // this will let the global error handler handle - // the ValidationError and get error string for - // the each invalid field. - return err - } - - return nil -} - -func validateUUID(id string, fieldName string) (string, error) { - if id == "" { - return "", echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Missing %s ID :(", fieldName)) - } - if err := uuid.Validate(id); err != nil { - return "", echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid %s ID format :(", fieldName)) - } - - return id, nil -} - -func handleDBError(err error, operation string, resourceType string) error { - if err == nil { - return nil - } - - if isNoRowsError(err) { - return echo.NewHTTPError(http.StatusNotFound, fmt.Sprintf("%s not found :(", resourceType)) - } - - fmt.Printf("Database error during %s %s: %v\n", operation, resourceType, err) - - return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to %s %s :(", operation, resourceType)) -} - -func isNoRowsError(err error) bool { - if err == nil { - return false - } - errMsg := err.Error() - return errMsg == "no rows in result set" || - errMsg == "no rows in dis set" || - errMsg == "scanning empty row" -} - -func numericFromFloat(f float64) pgtype.Numeric { - var num pgtype.Numeric - num.Scan(f) - return num -} - -func validateNumeric(value string) (pgtype.Numeric, error) { - var num pgtype.Numeric - err := num.Scan(value) - if err != nil { - return num, echo.NewHTTPError(http.StatusBadRequest, "Invalid numeric value :(") - } - - return num, nil -} - -func validateTimestamp(timeStr string, fieldName string) (*time.Time, error) { - if timeStr == "" { - return nil, echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Missing %s :(", fieldName)) - } - - parsedTime, err := time.Parse(time.RFC3339, timeStr) - if err != nil { - return nil, echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid %s format :(", fieldName)) - } - - return &parsedTime, nil -} - -func validateTimeRange(startTime, endTime time.Time) error { - if endTime.Before(startTime) { - return echo.NewHTTPError(http.StatusBadRequest, "End time cannot be before start time :(") - } - - return nil -} - -var allowedSignupRoles = map[db.UserRole]bool{ - db.UserRole("startup_owner"): true, - db.UserRole("investor"): true, -} - -func validateSignupRole(role db.UserRole) error { - if !allowedSignupRoles[role] { - return fmt.Errorf("invalid role for signup: %s", role) - } - - return nil -} diff --git a/backend/internal/server/healthcheck.go b/backend/internal/server/healthcheck.go deleted file mode 100644 index 5892a6d..0000000 --- a/backend/internal/server/healthcheck.go +++ /dev/null @@ -1,70 +0,0 @@ -package server - -import ( - "context" - "net/http" - "runtime" - "time" - - "github.com/labstack/echo/v4" -) - -func getSystemInfo() SystemInfo { - var mem runtime.MemStats - runtime.ReadMemStats(&mem) - - return SystemInfo{ - Version: "1.0.0", - GoVersion: runtime.Version(), - NumGoRoutine: runtime.NumGoroutine(), - MemoryUsage: float64(mem.Alloc) / 1024 / 1024, - } -} - -func checkDatabase(s *Server) DatabaseInfo { - info := DatabaseInfo{ - Connected: false, - } - - ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) - defer cancel() - - start := time.Now() - - var version string - err := s.DBPool.QueryRow(ctx, "SELECT version()").Scan(&version) - - latency := time.Since(start) - info.LatencyMs = float64(latency.Microseconds()) / 1000.0 - - if err != nil { - info.Connected = false - info.Error = err.Error() - - return info - } - - info.Connected = true - info.PostgresVersion = version - - return info -} - -func (s *Server) handleHealthCheck(c echo.Context) error { - report := HealthReport{ - Timestamp: time.Now(), - System: getSystemInfo(), - } - - dbInfo := checkDatabase(s) - report.Database = dbInfo - - if dbInfo.Connected { - report.Status = "healthy" - } else { - report.Status = "unhealthy" - return c.JSON(http.StatusServiceUnavailable, report) - } - - return c.JSON(http.StatusOK, report) -} diff --git a/backend/internal/server/healthcheck_test.go b/backend/internal/server/healthcheck_test.go deleted file mode 100644 index f076200..0000000 --- a/backend/internal/server/healthcheck_test.go +++ /dev/null @@ -1,98 +0,0 @@ -package server - -import ( - "encoding/json" - "net/http" - "net/http/httptest" - "os" - "testing" - - "github.com/labstack/echo/v4" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestGetSystemInfo(t *testing.T) { - info := getSystemInfo() - - assert.NotEmpty(t, info.Version) - assert.NotEmpty(t, info.GoVersion) - assert.Greater(t, info.NumGoRoutine, 0) - assert.GreaterOrEqual(t, info.MemoryUsage, 0.0) -} - -func TestHealthCheckHandler(t *testing.T) { - // setup test environment - os.Setenv("DB_HOST", "localhost") - os.Setenv("DB_PORT", "5432") - os.Setenv("DB_USER", "postgres") - os.Setenv("DB_PASSWORD", "postgres") - os.Setenv("DB_NAME", "postgres") - os.Setenv("DB_SSLMODE", "disable") - - // create test server - s, err := New(true) - require.NoError(t, err) - defer s.DBPool.Close() - - tests := []struct { - name string - setupFunc func(*Server) - expectedStatus int - expectedHealth string - }{ - { - name: "healthy_system", - setupFunc: nil, // no special setup needed - expectedStatus: http.StatusOK, - expectedHealth: "healthy", - }, - { - name: "unhealthy_system", - setupFunc: func(s *Server) { - s.DBPool.Close() - }, - expectedStatus: http.StatusServiceUnavailable, - expectedHealth: "unhealthy", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // setup - e := echo.New() - req := httptest.NewRequest(http.MethodGet, "/health", nil) - rec := httptest.NewRecorder() - c := e.NewContext(req, rec) - - if tt.setupFunc != nil { - tt.setupFunc(s) - } - - // test - err := s.handleHealthCheck(c) - require.NoError(t, err) - - // sssertions - assert.Equal(t, tt.expectedStatus, rec.Code) - - var response HealthReport - err = json.Unmarshal(rec.Body.Bytes(), &response) - require.NoError(t, err) - - assert.Equal(t, tt.expectedHealth, response.Status) - assert.NotEmpty(t, response.Timestamp) - assert.NotNil(t, response.System) - assert.NotNil(t, response.Database) - - if tt.expectedHealth == "unhealthy" { - assert.False(t, response.Database.Connected) - assert.NotEmpty(t, response.Database.Error) - } else { - assert.True(t, response.Database.Connected) - assert.NotEmpty(t, response.Database.PostgresVersion) - assert.GreaterOrEqual(t, response.Database.LatencyMs, 0.0) - } - }) - } -} diff --git a/backend/internal/server/meetings.go b/backend/internal/server/meetings.go deleted file mode 100644 index 45acd2d..0000000 --- a/backend/internal/server/meetings.go +++ /dev/null @@ -1,194 +0,0 @@ -package server - -import ( - "net/http" - "time" - - "KonferCA/SPUR/db" - mw "KonferCA/SPUR/internal/middleware" - - "github.com/labstack/echo/v4" -) - -func (s *Server) handleCreateMeeting(c echo.Context) error { - ctx := c.Request().Context() - - var req *CreateMeetingRequest - req, ok := c.Get(mw.REQUEST_BODY_KEY).(*CreateMeetingRequest) - if !ok { - return echo.NewHTTPError(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError)) - } - - projectID, err := validateUUID(req.ProjectID, "project") - if err != nil { - return err - } - - userID, err := validateUUID(req.ScheduledByUserID, "user") - if err != nil { - return err - } - - startTime, err := time.Parse(time.RFC3339, req.StartTime) - if err != nil { - return err - } - - endTime, err := time.Parse(time.RFC3339, req.EndTime) - if err != nil { - return err - } - - if err := validateTimeRange(startTime, endTime); err != nil { - return err - } - - queries := db.New(s.DBPool) - - _, err = queries.GetProject(ctx, projectID) - if err != nil { - return handleDBError(err, "verify", "project") - } - - params := db.CreateMeetingParams{ - ProjectID: projectID, - ScheduledByUserID: userID, - StartTime: startTime, - EndTime: endTime, - MeetingUrl: req.MeetingURL, - Location: req.Location, - Notes: req.Notes, - } - - meeting, err := queries.CreateMeeting(ctx, params) - if err != nil { - return handleDBError(err, "create", "meeting") - } - - return c.JSON(http.StatusCreated, meeting) -} - -func (s *Server) handleGetMeeting(c echo.Context) error { - ctx := c.Request().Context() - - meetingID, err := validateUUID(c.Param("id"), "meeting") - if err != nil { - return err - } - - queries := db.New(s.DBPool) - - meeting, err := queries.GetMeeting(ctx, meetingID) - if err != nil { - if err.Error() == "no rows in result set" { - return echo.NewHTTPError(http.StatusNotFound, "meeting not found :(") - } - - return handleDBError(err, "fetch", "meeting") - } - - return c.JSON(http.StatusOK, meeting) -} - -func (s *Server) handleListMeetings(c echo.Context) error { - ctx := c.Request().Context() - - queries := db.New(s.DBPool) - projectID := c.QueryParam("project_id") - - if projectID != "" { - projectUUID, err := validateUUID(projectID, "project") - if err != nil { - return err - } - - meetings, err := queries.ListProjectMeetings(ctx, projectUUID) - if err != nil { - return handleDBError(err, "fetch", "meetings") - } - - return c.JSON(http.StatusOK, meetings) - } - - meetings, err := queries.ListMeetings(ctx) - if err != nil { - return handleDBError(err, "fetch", "meetings") - } - - return c.JSON(http.StatusOK, meetings) -} - -func (s *Server) handleUpdateMeeting(c echo.Context) error { - ctx := c.Request().Context() - - meetingID, err := validateUUID(c.Param("id"), "meeting") - if err != nil { - return err - } - - var req *UpdateMeetingRequest - req, ok := c.Get(mw.REQUEST_BODY_KEY).(*UpdateMeetingRequest) - if !ok { - return echo.NewHTTPError(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError)) - } - - startTime, err := time.Parse(time.RFC3339, req.StartTime) - if err != nil { - return err - } - - endTime, err := time.Parse(time.RFC3339, req.EndTime) - if err != nil { - return err - } - - if err := validateTimeRange(startTime, endTime); err != nil { - return err - } - - queries := db.New(s.DBPool) - - _, err = queries.GetMeeting(ctx, meetingID) - if err != nil { - return handleDBError(err, "verify", "meeting") - } - - params := db.UpdateMeetingParams{ - ID: meetingID, - StartTime: startTime, - EndTime: endTime, - MeetingUrl: req.MeetingURL, - Location: req.Location, - Notes: req.Notes, - } - - meeting, err := queries.UpdateMeeting(ctx, params) - if err != nil { - return handleDBError(err, "update", "meeting") - } - - return c.JSON(http.StatusOK, meeting) -} - -func (s *Server) handleDeleteMeeting(c echo.Context) error { - ctx := c.Request().Context() - - meetingID, err := validateUUID(c.Param("id"), "meeting") - if err != nil { - return err - } - - queries := db.New(s.DBPool) - - _, err = queries.GetMeeting(ctx, meetingID) - if err != nil { - return handleDBError(err, "verify", "meeting") - } - - err = queries.DeleteMeeting(ctx, meetingID) - if err != nil { - return handleDBError(err, "delete", "meeting") - } - - return c.NoContent(http.StatusNoContent) -} diff --git a/backend/internal/server/project_comment_test.go b/backend/internal/server/project_comment_test.go deleted file mode 100644 index 54b96ff..0000000 --- a/backend/internal/server/project_comment_test.go +++ /dev/null @@ -1,261 +0,0 @@ -package server - -import ( - "bytes" - "context" - "crypto/rand" - "encoding/json" - "net/http" - "net/http/httptest" - "os" - "testing" - - "github.com/google/uuid" - "github.com/stretchr/testify/assert" - "KonferCA/SPUR/internal/jwt" -) - -func TestProjectCommentEndpoints(t *testing.T) { - // setup test environment - os.Setenv("DB_HOST", "localhost") - os.Setenv("DB_PORT", "5432") - os.Setenv("DB_USER", "postgres") - os.Setenv("DB_PASSWORD", "postgres") - os.Setenv("DB_NAME", "postgres") - os.Setenv("DB_SSLMODE", "disable") - - // create server - s, err := New(true) - if err != nil { - t.Fatalf("failed to create server: %v", err) - } - defer s.DBPool.Close() - - // clean up database before tests - ctx := context.Background() - _, err = s.DBPool.Exec(ctx, "DELETE FROM project_comments") - if err != nil { - t.Fatalf("failed to clean up project_comments: %v", err) - } - _, err = s.DBPool.Exec(ctx, "DELETE FROM projects") - if err != nil { - t.Fatalf("failed to clean up projects: %v", err) - } - _, err = s.DBPool.Exec(ctx, "DELETE FROM companies WHERE name = $1", "Test Company") - if err != nil { - t.Fatalf("failed to clean up companies: %v", err) - } - _, err = s.DBPool.Exec(ctx, "DELETE FROM users WHERE email = $1", "test@example.com") - if err != nil { - t.Fatalf("failed to clean up test user: %v", err) - } - - // Create a test user directly in the database - userID := uuid.New().String() - _, err = s.DBPool.Exec(ctx, ` - INSERT INTO users (id, email, password_hash, first_name, last_name, role, token_salt) - VALUES ($1, $2, $3, $4, $5, 'startup_owner', gen_random_bytes(32)) - `, userID, "test@example.com", "hashedpassword", "Test", "User") - if err != nil { - t.Fatalf("failed to create test user: %v", err) - } - - // After creating the test user, generate a JWT token - salt := make([]byte, 32) - _, err = rand.Read(salt) - if err != nil { - t.Fatalf("failed to generate salt: %v", err) - } - - // Update user with salt - _, err = s.DBPool.Exec(ctx, "UPDATE users SET token_salt = $1 WHERE id = $2", salt, userID) - if err != nil { - t.Fatalf("failed to update user salt: %v", err) - } - - // Generate tokens - accessToken, _, err := jwt.GenerateWithSalt(userID, "startup_owner", salt) - if err != nil { - t.Fatalf("failed to generate tokens: %v", err) - } - - // Create a company - description := "Test Company Description" - companyPayload := CreateCompanyRequest{ - OwnerUserID: userID, - Name: "Test Company", - Description: &description, - } - companyBody, _ := json.Marshal(companyPayload) - - req := httptest.NewRequest(http.MethodPost, "/api/v1/companies", bytes.NewReader(companyBody)) - req.Header.Set("Content-Type", "application/json") - req.Header.Set("Authorization", "Bearer "+accessToken) - rec := httptest.NewRecorder() - s.echoInstance.ServeHTTP(rec, req) - - t.Logf("Company creation response: %s", rec.Body.String()) - - var companyResponse map[string]interface{} - err = json.NewDecoder(rec.Body).Decode(&companyResponse) - if !assert.NoError(t, err) { - t.Fatalf("Failed to decode company response: %v", err) - } - - companyID, ok := companyResponse["ID"].(string) - if !assert.True(t, ok, "Company ID should be a string") { - t.Fatalf("Failed to get company ID from response: %v", companyResponse) - } - - // Create a project - projectDescription := "Test Description" - projectPayload := CreateProjectRequest{ - CompanyID: companyID, - Title: "Test Project", - Description: &projectDescription, - Status: "draft", - } - projectBody, _ := json.Marshal(projectPayload) - - req = httptest.NewRequest(http.MethodPost, "/api/v1/projects", bytes.NewReader(projectBody)) - req.Header.Set("Content-Type", "application/json") - req.Header.Set("Authorization", "Bearer "+accessToken) - rec = httptest.NewRecorder() - s.echoInstance.ServeHTTP(rec, req) - - t.Logf("Project creation response: %s", rec.Body.String()) - - var projectResponse map[string]interface{} - err = json.NewDecoder(rec.Body).Decode(&projectResponse) - if !assert.NoError(t, err) { - t.Fatalf("Failed to decode project response: %v", err) - } - - projectID, ok := projectResponse["ID"].(string) - if !assert.True(t, ok, "Project ID should be a string") { - t.Fatalf("Failed to get project ID from response: %v", projectResponse) - } - - // test create comment - t.Run("create comment", func(t *testing.T) { - commentPayload := CreateProjectCommentRequest{ - UserID: userID, - Comment: "This is a test comment", - } - body, _ := json.Marshal(commentPayload) - - req := httptest.NewRequest(http.MethodPost, "/api/v1/projects/"+projectID+"/comments", bytes.NewReader(body)) - req.Header.Set("Content-Type", "application/json") - req.Header.Set("Authorization", "Bearer "+accessToken) - rec := httptest.NewRecorder() - - s.echoInstance.ServeHTTP(rec, req) - t.Logf("Create comment response: %s", rec.Body.String()) - assert.Equal(t, http.StatusCreated, rec.Code) - - var response map[string]interface{} - err := json.NewDecoder(rec.Body).Decode(&response) - assert.NoError(t, err) - assert.Equal(t, projectID, response["ProjectID"]) - assert.Equal(t, commentPayload.Comment, response["Comment"]) - }) - - // test list comments - t.Run("list comments", func(t *testing.T) { - req := httptest.NewRequest(http.MethodGet, "/api/v1/projects/"+projectID+"/comments", nil) - req.Header.Set("Authorization", "Bearer "+accessToken) - rec := httptest.NewRecorder() - - s.echoInstance.ServeHTTP(rec, req) - t.Logf("List comments response: %s", rec.Body.String()) - assert.Equal(t, http.StatusOK, rec.Code) - - var response []map[string]interface{} - err := json.NewDecoder(rec.Body).Decode(&response) - assert.NoError(t, err) - assert.Len(t, response, 1) - assert.Equal(t, "This is a test comment", response[0]["Comment"]) - }) - - // test delete comment - t.Run("delete comment", func(t *testing.T) { - // Get the comment ID from the list response - req := httptest.NewRequest(http.MethodGet, "/api/v1/projects/"+projectID+"/comments", nil) - req.Header.Set("Authorization", "Bearer "+accessToken) - rec := httptest.NewRecorder() - s.echoInstance.ServeHTTP(rec, req) - - var listResponse []map[string]interface{} - err := json.NewDecoder(rec.Body).Decode(&listResponse) - assert.NoError(t, err) - assert.NotEmpty(t, listResponse) - - commentID := listResponse[0]["ID"].(string) - - // Delete the comment - req = httptest.NewRequest(http.MethodDelete, "/api/v1/projects/comments/"+commentID, nil) - req.Header.Set("Authorization", "Bearer "+accessToken) - rec = httptest.NewRecorder() - - s.echoInstance.ServeHTTP(rec, req) - t.Logf("Delete comment response: %s", rec.Body.String()) - assert.Equal(t, http.StatusNoContent, rec.Code) - - // Verify deletion - req = httptest.NewRequest(http.MethodGet, "/api/v1/projects/"+projectID+"/comments", nil) - req.Header.Set("Authorization", "Bearer "+accessToken) - rec = httptest.NewRecorder() - - s.echoInstance.ServeHTTP(rec, req) - t.Logf("List comments after delete response: %s", rec.Body.String()) - assert.Equal(t, http.StatusOK, rec.Code) - - var response []map[string]interface{} - err = json.NewDecoder(rec.Body).Decode(&response) - assert.NoError(t, err) - assert.Len(t, response, 0, "Comment list should be empty after deletion") - }) - - // test error cases - t.Run("create comment with invalid project ID", func(t *testing.T) { - commentPayload := CreateProjectCommentRequest{ - UserID: userID, - Comment: "This is a test comment", - } - body, _ := json.Marshal(commentPayload) - - req := httptest.NewRequest(http.MethodPost, "/api/v1/projects/invalid-uuid/comments", bytes.NewReader(body)) - req.Header.Set("Content-Type", "application/json") - req.Header.Set("Authorization", "Bearer "+accessToken) - rec := httptest.NewRecorder() - - s.echoInstance.ServeHTTP(rec, req) - assert.Equal(t, http.StatusBadRequest, rec.Code) - }) - - t.Run("create comment with invalid user ID", func(t *testing.T) { - commentPayload := CreateProjectCommentRequest{ - UserID: "invalid-uuid", - Comment: "This is a test comment", - } - body, _ := json.Marshal(commentPayload) - - req := httptest.NewRequest(http.MethodPost, "/api/v1/projects/"+projectID+"/comments", bytes.NewReader(body)) - req.Header.Set("Content-Type", "application/json") - req.Header.Set("Authorization", "Bearer "+accessToken) - rec := httptest.NewRecorder() - - s.echoInstance.ServeHTTP(rec, req) - assert.Equal(t, http.StatusBadRequest, rec.Code) - }) - - t.Run("delete non-existent comment", func(t *testing.T) { - nonExistentID := uuid.New().String() - req := httptest.NewRequest(http.MethodDelete, "/api/v1/projects/comments/"+nonExistentID, nil) - req.Header.Set("Authorization", "Bearer "+accessToken) - rec := httptest.NewRecorder() - - s.echoInstance.ServeHTTP(rec, req) - assert.Equal(t, http.StatusNotFound, rec.Code) - }) -} diff --git a/backend/internal/server/project_tag_test.go b/backend/internal/server/project_tag_test.go deleted file mode 100644 index 766adc5..0000000 --- a/backend/internal/server/project_tag_test.go +++ /dev/null @@ -1,246 +0,0 @@ -package server - -import ( - "bytes" - "context" - "crypto/rand" - "encoding/json" - "net/http" - "net/http/httptest" - "os" - "testing" - - "github.com/google/uuid" - "github.com/stretchr/testify/assert" - "KonferCA/SPUR/internal/jwt" -) - -func TestProjectTagEndpoints(t *testing.T) { - // setup test environment - os.Setenv("DB_HOST", "localhost") - os.Setenv("DB_PORT", "5432") - os.Setenv("DB_USER", "postgres") - os.Setenv("DB_PASSWORD", "postgres") - os.Setenv("DB_NAME", "postgres") - os.Setenv("DB_SSLMODE", "disable") - - // create server - s, err := New(true) - if err != nil { - t.Fatalf("failed to create server: %v", err) - } - defer s.DBPool.Close() - - // clean up database before tests - ctx := context.Background() - _, err = s.DBPool.Exec(ctx, "DELETE FROM project_tags") - if err != nil { - t.Fatalf("failed to clean up project_tags: %v", err) - } - _, err = s.DBPool.Exec(ctx, "DELETE FROM projects") - if err != nil { - t.Fatalf("failed to clean up projects: %v", err) - } - _, err = s.DBPool.Exec(ctx, "DELETE FROM tags WHERE name = $1", "test-tag") - if err != nil { - t.Fatalf("failed to clean up tags: %v", err) - } - _, err = s.DBPool.Exec(ctx, "DELETE FROM companies WHERE name = $1", "Test Company") - if err != nil { - t.Fatalf("failed to clean up companies: %v", err) - } - _, err = s.DBPool.Exec(ctx, "DELETE FROM users WHERE email = $1", "test@example.com") - if err != nil { - t.Fatalf("failed to clean up users: %v", err) - } - - // Create a test user directly in the database - userID := uuid.New().String() - _, err = s.DBPool.Exec(ctx, ` - INSERT INTO users (id, email, password_hash, first_name, last_name, role, token_salt) - VALUES ($1, $2, $3, $4, $5, 'startup_owner', gen_random_bytes(32)) - `, userID, "test@example.com", "hashedpassword", "Test", "User") - if err != nil { - t.Fatalf("failed to create test user: %v", err) - } - - // After creating the test user, generate a JWT token - salt := make([]byte, 32) - _, err = rand.Read(salt) - if err != nil { - t.Fatalf("failed to generate salt: %v", err) - } - - // Update user with salt - _, err = s.DBPool.Exec(ctx, "UPDATE users SET token_salt = $1 WHERE id = $2", salt, userID) - if err != nil { - t.Fatalf("failed to update user salt: %v", err) - } - - // Generate tokens - accessToken, _, err := jwt.GenerateWithSalt(userID, "startup_owner", salt) - if err != nil { - t.Fatalf("failed to generate tokens: %v", err) - } - - // First create a company - description := "Test Company Description" - companyPayload := CreateCompanyRequest{ - OwnerUserID: userID, // Use the created user's ID instead of zeros - Name: "Test Company", - Description: &description, - } - companyBody, _ := json.Marshal(companyPayload) - - req := httptest.NewRequest(http.MethodPost, "/api/v1/companies", bytes.NewReader(companyBody)) - req.Header.Set("Content-Type", "application/json") - rec := httptest.NewRecorder() - s.echoInstance.ServeHTTP(rec, req) - - t.Logf("Company creation response: %s", rec.Body.String()) - - var companyResponse map[string]interface{} - err = json.NewDecoder(rec.Body).Decode(&companyResponse) - if !assert.NoError(t, err) { - t.Fatalf("Failed to decode company response: %v", err) - } - - if rec.Code != http.StatusCreated { - t.Fatalf("Failed to create company. Status: %d, Response: %s", rec.Code, rec.Body.String()) - } - - companyID, ok := companyResponse["ID"].(string) - if !assert.True(t, ok, "Company ID should be a string") { - t.Fatalf("Failed to get company ID from response: %v", companyResponse) - } - - // Now create a project - projectDescription := "Test Description" - projectPayload := CreateProjectRequest{ - CompanyID: companyID, - Title: "Test Project", - Description: &projectDescription, - Status: "draft", - } - projectBody, _ := json.Marshal(projectPayload) - - req = httptest.NewRequest(http.MethodPost, "/api/v1/projects", bytes.NewReader(projectBody)) - req.Header.Set("Content-Type", "application/json") - req.Header.Set("Authorization", "Bearer "+accessToken) - rec = httptest.NewRecorder() - s.echoInstance.ServeHTTP(rec, req) - - t.Logf("Project creation response: %s", rec.Body.String()) - - var projectResponse map[string]interface{} - err = json.NewDecoder(rec.Body).Decode(&projectResponse) - if !assert.NoError(t, err) { - t.Fatalf("Failed to decode project response: %v", err) - } - - if rec.Code != http.StatusCreated { - t.Fatalf("Failed to create project. Status: %d, Response: %s", rec.Code, rec.Body.String()) - } - - projectID, ok := projectResponse["ID"].(string) - if !assert.True(t, ok, "Project ID should be a string") { - t.Fatalf("Failed to get project ID from response: %v", projectResponse) - } - - // create a test tag - tagPayload := CreateTagRequest{ - Name: "test-tag", - } - tagBody, _ := json.Marshal(tagPayload) - - req = httptest.NewRequest(http.MethodPost, "/api/v1/tags", bytes.NewReader(tagBody)) - req.Header.Set("Content-Type", "application/json") - rec = httptest.NewRecorder() - s.echoInstance.ServeHTTP(rec, req) - - t.Logf("Tag creation response: %s", rec.Body.String()) - - var tagResponse map[string]interface{} - err = json.NewDecoder(rec.Body).Decode(&tagResponse) - if !assert.NoError(t, err) { - t.Fatalf("Failed to decode tag response: %v", err) - } - - if rec.Code != http.StatusCreated { - t.Fatalf("Failed to create tag. Status: %d, Response: %s", rec.Code, rec.Body.String()) - } - - tagID, ok := tagResponse["ID"].(string) - if !assert.True(t, ok, "Tag ID should be a string") { - t.Fatalf("Failed to get tag ID from response: %v", tagResponse) - } - - // test add project tag - t.Run("add project tag", func(t *testing.T) { - tagPayload := AddProjectTagRequest{ - TagID: tagID, - } - body, _ := json.Marshal(tagPayload) - - req := httptest.NewRequest(http.MethodPost, "/api/v1/projects/"+projectID+"/tags", bytes.NewReader(body)) - req.Header.Set("Content-Type", "application/json") - req.Header.Set("Authorization", "Bearer "+accessToken) - rec := httptest.NewRecorder() - - s.echoInstance.ServeHTTP(rec, req) - t.Logf("Add tag response: %s", rec.Body.String()) - assert.Equal(t, http.StatusCreated, rec.Code) - - var response map[string]interface{} - err := json.NewDecoder(rec.Body).Decode(&response) - assert.NoError(t, err) - assert.Equal(t, projectID, response["ProjectID"]) - assert.Equal(t, tagID, response["TagID"]) - }) - - // test list project tags - t.Run("list project tags", func(t *testing.T) { - req := httptest.NewRequest(http.MethodGet, "/api/v1/projects/"+projectID+"/tags", nil) - req.Header.Set("Authorization", "Bearer "+accessToken) - rec := httptest.NewRecorder() - - s.echoInstance.ServeHTTP(rec, req) - t.Logf("List tags response: %s", rec.Body.String()) - assert.Equal(t, http.StatusOK, rec.Code) - - var response []map[string]interface{} - err := json.NewDecoder(rec.Body).Decode(&response) - assert.NoError(t, err) - assert.Len(t, response, 1) - - // The response contains the project_tag ID, not the tag ID directly - projectTag := response[0] - assert.Equal(t, tagID, projectTag["TagID"], "TagID should match the created tag") - assert.Equal(t, projectID, projectTag["ProjectID"], "ProjectID should match the project") - }) - - // test delete project tag - t.Run("delete project tag", func(t *testing.T) { - req := httptest.NewRequest(http.MethodDelete, "/api/v1/projects/"+projectID+"/tags/"+tagID, nil) - req.Header.Set("Authorization", "Bearer "+accessToken) - rec := httptest.NewRecorder() - - s.echoInstance.ServeHTTP(rec, req) - t.Logf("Delete tag response: %s", rec.Body.String()) - assert.Equal(t, http.StatusNoContent, rec.Code) - - // verify deletion using list endpoint - req = httptest.NewRequest(http.MethodGet, "/api/v1/projects/"+projectID+"/tags", nil) - req.Header.Set("Authorization", "Bearer "+accessToken) - rec = httptest.NewRecorder() - - s.echoInstance.ServeHTTP(rec, req) - t.Logf("List tags after delete response: %s", rec.Body.String()) - assert.Equal(t, http.StatusOK, rec.Code) - - var response []map[string]interface{} - err := json.NewDecoder(rec.Body).Decode(&response) - assert.NoError(t, err) - assert.Len(t, response, 0, "Tag list should be empty after deletion") - }) -} diff --git a/backend/internal/server/projects.go b/backend/internal/server/projects.go deleted file mode 100644 index 9a84119..0000000 --- a/backend/internal/server/projects.go +++ /dev/null @@ -1,652 +0,0 @@ -package server - -import ( - "encoding/json" - "fmt" - "io" - "net/http" - "path/filepath" - "strings" - "time" - - "KonferCA/SPUR/db" - mw "KonferCA/SPUR/internal/middleware" - - "github.com/labstack/echo/v4" -) - -type CreateProjectRequest struct { - CompanyID string `json:"company_id"` - Title string `json:"title"` - Description *string `json:"description"` - Status string `json:"status"` - Files []ProjectFile `json:"files"` - Links []ProjectLink `json:"links"` - Sections []struct { - Title string `json:"title"` - Questions []struct { - Question string `json:"question"` - Answer string `json:"answer"` - } `json:"questions"` - } `json:"sections"` -} - -func (s *Server) handleCreateProject(c echo.Context) error { - ctx := c.Request().Context() - - var req *CreateProjectRequest - req, ok := c.Get(mw.REQUEST_BODY_KEY).(*CreateProjectRequest) - if !ok { - return echo.NewHTTPError(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError)) - } - - companyID, err := validateUUID(req.CompanyID, "company") - if err != nil { - return err - } - - queries := db.New(s.DBPool) - - // Start a transaction - tx, err := s.DBPool.Begin(ctx) - if err != nil { - return handleDBError(err, "begin", "transaction") - } - defer tx.Rollback(ctx) - - // Create project - qtx := queries.WithTx(tx) - params := db.CreateProjectParams{ - CompanyID: companyID, - Title: req.Title, - Description: req.Description, - Status: req.Status, - } - - project, err := qtx.CreateProject(ctx, params) - if err != nil { - return handleDBError(err, "create", "project") - } - - // Create files - for _, file := range req.Files { - fileParams := db.CreateProjectFileParams{ - ProjectID: project.ID, - FileType: file.FileType, - FileUrl: file.FileURL, - } - _, err := qtx.CreateProjectFile(ctx, fileParams) - if err != nil { - return handleDBError(err, "create", "project file") - } - } - - // Create links - for _, link := range req.Links { - linkParams := db.CreateProjectLinkParams{ - ProjectID: project.ID, - LinkType: link.LinkType, - Url: link.URL, - } - _, err := qtx.CreateProjectLink(ctx, linkParams) - if err != nil { - return handleDBError(err, "create", "project link") - } - } - - // Create sections and questions - for _, section := range req.Sections { - sectionParams := db.CreateProjectSectionParams{ - ProjectID: project.ID, - Title: section.Title, - } - - projectSection, err := qtx.CreateProjectSection(ctx, sectionParams) - if err != nil { - return handleDBError(err, "create", "project section") - } - - // Create questions for this section - for _, q := range section.Questions { - questionParams := db.CreateProjectQuestionParams{ - SectionID: projectSection.ID, - QuestionText: q.Question, - AnswerText: q.Answer, - } - - _, err := qtx.CreateProjectQuestion(ctx, questionParams) - if err != nil { - return handleDBError(err, "create", "project question") - } - } - } - - // Commit transaction - if err := tx.Commit(ctx); err != nil { - return handleDBError(err, "commit", "transaction") - } - - return c.JSON(http.StatusCreated, project) -} - -func (s *Server) handleGetProject(c echo.Context) error { - ctx := c.Request().Context() - - projectID, err := validateUUID(c.Param("id"), "project") - if err != nil { - return err - } - - queries := db.New(s.DBPool) - project, err := queries.ListProjectWithDetails(ctx, projectID) - if err != nil { - return handleDBError(err, "fetch", "project") - } - - return c.JSON(http.StatusOK, project) -} - -func (s *Server) handleListProjects(c echo.Context) error { - ctx := c.Request().Context() - - // Get authenticated user from context - user := c.Get("user").(db.User) - fmt.Printf("DEBUG: User accessing projects - ID: %s, Role: %s\n", user.ID, user.Role) - - queries := db.New(s.DBPool) - - // If user is admin, they can see all projects or filter by company - if user.Role == "admin" { - fmt.Println("DEBUG: User is admin, can see all projects") - companyID := c.QueryParam("company_id") - if companyID != "" { - fmt.Printf("DEBUG: Admin filtering by company_id: %s\n", companyID) - companyUUID, err := validateUUID(companyID, "company") - if err != nil { - return err - } - projects, err := queries.ListProjectsByCompany(ctx, companyUUID) - if err != nil { - return handleDBError(err, "fetch", "projects") - } - return c.JSON(http.StatusOK, projects) - } - - projects, err := queries.ListProjects(ctx) - if err != nil { - return handleDBError(err, "fetch", "projects") - } - return c.JSON(http.StatusOK, projects) - } - - // For non-admin users, get their company ID from employee record - employee, err := queries.GetEmployeeByEmail(ctx, user.Email) - if err != nil { - fmt.Printf("DEBUG: Error fetching employee record: %v\n", err) - return handleDBError(err, "fetch", "employee") - } - - fmt.Printf("DEBUG: Regular user, filtering by their company_id: %s\n", employee.CompanyID) - projects, err := queries.ListProjectsByCompany(ctx, employee.CompanyID) - if err != nil { - fmt.Printf("DEBUG: Error fetching projects: %v\n", err) - return handleDBError(err, "fetch", "projects") - } - fmt.Printf("DEBUG: Found %d projects for company %s\n", len(projects), employee.CompanyID) - return c.JSON(http.StatusOK, projects) -} - -func (s *Server) handleDeleteProject(c echo.Context) error { - ctx := c.Request().Context() - - projectID, err := validateUUID(c.Param("id"), "project") - if err != nil { - return err - } - - queries := db.New(s.DBPool) - _, err = queries.GetProject(ctx, projectID) - if err != nil { - return handleDBError(err, "verify", "project") - } - - err = queries.DeleteProject(ctx, projectID) - if err != nil { - return handleDBError(err, "delete", "project") - } - - return c.NoContent(http.StatusNoContent) -} - -func (s *Server) handleCreateProjectFile(c echo.Context) error { - ctx := c.Request().Context() - - projectID, err := validateUUID(c.Param("id"), "project") - if err != nil { - return err - } - - // Get the file from form data - file, err := c.FormFile("file") - if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, "file is required") - } - - fileType := c.FormValue("file_type") - if fileType == "" { - return echo.NewHTTPError(http.StatusBadRequest, "file_type is required") - } - - // Upload file to storage - src, err := file.Open() - if err != nil { - return echo.NewHTTPError(http.StatusInternalServerError, "failed to read file") - } - defer src.Close() - - // Read file contents - fileBytes, err := io.ReadAll(src) - if err != nil { - return echo.NewHTTPError(http.StatusInternalServerError, "failed to read file contents") - } - - // Generate unique file key - fileExt := filepath.Ext(file.Filename) - fileKey := fmt.Sprintf("uploads/%d%s", time.Now().UnixNano(), fileExt) - - fileURL, err := s.Storage.UploadFile(c.Request().Context(), fileKey, fileBytes) - if err != nil { - return echo.NewHTTPError(http.StatusInternalServerError, "failed to upload file") - } - - // Create database record - queries := db.New(s.DBPool) - params := db.CreateProjectFileParams{ - ProjectID: projectID, - FileType: fileType, - FileUrl: fileURL, - } - - projectFile, err := queries.CreateProjectFile(ctx, params) - if err != nil { - // Try to cleanup the uploaded file if database record creation fails - if fileURL != "" { - // Extract key from URL - parts := strings.Split(fileURL, ".amazonaws.com/") - if len(parts) == 2 { - _ = s.Storage.DeleteFile(c.Request().Context(), parts[1]) - } - } - return handleDBError(err, "create", "project file") - } - - return c.JSON(http.StatusCreated, projectFile) -} - -func (s *Server) handleListProjectFiles(c echo.Context) error { - ctx := c.Request().Context() - - projectID, err := validateUUID(c.Param("id"), "project") - if err != nil { - return err - } - - queries := db.New(s.DBPool) - files, err := queries.ListProjectFiles(ctx, projectID) - if err != nil { - return handleDBError(err, "fetch", "project files") - } - - return c.JSON(http.StatusOK, files) -} - -func (s *Server) handleDeleteProjectFile(c echo.Context) error { - ctx := c.Request().Context() - - fileID, err := validateUUID(c.Param("id"), "file") - if err != nil { - return err - } - - queries := db.New(s.DBPool) - err = queries.DeleteProjectFile(ctx, fileID) - if err != nil { - return handleDBError(err, "delete", "project file") - } - - return c.NoContent(http.StatusNoContent) -} - -func (s *Server) handleCreateProjectComment(c echo.Context) error { - ctx := c.Request().Context() - - projectID, err := validateUUID(c.Param("id"), "project") - if err != nil { - return err - } - - var req *CreateProjectCommentRequest - req, ok := c.Get(mw.REQUEST_BODY_KEY).(*CreateProjectCommentRequest) - if !ok { - return echo.NewHTTPError(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError)) - } - - userID, err := validateUUID(req.UserID, "user") - if err != nil { - return err - } - - queries := db.New(s.DBPool) - _, err = queries.GetProject(ctx, projectID) - if err != nil { - return handleDBError(err, "verify", "project") - } - - params := db.CreateProjectCommentParams{ - ProjectID: projectID, - UserID: userID, - Comment: req.Comment, - } - - comment, err := queries.CreateProjectComment(ctx, params) - if err != nil { - return handleDBError(err, "create", "project comment") - } - - return c.JSON(http.StatusCreated, comment) -} - -func (s *Server) handleListProjectComments(c echo.Context) error { - ctx := c.Request().Context() - - projectID, err := validateUUID(c.Param("id"), "project") - if err != nil { - return err - } - - queries := db.New(s.DBPool) - comments, err := queries.GetProjectComments(ctx, projectID) - if err != nil { - return handleDBError(err, "fetch", "project comments") - } - - return c.JSON(http.StatusOK, comments) -} - -func (s *Server) handleDeleteProjectComment(c echo.Context) error { - ctx := c.Request().Context() - - commentID, err := validateUUID(c.Param("id"), "comment") - if err != nil { - return err - } - - queries := db.New(s.DBPool) - - // First check if the comment exists using a direct query - var exists bool - err = s.DBPool.QueryRow(ctx, "SELECT EXISTS(SELECT 1 FROM project_comments WHERE id = $1)", commentID).Scan(&exists) - if err != nil { - return handleDBError(err, "verify", "project comment") - } - if !exists { - return echo.NewHTTPError(http.StatusNotFound, "project comment not found :(") - } - - err = queries.DeleteProjectComment(ctx, commentID) - if err != nil { - return handleDBError(err, "delete", "project comment") - } - - return c.NoContent(http.StatusNoContent) -} - -func (s *Server) handleCreateProjectLink(c echo.Context) error { - ctx := c.Request().Context() - - projectID, err := validateUUID(c.Param("id"), "project") - if err != nil { - return err - } - - var req *CreateProjectLinkRequest - req, ok := c.Get(mw.REQUEST_BODY_KEY).(*CreateProjectLinkRequest) - if !ok { - return echo.NewHTTPError(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError)) - } - - queries := db.New(s.DBPool) - _, err = queries.GetProject(ctx, projectID) - if err != nil { - return handleDBError(err, "verify", "project") - } - - params := db.CreateProjectLinkParams{ - ProjectID: projectID, - LinkType: req.LinkType, - Url: req.URL, - } - - link, err := queries.CreateProjectLink(ctx, params) - if err != nil { - return handleDBError(err, "create", "project link") - } - - return c.JSON(http.StatusCreated, link) -} - -func (s *Server) handleListProjectLinks(c echo.Context) error { - ctx := c.Request().Context() - - projectID, err := validateUUID(c.Param("id"), "project") - if err != nil { - return err - } - - queries := db.New(s.DBPool) - links, err := queries.ListProjectLinks(ctx, projectID) - if err != nil { - return handleDBError(err, "fetch", "project links") - } - - return c.JSON(http.StatusOK, links) -} - -func (s *Server) handleDeleteProjectLink(c echo.Context) error { - ctx := c.Request().Context() - - linkID, err := validateUUID(c.Param("id"), "link") - if err != nil { - return err - } - - queries := db.New(s.DBPool) - err = queries.DeleteProjectLink(ctx, linkID) - if err != nil { - return handleDBError(err, "delete", "project link") - } - - return c.NoContent(http.StatusNoContent) -} - -func (s *Server) handleAddProjectTag(c echo.Context) error { - ctx := c.Request().Context() - - projectID, err := validateUUID(c.Param("id"), "project") - if err != nil { - return err - } - - var req *AddProjectTagRequest - req, ok := c.Get(mw.REQUEST_BODY_KEY).(*AddProjectTagRequest) - if !ok { - return echo.NewHTTPError(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError)) - } - - tagID, err := validateUUID(req.TagID, "tag") - if err != nil { - return err - } - - queries := db.New(s.DBPool) - - _, err = queries.GetProject(ctx, projectID) - if err != nil { - return handleDBError(err, "verify", "project") - } - - params := db.AddProjectTagParams{ - ProjectID: projectID, - TagID: tagID, - } - - projectTag, err := queries.AddProjectTag(ctx, params) - if err != nil { - return handleDBError(err, "create", "project tag") - } - - return c.JSON(http.StatusCreated, projectTag) -} - -func (s *Server) handleListProjectTags(c echo.Context) error { - ctx := c.Request().Context() - - projectID, err := validateUUID(c.Param("id"), "project") - if err != nil { - return err - } - - queries := db.New(s.DBPool) - tags, err := queries.ListProjectTags(ctx, projectID) - if err != nil { - return handleDBError(err, "fetch", "project tags") - } - - return c.JSON(http.StatusOK, tags) -} - -func (s *Server) handleDeleteProjectTag(c echo.Context) error { - ctx := c.Request().Context() - - projectID, err := validateUUID(c.Param("id"), "project") - if err != nil { - return err - } - - tagID, err := validateUUID(c.Param("tag_id"), "tag") - if err != nil { - return err - } - - queries := db.New(s.DBPool) - params := db.DeleteProjectTagParams{ - ProjectID: projectID, - TagID: tagID, - } - - err = queries.DeleteProjectTag(ctx, params) - if err != nil { - return handleDBError(err, "delete", "project tag") - } - - return c.NoContent(http.StatusNoContent) -} - -func (s *Server) handleUpdateProject(c echo.Context) error { - ctx := c.Request().Context() - - projectID, err := validateUUID(c.Param("id"), "project") - if err != nil { - return err - } - - var req *UpdateProjectRequest - req, ok := c.Get(mw.REQUEST_BODY_KEY).(*UpdateProjectRequest) - if !ok { - return echo.NewHTTPError(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError)) - } - - queries := db.New(s.DBPool) - - // Verify project exists - _, err = queries.GetProject(ctx, projectID) - if err != nil { - return handleDBError(err, "verify", "project") - } - - description := req.Description - params := db.UpdateProjectParams{ - ID: projectID, - Title: req.Title, - Description: &description, - Status: req.Status, - } - - project, err := queries.UpdateProject(ctx, params) - if err != nil { - return handleDBError(err, "update", "project") - } - - return c.JSON(http.StatusOK, project) -} - -func (s *Server) handleGetProjectDetails(c echo.Context) error { - ctx := c.Request().Context() - - projectID, err := validateUUID(c.Param("id"), "project") - if err != nil { - return err - } - - queries := db.New(s.DBPool) - project, err := queries.ListProjectWithDetails(ctx, projectID) - if err != nil { - return handleDBError(err, "fetch", "project details") - } - - // decode sections from json - var sections []map[string]interface{} - if project.Sections != nil { - sectionsBytes, ok := project.Sections.([]byte) - if !ok { - return echo.NewHTTPError(http.StatusInternalServerError, "invalid sections format") - } - if err := json.Unmarshal(sectionsBytes, §ions); err != nil { - return echo.NewHTTPError(http.StatusInternalServerError, "failed to parse sections") - } - } - - // decode documents from json - var documents []map[string]interface{} - if project.Documents != nil { - documentsBytes, ok := project.Documents.([]byte) - if !ok { - return echo.NewHTTPError(http.StatusInternalServerError, "invalid documents format") - } - if err := json.Unmarshal(documentsBytes, &documents); err != nil { - return echo.NewHTTPError(http.StatusInternalServerError, "failed to parse documents") - } - } - - // create response with parsed json - response := map[string]interface{}{ - "id": project.ID, - "company_id": project.CompanyID, - "title": project.Title, - "description": project.Description, - "status": project.Status, - "created_at": project.CreatedAt, - "updated_at": project.UpdatedAt, - "company": map[string]interface{}{ - "id": project.CompanyID, - "name": project.CompanyName, - "industry": project.CompanyIndustry, - "founded_date": project.CompanyFoundedDate, - "stage": project.CompanyStage, - }, - "sections": sections, - "documents": documents, - } - - return c.JSON(http.StatusOK, response) -} diff --git a/backend/internal/server/resource_request.go b/backend/internal/server/resource_request.go deleted file mode 100644 index 4e04ed6..0000000 --- a/backend/internal/server/resource_request.go +++ /dev/null @@ -1,152 +0,0 @@ -package server - -import ( - "net/http" - - "KonferCA/SPUR/db" - mw "KonferCA/SPUR/internal/middleware" - - "github.com/labstack/echo/v4" -) - -type UpdateResourceRequestStatusRequest struct { - Status string `json:"status" validate:"required"` -} - -func (s *Server) handleCreateResourceRequest(c echo.Context) error { - ctx := c.Request().Context() - - var req *CreateResourceRequestRequest - req, ok := c.Get(mw.REQUEST_BODY_KEY).(*CreateResourceRequestRequest) - if !ok { - return echo.NewHTTPError(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError)) - } - - companyID, err := validateUUID(req.CompanyID, "company") - if err != nil { - return err - } - - queries := db.New(s.DBPool) - _, err = queries.GetCompanyByID(ctx, companyID) - if err != nil { - return handleDBError(err, "verify", "company") - } - - params := db.CreateResourceRequestParams{ - CompanyID: companyID, - ResourceType: req.ResourceType, - Description: req.Description, - Status: req.Status, - } - - request, err := queries.CreateResourceRequest(ctx, params) - if err != nil { - return handleDBError(err, "create", "resource request") - } - - return c.JSON(http.StatusCreated, request) -} - -func (s *Server) handleGetResourceRequest(c echo.Context) error { - ctx := c.Request().Context() - - requestID, err := validateUUID(c.Param("id"), "resource request") - if err != nil { - return err - } - - queries := db.New(s.DBPool) - request, err := queries.GetResourceRequestByID(ctx, requestID) - if err != nil { - return handleDBError(err, "fetch", "resource request") - } - - return c.JSON(http.StatusOK, request) -} - -func (s *Server) handleListResourceRequests(c echo.Context) error { - ctx := c.Request().Context() - - companyID := c.QueryParam("company_id") - queries := db.New(s.DBPool) - - if companyID != "" { - companyUUID, err := validateUUID(companyID, "company") - if err != nil { - return err - } - - _, err = queries.GetCompanyByID(ctx, companyUUID) - if err != nil { - return handleDBError(err, "verify", "company") - } - - requests, err := queries.ListResourceRequestsByCompany(ctx, companyUUID) - if err != nil { - return handleDBError(err, "fetch", "resource requests") - } - - return c.JSON(http.StatusOK, requests) - } - - requests, err := queries.ListResourceRequests(ctx) - if err != nil { - return handleDBError(err, "fetch", "resource requests") - } - - return c.JSON(http.StatusOK, requests) -} - -func (s *Server) handleUpdateResourceRequestStatus(c echo.Context) error { - ctx := c.Request().Context() - - requestID, err := validateUUID(c.Param("id"), "resource request") - if err != nil { - return err - } - - var req *UpdateResourceRequestStatusRequest - req, ok := c.Get(mw.REQUEST_BODY_KEY).(*UpdateResourceRequestStatusRequest) - if !ok { - return echo.NewHTTPError(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError)) - } - - queries := db.New(s.DBPool) - _, err = queries.GetResourceRequestByID(ctx, requestID) - if err != nil { - return handleDBError(err, "verify", "resource request") - } - - request, err := queries.UpdateResourceRequestStatus(ctx, db.UpdateResourceRequestStatusParams{ - ID: requestID, - Status: req.Status, - }) - if err != nil { - return handleDBError(err, "update", "resource request status") - } - - return c.JSON(http.StatusOK, request) -} - -func (s *Server) handleDeleteResourceRequest(c echo.Context) error { - ctx := c.Request().Context() - - requestID, err := validateUUID(c.Param("id"), "resource request") - if err != nil { - return err - } - - queries := db.New(s.DBPool) - _, err = queries.GetResourceRequestByID(ctx, requestID) - if err != nil { - return handleDBError(err, "verify", "resource request") - } - - err = queries.DeleteResourceRequest(ctx, requestID) - if err != nil { - handleDBError(err, "delete", "resource request") - } - - return c.NoContent(http.StatusNoContent) -} diff --git a/backend/internal/server/startup.go b/backend/internal/server/startup.go deleted file mode 100644 index 968eb94..0000000 --- a/backend/internal/server/startup.go +++ /dev/null @@ -1,50 +0,0 @@ -package server - -import ( - "context" - "net/http" - - "KonferCA/SPUR/db" - "github.com/google/uuid" - "github.com/labstack/echo/v4" -) - -func (s *Server) handleCreateStartup(c echo.Context) error { - var req CreateCompanyRequest - if err := c.Bind(&req); err != nil { - return echo.NewHTTPError(http.StatusBadRequest, "Invalid request body :(") - } - - if err := c.Validate(req); err != nil { - return echo.NewHTTPError(http.StatusBadRequest, err.Error()) - } - - if err := uuid.Validate(req.OwnerUserID); err != nil { - return echo.NewHTTPError(http.StatusBadRequest, "invalid owner ID format") - } - - queries := db.New(s.DBPool) - params := db.CreateCompanyParams{ - OwnerUserID: req.OwnerUserID, - Name: req.Name, - Description: req.Description, - } - - company, err := queries.CreateCompany(context.Background(), params) - if err != nil { - return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create company :(") - } - - return c.JSON(http.StatusCreated, company) -} - -func (s *Server) handleGetStartup(c echo.Context) error { - queries := db.New(s.DBPool) - - companies, err := queries.ListCompanies(context.Background()) - if err != nil { - return echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch companies :(") - } - - return c.JSON(http.StatusOK, companies) -} diff --git a/backend/internal/server/static.go b/backend/internal/server/static.go deleted file mode 100644 index a2673d6..0000000 --- a/backend/internal/server/static.go +++ /dev/null @@ -1,69 +0,0 @@ -package server - -import ( - "mime" - "net/http" - "net/url" - "path/filepath" - "strings" - - "github.com/labstack/echo/v4" - "github.com/labstack/echo/v4/middleware" -) - -func (s *Server) setupStaticRoutes() { - // add mime types - mime.AddExtensionType(".js", "application/javascript") - mime.AddExtensionType(".css", "text/css") - mime.AddExtensionType(".html", "text/html") - - // hardcode static directory - staticDir := "static/dist" - - // serve static files, excluding API routes - s.echoInstance.Use(middleware.StaticWithConfig(middleware.StaticConfig{ - Root: staticDir, - Index: "index.html", - HTML5: true, - Browse: false, - IgnoreBase: true, - Skipper: func(c echo.Context) bool { - // Skip static file handling for API routes - return strings.HasPrefix(c.Request().URL.Path, "/api/") - }, - })) - - // serve assets with correct mime types - s.echoInstance.GET("/assets/*", func(c echo.Context) error { - // url decode and clean the path - requestedPath, err := url.QueryUnescape(c.Param("*")) - if err != nil { - return echo.NewHTTPError(http.StatusForbidden, "invalid path") - } - - requestedPath = filepath.Clean(requestedPath) - if strings.Contains(requestedPath, "..") { - return echo.NewHTTPError(http.StatusForbidden, "invalid path") - } - - // create a safe path within static directory - path := filepath.Join(staticDir, "assets", requestedPath) - - // verify the final path is still within the static directory - absStaticDir, _ := filepath.Abs(staticDir) - absPath, _ := filepath.Abs(path) - if !strings.HasPrefix(absPath, absStaticDir) { - return echo.NewHTTPError(http.StatusForbidden, "invalid path") - } - - return c.File(path) - }) - - // catch all route - s.echoInstance.GET("/*", func(c echo.Context) error { - if strings.HasPrefix(c.Path(), "/api") { - return echo.NotFoundHandler(c) - } - return c.File(filepath.Join(staticDir, "index.html")) - }) -} diff --git a/backend/internal/server/static_test.go b/backend/internal/server/static_test.go deleted file mode 100644 index 731ef46..0000000 --- a/backend/internal/server/static_test.go +++ /dev/null @@ -1,69 +0,0 @@ -package server - -import ( - "net/http" - "net/http/httptest" - "os" - "path/filepath" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestStaticFileServing(t *testing.T) { - // setup test directory structure - staticDir := "static/dist" - assetsDir := filepath.Join(staticDir, "assets", "img") - err := os.MkdirAll(assetsDir, 0755) - assert.NoError(t, err) - defer os.RemoveAll("static") // cleanup after test - - // create a test file - testFile := filepath.Join(assetsDir, "logo.png") - err = os.WriteFile(testFile, []byte("test content"), 0644) - assert.NoError(t, err) - - // create index.html - err = os.WriteFile(filepath.Join(staticDir, "index.html"), []byte("test"), 0644) - assert.NoError(t, err) - - // setup test server - s, err := New(true) - assert.NoError(t, err) - - tests := []struct { - name string - path string - expectedCode int - }{ - { - name: "normal asset path", - path: "/assets/img/logo.png", - expectedCode: http.StatusOK, - }, - { - name: "path traversal attempt 1", - path: "/assets/../../../etc/passwd", - expectedCode: http.StatusForbidden, - }, - { - name: "path traversal attempt 2", - path: "/assets/%2e%2e%2f%2e%2e%2f%2e%2e%2fetc%2fpasswd", - expectedCode: http.StatusForbidden, - }, - { - name: "path traversal attempt 3", - path: "/assets/..%2f..%2f..%2fetc%2fpasswd", - expectedCode: http.StatusForbidden, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - req := httptest.NewRequest(http.MethodGet, tt.path, nil) - rec := httptest.NewRecorder() - s.echoInstance.ServeHTTP(rec, req) - assert.Equal(t, tt.expectedCode, rec.Code) - }) - } -} diff --git a/backend/internal/server/storage.go b/backend/internal/server/storage.go deleted file mode 100644 index 2416105..0000000 --- a/backend/internal/server/storage.go +++ /dev/null @@ -1,102 +0,0 @@ -package server - -import ( - "context" - "errors" - "fmt" - "net/http" - "os" - "path/filepath" - "time" - - "github.com/aws/aws-sdk-go-v2/aws" - "github.com/aws/aws-sdk-go-v2/config" - "github.com/aws/aws-sdk-go-v2/service/s3" - "github.com/aws/smithy-go" - "github.com/labstack/echo/v4" -) - -type UploadResponse struct { - FileURL string `json:"file_url"` -} - -func (s *Server) setupStorageRoutes() { - fmt.Println("Setting up storage routes...") - s.apiV1.POST("/storage/upload", s.handleFileUpload) - fmt.Println("Storage routes set up") -} - -func (s *Server) handleFileUpload(c echo.Context) error { - fmt.Println("Handling file upload...") - fmt.Printf("Content-Type: %s\n", c.Request().Header.Get("Content-Type")) - - // Parse multipart form with 10MB limit - err := c.Request().ParseMultipartForm(10 << 20) - if err != nil { - fmt.Printf("Error parsing multipart form: %v\n", err) - return echo.NewHTTPError(http.StatusBadRequest, "Failed to parse multipart form") - } - - // Get file from request - file, err := c.FormFile("file") - if err != nil { - fmt.Printf("Error getting form file: %v\n", err) - return echo.NewHTTPError(http.StatusBadRequest, "Invalid file upload") - } - - // Open the uploaded file - src, err := file.Open() - if err != nil { - fmt.Printf("Error opening file: %v\n", err) - return echo.NewHTTPError(http.StatusInternalServerError, "Failed to read uploaded file") - } - defer src.Close() - - // Load AWS configuration - cfg, err := config.LoadDefaultConfig(context.TODO(), - config.WithRegion("us-east-1"), - config.WithClientLogMode(aws.LogRequestWithBody|aws.LogResponseWithBody), - ) - if err != nil { - fmt.Printf("Error loading AWS config: %v\n", err) - return echo.NewHTTPError(http.StatusInternalServerError, "Failed to load AWS config") - } - - // Create S3 client with custom endpoint - bucket := os.Getenv("AWS_S3_BUCKET") - s3Client := s3.NewFromConfig(cfg, func(o *s3.Options) { - o.UsePathStyle = true - o.EndpointResolver = s3.EndpointResolverFromURL(fmt.Sprintf("https://%s.s3.us-east-1.amazonaws.com", bucket)) - - }) - - // Generate unique file key - fileExt := filepath.Ext(file.Filename) - fileKey := fmt.Sprintf("uploads/%d%s", time.Now().UnixNano(), fileExt) - - // Upload to S3 - _, err = s3Client.PutObject(context.TODO(), &s3.PutObjectInput{ - Bucket: aws.String(bucket), - Key: aws.String(fileKey), - Body: src, - ContentType: aws.String(file.Header.Get("Content-Type")), - }) - if err != nil { - var apiErr smithy.APIError - if errors.As(err, &apiErr) { - fmt.Printf("Response error details: %+v\n", apiErr) - } - fmt.Printf("Error uploading to S3: %v\n", err) - return echo.NewHTTPError(http.StatusInternalServerError, "Failed to upload to S3") - } - - // Construct the file URL - fileURL := fmt.Sprintf("https://%s.s3.us-east-1.amazonaws.com/%s", - bucket, - fileKey) - - fmt.Printf("File uploaded successfully: %s\n", fileURL) - return c.JSON(http.StatusOK, UploadResponse{ - FileURL: fileURL, - }) -} \ No newline at end of file diff --git a/backend/internal/server/tag.go b/backend/internal/server/tag.go deleted file mode 100644 index 13f11a0..0000000 --- a/backend/internal/server/tag.go +++ /dev/null @@ -1,74 +0,0 @@ -package server - -import ( - "net/http" - - "KonferCA/SPUR/db" - mw "KonferCA/SPUR/internal/middleware" - - "github.com/labstack/echo/v4" -) - -func (s *Server) handleCreateTag(c echo.Context) error { - ctx := c.Request().Context() - - var req *CreateTagRequest - req, ok := c.Get(mw.REQUEST_BODY_KEY).(*CreateTagRequest) - if !ok { - return echo.NewHTTPError(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError)) - } - - queries := db.New(s.DBPool) - tag, err := queries.CreateTag(ctx, req.Name) - if err != nil { - return handleDBError(err, "create", "tag") - } - - return c.JSON(http.StatusCreated, tag) -} - -func (s *Server) handleGetTag(c echo.Context) error { - ctx := c.Request().Context() - - tagID, err := validateUUID(c.Param("id"), "tag") - if err != nil { - return err - } - - queries := db.New(s.DBPool) - tag, err := queries.GetTag(ctx, tagID) - if err != nil { - return handleDBError(err, "fetch", "tag") - } - - return c.JSON(http.StatusOK, tag) -} - -func (s *Server) handleListTags(c echo.Context) error { - ctx := c.Request().Context() - - queries := db.New(s.DBPool) - tags, err := queries.ListTags(ctx) - if err != nil { - return handleDBError(err, "fetch", "tags") - } - - return c.JSON(http.StatusOK, tags) -} - -func (s *Server) handleDeleteTag(c echo.Context) error { - ctx := c.Request().Context() - - tagID, err := validateUUID(c.Param("id"), "tag") - if err != nil { - return err - } - - queries := db.New(s.DBPool) - err = queries.DeleteTag(ctx, tagID) - if err != nil { - return handleDBError(err, "delete", "tag") - } - - return c.NoContent(http.StatusNoContent) -} diff --git a/backend/internal/server/tag_test.go b/backend/internal/server/tag_test.go deleted file mode 100644 index 4fb72c7..0000000 --- a/backend/internal/server/tag_test.go +++ /dev/null @@ -1,130 +0,0 @@ -package server - -import ( - "bytes" - "context" - "encoding/json" - "net/http" - "net/http/httptest" - "os" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestTagEndpoints(t *testing.T) { - // setup test environment - os.Setenv("DB_HOST", "localhost") - os.Setenv("DB_PORT", "5432") - os.Setenv("DB_USER", "postgres") - os.Setenv("DB_PASSWORD", "postgres") - os.Setenv("DB_NAME", "postgres") - os.Setenv("DB_SSLMODE", "disable") - - // create server - s, err := New(true) - if err != nil { - t.Fatalf("failed to create server: %v", err) - } - defer s.DBPool.Close() - - // clean up database before tests - ctx := context.Background() - _, err = s.DBPool.Exec(ctx, "DELETE FROM tags WHERE name = $1", "test-tag") - if err != nil { - t.Fatalf("failed to clean up database: %v", err) - } - - // test create tag - t.Run("create tag", func(t *testing.T) { - payload := CreateTagRequest{ - Name: "test-tag", - } - body, _ := json.Marshal(payload) - - req := httptest.NewRequest(http.MethodPost, "/api/v1/tags", bytes.NewReader(body)) - req.Header.Set("Content-Type", "application/json") - rec := httptest.NewRecorder() - - s.echoInstance.ServeHTTP(rec, req) - assert.Equal(t, http.StatusCreated, rec.Code) - - var response map[string]interface{} - err := json.NewDecoder(rec.Body).Decode(&response) - assert.NoError(t, err) - assert.Contains(t, response, "ID") - assert.Contains(t, response, "Name") - - tagID := response["ID"].(string) - assert.NotEmpty(t, tagID) - assert.Equal(t, payload.Name, response["Name"]) - - // test get tag - t.Run("get tag", func(t *testing.T) { - req := httptest.NewRequest(http.MethodGet, "/api/v1/tags/"+tagID, nil) - rec := httptest.NewRecorder() - - s.echoInstance.ServeHTTP(rec, req) - assert.Equal(t, http.StatusOK, rec.Code) - - var getResponse map[string]interface{} - err := json.NewDecoder(rec.Body).Decode(&getResponse) - assert.NoError(t, err) - assert.Equal(t, tagID, getResponse["ID"]) - assert.Equal(t, payload.Name, getResponse["Name"]) - }) - - // test list tags - t.Run("list tags", func(t *testing.T) { - req := httptest.NewRequest(http.MethodGet, "/api/v1/tags", nil) - rec := httptest.NewRecorder() - - s.echoInstance.ServeHTTP(rec, req) - assert.Equal(t, http.StatusOK, rec.Code) - - var listResponse []map[string]interface{} - err := json.NewDecoder(rec.Body).Decode(&listResponse) - assert.NoError(t, err) - assert.NotEmpty(t, listResponse) - }) - - // test delete tag - t.Run("delete tag", func(t *testing.T) { - req := httptest.NewRequest(http.MethodDelete, "/api/v1/tags/"+tagID, nil) - rec := httptest.NewRecorder() - - s.echoInstance.ServeHTTP(rec, req) - assert.Equal(t, http.StatusNoContent, rec.Code) - - // verify deletion using database query - var count int - err := s.DBPool.QueryRow(context.Background(), "SELECT COUNT(*) FROM tags WHERE id = $1", tagID).Scan(&count) - assert.NoError(t, err) - assert.Equal(t, 0, count, "Tag should be deleted from database") - }) - }) - - // test validation errors - t.Run("validation errors", func(t *testing.T) { - payload := CreateTagRequest{ - Name: "", // empty name should fail validation - } - body, _ := json.Marshal(payload) - - req := httptest.NewRequest(http.MethodPost, "/api/v1/tags", bytes.NewReader(body)) - req.Header.Set("Content-Type", "application/json") - rec := httptest.NewRecorder() - - s.echoInstance.ServeHTTP(rec, req) - assert.Equal(t, http.StatusBadRequest, rec.Code) - }) - - // test invalid uuid - t.Run("invalid uuid", func(t *testing.T) { - req := httptest.NewRequest(http.MethodGet, "/api/v1/tags/invalid-uuid", nil) - rec := httptest.NewRecorder() - - s.echoInstance.ServeHTTP(rec, req) - assert.Equal(t, http.StatusBadRequest, rec.Code) - }) -} diff --git a/backend/internal/server/test_helpers.go b/backend/internal/server/test_helpers.go deleted file mode 100644 index d9069a9..0000000 --- a/backend/internal/server/test_helpers.go +++ /dev/null @@ -1,15 +0,0 @@ -package server - -import ( - "github.com/labstack/echo/v4" -) - -// TestIPMiddleware adds a fake IP (for testing rate limits) -func TestIPMiddleware(ip string) echo.MiddlewareFunc { - return func(next echo.HandlerFunc) echo.HandlerFunc { - return func(c echo.Context) error { - c.Request().Header.Set("X-Real-IP", ip) - return next(c) - } - } -} From 06e6503cf2ed4c50eb1a4731936c9e3d498cdfa4 Mon Sep 17 00:00:00 2001 From: Aidan Traboulay Date: Sun, 15 Dec 2024 16:17:25 -0400 Subject: [PATCH 17/24] add migration for initial schema --- ....sql => 20241215194302_initial_schema.sql} | 46 ++++++++++++++++--- 1 file changed, 39 insertions(+), 7 deletions(-) rename backend/.sqlc/migrations/{schema.sql => 20241215194302_initial_schema.sql} (73%) diff --git a/backend/.sqlc/migrations/schema.sql b/backend/.sqlc/migrations/20241215194302_initial_schema.sql similarity index 73% rename from backend/.sqlc/migrations/schema.sql rename to backend/.sqlc/migrations/20241215194302_initial_schema.sql index ff7f722..130b293 100644 --- a/backend/.sqlc/migrations/schema.sql +++ b/backend/.sqlc/migrations/20241215194302_initial_schema.sql @@ -1,3 +1,9 @@ +-- +goose Up +-- +goose StatementBegin +CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; +CREATE EXTENSION IF NOT EXISTS pgcrypto; +SET TIME ZONE 'UTC'; + CREATE TYPE user_role AS ENUM ( 'admin', 'startup_owner', @@ -118,10 +124,36 @@ CREATE TABLE IF NOT EXISTS transactions ( created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ); -CREATE INDEX idx_users_email ON users(email); -CREATE INDEX idx_companies_owner ON companies(owner_id); -CREATE INDEX idx_projects_company ON projects(company_id); -CREATE INDEX idx_project_answers_project ON project_answers(project_id); -CREATE INDEX idx_transactions_project ON transactions(project_id); -CREATE INDEX idx_transactions_company ON transactions(company_id); -CREATE INDEX idx_team_members_company ON team_members(company_id); \ No newline at end of file +CREATE INDEX IF NOT EXISTS idx_users_email ON users(email); +CREATE INDEX IF NOT EXISTS idx_companies_owner ON companies(owner_id); +CREATE INDEX IF NOT EXISTS idx_projects_company ON projects(company_id); +CREATE INDEX IF NOT EXISTS idx_project_answers_project ON project_answers(project_id); +CREATE INDEX IF NOT EXISTS idx_transactions_project ON transactions(project_id); +CREATE INDEX IF NOT EXISTS idx_transactions_company ON transactions(company_id); +CREATE INDEX IF NOT EXISTS idx_team_members_company ON team_members(company_id); +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +DROP INDEX IF EXISTS idx_team_members_company; +DROP INDEX IF EXISTS idx_transactions_company; +DROP INDEX IF EXISTS idx_transactions_project; +DROP INDEX IF EXISTS idx_project_answers_project; +DROP INDEX IF EXISTS idx_projects_company; +DROP INDEX IF EXISTS idx_companies_owner; +DROP INDEX IF EXISTS idx_users_email; + +DROP TABLE IF EXISTS transactions; +DROP TABLE IF EXISTS project_comments; +DROP TABLE IF EXISTS project_documents; +DROP TABLE IF EXISTS project_answers; +DROP TABLE IF EXISTS project_questions; +DROP TABLE IF EXISTS projects; +DROP TABLE IF EXISTS team_members; +DROP TABLE IF EXISTS companies; +DROP TABLE IF EXISTS verify_email_tokens; +DROP TABLE IF EXISTS users; + +DROP TYPE IF EXISTS project_status; +DROP TYPE IF EXISTS user_role; +-- +goose StatementEnd \ No newline at end of file From 2f308d19bc5204f9969dad229186aea56a2bca39 Mon Sep 17 00:00:00 2001 From: Aidan Traboulay Date: Sun, 15 Dec 2024 16:18:16 -0400 Subject: [PATCH 18/24] rename healthcheck --- backend/internal/v1/v1_health/{healtcheck.go => healthcheck.go} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename backend/internal/v1/v1_health/{healtcheck.go => healthcheck.go} (100%) diff --git a/backend/internal/v1/v1_health/healtcheck.go b/backend/internal/v1/v1_health/healthcheck.go similarity index 100% rename from backend/internal/v1/v1_health/healtcheck.go rename to backend/internal/v1/v1_health/healthcheck.go From 86a2cc7dbd658227ad7a74d68f26010c9a7a1a84 Mon Sep 17 00:00:00 2001 From: Aidan Traboulay Date: Sun, 15 Dec 2024 16:22:04 -0400 Subject: [PATCH 19/24] remove owner id --- backend/.sqlc/migrations/20241215194302_initial_schema.sql | 2 -- 1 file changed, 2 deletions(-) diff --git a/backend/.sqlc/migrations/20241215194302_initial_schema.sql b/backend/.sqlc/migrations/20241215194302_initial_schema.sql index 130b293..eca810a 100644 --- a/backend/.sqlc/migrations/20241215194302_initial_schema.sql +++ b/backend/.sqlc/migrations/20241215194302_initial_schema.sql @@ -37,7 +37,6 @@ CREATE TABLE IF NOT EXISTS verify_email_tokens ( CREATE TABLE IF NOT EXISTS companies ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), - owner_id uuid NOT NULL REFERENCES users(id), name varchar NOT NULL, wallet_address varchar, linkedin_url varchar NOT NULL, @@ -125,7 +124,6 @@ CREATE TABLE IF NOT EXISTS transactions ( ); CREATE INDEX IF NOT EXISTS idx_users_email ON users(email); -CREATE INDEX IF NOT EXISTS idx_companies_owner ON companies(owner_id); CREATE INDEX IF NOT EXISTS idx_projects_company ON projects(company_id); CREATE INDEX IF NOT EXISTS idx_project_answers_project ON project_answers(project_id); CREATE INDEX IF NOT EXISTS idx_transactions_project ON transactions(project_id); From 8fa99b6d4cc85a450cd951c76069f8068056d95c Mon Sep 17 00:00:00 2001 From: Aidan Traboulay Date: Sun, 15 Dec 2024 16:32:57 -0400 Subject: [PATCH 20/24] add owner id --- backend/.sqlc/migrations/20241215194302_initial_schema.sql | 2 ++ 1 file changed, 2 insertions(+) diff --git a/backend/.sqlc/migrations/20241215194302_initial_schema.sql b/backend/.sqlc/migrations/20241215194302_initial_schema.sql index eca810a..130b293 100644 --- a/backend/.sqlc/migrations/20241215194302_initial_schema.sql +++ b/backend/.sqlc/migrations/20241215194302_initial_schema.sql @@ -37,6 +37,7 @@ CREATE TABLE IF NOT EXISTS verify_email_tokens ( CREATE TABLE IF NOT EXISTS companies ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + owner_id uuid NOT NULL REFERENCES users(id), name varchar NOT NULL, wallet_address varchar, linkedin_url varchar NOT NULL, @@ -124,6 +125,7 @@ CREATE TABLE IF NOT EXISTS transactions ( ); CREATE INDEX IF NOT EXISTS idx_users_email ON users(email); +CREATE INDEX IF NOT EXISTS idx_companies_owner ON companies(owner_id); CREATE INDEX IF NOT EXISTS idx_projects_company ON projects(company_id); CREATE INDEX IF NOT EXISTS idx_project_answers_project ON project_answers(project_id); CREATE INDEX IF NOT EXISTS idx_transactions_project ON transactions(project_id); From 2ff298dc9da2ce3f32c66d28ceda5aa2af5e57ce Mon Sep 17 00:00:00 2001 From: jc <46619361+juancwu@users.noreply.github.com> Date: Sun, 15 Dec 2024 15:38:57 -0500 Subject: [PATCH 21/24] change time types to bigint for epoch --- .../20241215194302_initial_schema.sql | 40 +++++++++---------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/backend/.sqlc/migrations/20241215194302_initial_schema.sql b/backend/.sqlc/migrations/20241215194302_initial_schema.sql index 130b293..09c7de4 100644 --- a/backend/.sqlc/migrations/20241215194302_initial_schema.sql +++ b/backend/.sqlc/migrations/20241215194302_initial_schema.sql @@ -23,16 +23,16 @@ CREATE TABLE IF NOT EXISTS users ( password char(256) NOT NULL, role user_role NOT NULL, email_verified boolean NOT NULL DEFAULT false, - created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + created_at bigint NOT NULL DEFAULT extract(epoch from now()), + updated_at bigint NOT NULL DEFAULT extract(epoch from now()), token_salt bytea UNIQUE NOT NULL DEFAULT gen_random_bytes(32) ); CREATE TABLE IF NOT EXISTS verify_email_tokens ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE, - created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, - expires_at timestamp NOT NULL + created_at bigint NOT NULL DEFAULT extract(epoch from now()), + expires_at bigint NOT NULL ); CREATE TABLE IF NOT EXISTS companies ( @@ -41,8 +41,8 @@ CREATE TABLE IF NOT EXISTS companies ( name varchar NOT NULL, wallet_address varchar, linkedin_url varchar NOT NULL, - created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP + created_at bigint NOT NULL DEFAULT extract(epoch from now()), + updated_at bigint NOT NULL DEFAULT extract(epoch from now()) ); CREATE TABLE IF NOT EXISTS team_members ( @@ -54,8 +54,8 @@ CREATE TABLE IF NOT EXISTS team_members ( bio varchar NOT NULL, linkedin_url varchar NOT NULL, is_account_owner boolean NOT NULL DEFAULT false, - created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP + created_at bigint NOT NULL DEFAULT extract(epoch from now()), + updated_at bigint NOT NULL DEFAULT extract(epoch from now()) ); CREATE TABLE IF NOT EXISTS projects ( @@ -64,16 +64,16 @@ CREATE TABLE IF NOT EXISTS projects ( title varchar NOT NULL, description varchar, status project_status NOT NULL DEFAULT 'draft', - created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP + created_at bigint NOT NULL DEFAULT extract(epoch from now()), + updated_at bigint NOT NULL DEFAULT extract(epoch from now()) ); CREATE TABLE IF NOT EXISTS project_questions ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), question varchar NOT NULL, section varchar NOT NULL DEFAULT 'overall', - created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP + created_at bigint NOT NULL DEFAULT extract(epoch from now()), + updated_at bigint NOT NULL DEFAULT extract(epoch from now()) ); CREATE TABLE IF NOT EXISTS project_answers ( @@ -81,8 +81,8 @@ CREATE TABLE IF NOT EXISTS project_answers ( project_id uuid NOT NULL REFERENCES projects(id) ON DELETE CASCADE, question_id uuid NOT NULL REFERENCES project_questions(id), answer varchar NOT NULL DEFAULT '', - created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + created_at bigint NOT NULL DEFAULT extract(epoch from now()), + updated_at bigint NOT NULL DEFAULT extract(epoch from now()), UNIQUE(project_id, question_id) ); @@ -92,8 +92,8 @@ CREATE TABLE IF NOT EXISTS project_documents ( name varchar NOT NULL, url varchar NOT NULL, section varchar NOT NULL DEFAULT 'overall', - created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP + created_at bigint NOT NULL DEFAULT extract(epoch from now()), + updated_at bigint NOT NULL DEFAULT extract(epoch from now()) ); CREATE TABLE IF NOT EXISTS project_comments ( @@ -102,8 +102,8 @@ CREATE TABLE IF NOT EXISTS project_comments ( target_id uuid NOT NULL, comment uuid NOT NULL, commenter_id uuid NOT NULL REFERENCES users(id), - created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP + created_at bigint NOT NULL DEFAULT extract(epoch from now()), + updated_at bigint NOT NULL DEFAULT extract(epoch from now()) ); CREATE TABLE IF NOT EXISTS transactions ( @@ -121,7 +121,7 @@ CREATE TABLE IF NOT EXISTS transactions ( total_fee decimal(65,18), status boolean NOT NULL, nonce bigint NOT NULL, - created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP + created_at bigint NOT NULL DEFAULT extract(epoch from now()) ); CREATE INDEX IF NOT EXISTS idx_users_email ON users(email); @@ -156,4 +156,4 @@ DROP TABLE IF EXISTS users; DROP TYPE IF EXISTS project_status; DROP TYPE IF EXISTS user_role; --- +goose StatementEnd \ No newline at end of file +-- +goose StatementEnd From f8d726994e8027a064adb125da6ae4b59cd83898 Mon Sep 17 00:00:00 2001 From: jc <46619361+juancwu@users.noreply.github.com> Date: Sun, 15 Dec 2024 15:46:29 -0500 Subject: [PATCH 22/24] update transactions table --- .../.sqlc/migrations/20241215194302_initial_schema.sql | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/backend/.sqlc/migrations/20241215194302_initial_schema.sql b/backend/.sqlc/migrations/20241215194302_initial_schema.sql index 09c7de4..cce7251 100644 --- a/backend/.sqlc/migrations/20241215194302_initial_schema.sql +++ b/backend/.sqlc/migrations/20241215194302_initial_schema.sql @@ -111,17 +111,9 @@ CREATE TABLE IF NOT EXISTS transactions ( project_id uuid NOT NULL REFERENCES projects(id), company_id uuid NOT NULL REFERENCES companies(id), tx_hash varchar NOT NULL, - block_number bigint NOT NULL, from_address varchar NOT NULL, to_address varchar NOT NULL, - value_amount decimal(65,18) NOT NULL, - currency_symbol varchar NOT NULL, - gas_price decimal(65,18), - gas_used bigint, - total_fee decimal(65,18), - status boolean NOT NULL, - nonce bigint NOT NULL, - created_at bigint NOT NULL DEFAULT extract(epoch from now()) + value_amount decimal(65,18) NOT NULL ); CREATE INDEX IF NOT EXISTS idx_users_email ON users(email); From 05929b9990118bea17de4d2dbc053ae8a387e3aa Mon Sep 17 00:00:00 2001 From: Aidan Traboulay Date: Sun, 15 Dec 2024 17:06:08 -0400 Subject: [PATCH 23/24] add interface types --- backend/internal/interfaces/types.go | 61 ++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/backend/internal/interfaces/types.go b/backend/internal/interfaces/types.go index e69de29..12a1243 100644 --- a/backend/internal/interfaces/types.go +++ b/backend/internal/interfaces/types.go @@ -0,0 +1,61 @@ +package interfaces + +import ( + "github.com/jackc/pgx/v5/pgxpool" + "github.com/labstack/echo/v4" + + "KonferCA/SPUR/db" + "KonferCA/SPUR/internal/middleware" + "KonferCA/SPUR/storage" +) + +/* +CoreServer defines the complete set of capabilities required by API versions. +This interface is implemented by the main server struct and provides access +to all core services and dependencies needed across the application. + +Usage: + - Implemented by the main server struct + - Used by API versioned packages to access core functionality + - Provides complete access to all server capabilities + +Example: + + func SetupRoutes(e *echo.Group, s CoreServer) { + // Use s.GetQueries() for database access + // Use s.GetStorage() for file operations + // etc. + } +*/ +type CoreServer interface { + GetDB() *pgxpool.Pool + GetQueries() *db.Queries + GetStorage() *storage.Storage + GetAuthLimiter() *middleware.RateLimiter + GetAPILimiter() *middleware.RateLimiter + GetEcho() *echo.Echo +} + +/* +HandlerDependencies defines a minimal interface for individual handlers. +This interface provides only the essential services needed for most handlers, +reducing coupling and making handlers easier to test. + +Usage: + - Used by individual route handlers + - Provides minimal required dependencies + - Makes handlers more testable with minimal mocking + +Example: + + func (h *Handler) HandleCreateUser(deps HandlerDependencies) echo.HandlerFunc { + return func(c echo.Context) error { + // Use deps.GetQueries() for database operations + // Use deps.GetStorage() for file operations + } + } +*/ +type HandlerDependencies interface { + GetQueries() *db.Queries + GetStorage() *storage.Storage +} From c1ea811c6b62b6f6533e0e94fa0b5cc9c1aa9133 Mon Sep 17 00:00:00 2001 From: Aidan Traboulay Date: Sun, 15 Dec 2024 17:17:59 -0400 Subject: [PATCH 24/24] remove handler dependecy interface --- backend/internal/interfaces/types.go | 24 ------------------------ 1 file changed, 24 deletions(-) diff --git a/backend/internal/interfaces/types.go b/backend/internal/interfaces/types.go index 12a1243..c5f0f07 100644 --- a/backend/internal/interfaces/types.go +++ b/backend/internal/interfaces/types.go @@ -35,27 +35,3 @@ type CoreServer interface { GetAPILimiter() *middleware.RateLimiter GetEcho() *echo.Echo } - -/* -HandlerDependencies defines a minimal interface for individual handlers. -This interface provides only the essential services needed for most handlers, -reducing coupling and making handlers easier to test. - -Usage: - - Used by individual route handlers - - Provides minimal required dependencies - - Makes handlers more testable with minimal mocking - -Example: - - func (h *Handler) HandleCreateUser(deps HandlerDependencies) echo.HandlerFunc { - return func(c echo.Context) error { - // Use deps.GetQueries() for database operations - // Use deps.GetStorage() for file operations - } - } -*/ -type HandlerDependencies interface { - GetQueries() *db.Queries - GetStorage() *storage.Storage -}