Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Handle resume & founders agreement file upload in TeamMembers.tsx #395

Merged
merged 8 commits into from
Feb 4, 2025
7 changes: 7 additions & 0 deletions backend/.sqlc/queries/team_members.sql
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,13 @@ INSERT INTO team_members (
)
RETURNING *;

-- name: UpdateTeamMemberDocuments :exec
UPDATE team_members
SET
resume_internal_url = $1,
founders_agreement_internal_url = $2
WHERE id = $3 AND company_id = $4;

-- name: ListTeamMembers :many
SELECT * FROM team_members
WHERE company_id = $1
Expand Down
25 changes: 25 additions & 0 deletions backend/db/team_members.sql.go

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

16 changes: 16 additions & 0 deletions backend/internal/middleware/jwt.go
Original file line number Diff line number Diff line change
@@ -1,19 +1,35 @@
package middleware

import (
"errors"
"net/http"
"strings"

"KonferCA/SPUR/db"
"KonferCA/SPUR/internal/jwt"
"KonferCA/SPUR/internal/permissions"
"KonferCA/SPUR/internal/v1/v1_common"

"github.com/google/uuid"

"github.com/jackc/pgx/v5/pgxpool"
"github.com/labstack/echo/v4"
)

var (
ErrNoUserInContext = errors.New("user not found in context")
)

// GetUserFromContext tries to get the user object from the context.
// Returns an error if the user object is not found or is not the correct type.
func GetUserFromContext(c echo.Context) (*db.User, error) {
user, ok := c.Get("user").(*db.User)
if !ok {
return nil, ErrNoUserInContext
}
return user, nil
}

// CompanyAccess creates a middleware that validates company ownership
func CompanyAccess(dbPool *pgxpool.Pool) echo.MiddlewareFunc {
queries := db.New(dbPool)
Expand Down
3 changes: 0 additions & 3 deletions backend/internal/v1/v1_projects/documents.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import (

"github.com/google/uuid"
"github.com/labstack/echo/v4"
"github.com/rs/zerolog/log"
)

/*
Expand Down Expand Up @@ -42,8 +41,6 @@ func (h *Handler) handleUploadProjectDocument(c echo.Context) error {
return v1_common.Fail(c, 400, "Invalid request", err)
}

log.Debug().Any("req", req).Send()

form := c.Request().MultipartForm
var file *multipart.FileHeader
for _, files := range form.File {
Expand Down
103 changes: 103 additions & 0 deletions backend/internal/v1/v1_teams/documents.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package v1_teams

import (
"KonferCA/SPUR/db"
"KonferCA/SPUR/internal/middleware"
"KonferCA/SPUR/internal/v1/v1_common"
"fmt"
"io"
"mime/multipart"
"net/http"
"path/filepath"

"github.com/google/uuid"
"github.com/labstack/echo/v4"
)

const (
docTypeResume = "resume"
docTypeFoundersAgreement = "founders_agreement"
)

func (h *Handler) handleUploadTeamMemberDocument(c echo.Context) error {
user, err := middleware.GetUserFromContext(c)
if err != nil {
return v1_common.Fail(c, http.StatusUnauthorized, "unauthorized", err)
}

memberID := c.Param("member_id")
if _, err := uuid.Parse(memberID); err != nil {
return v1_common.Fail(c, http.StatusBadRequest, "Invalid uuid", err)
}

docType := c.Param("type")
if docType != docTypeResume && docType != docTypeFoundersAgreement {
return v1_common.Fail(c, http.StatusBadRequest, "Invalid document type", nil)
}

queries := h.server.GetQueries()

company, err := queries.GetCompanyByUserID(c.Request().Context(), user.ID)
if err != nil {
return v1_common.Fail(c, 404, "Company not found", err)
}

form := c.Request().MultipartForm
var file *multipart.FileHeader
for _, files := range form.File {
file = files[0]
break
}

// Open the file
src, err := file.Open()
if err != nil {
return v1_common.Fail(c, 500, "Failed to open file", err)
}
defer src.Close()

// Read file content
fileContent, err := io.ReadAll(src)
if err != nil {
return v1_common.Fail(c, 500, "Failed to read file", err)
}

// Generate S3 key
fileExt := filepath.Ext(file.Filename)
s3Key := fmt.Sprintf("member/%s/documents/%s/%s%s", memberID, docType, uuid.New().String(), fileExt)

// Upload to S3
fileURL, err := h.server.GetStorage().UploadFile(c.Request().Context(), s3Key, fileContent)
if err != nil {
return v1_common.Fail(c, 500, "Failed to upload file", err)
}

// get the team member
member, err := queries.GetTeamMember(c.Request().Context(), db.GetTeamMemberParams{ID: memberID, CompanyID: company.ID})
if err != nil {
return v1_common.Fail(c, 400, "Failed to get team member", err)
}

uploadArg := db.UpdateTeamMemberDocumentsParams{
ID: member.ID,
CompanyID: member.CompanyID,
ResumeInternalUrl: member.ResumeInternalUrl,
FoundersAgreementInternalUrl: member.FoundersAgreementInternalUrl,
}

switch docType {
case docTypeFoundersAgreement:
uploadArg.FoundersAgreementInternalUrl = &fileURL
default:
// default upload as resume
uploadArg.ResumeInternalUrl = &fileURL
}

err = queries.UpdateTeamMemberDocuments(c.Request().Context(), uploadArg)
if err != nil {
_ = h.server.GetStorage().DeleteFile(c.Request().Context(), s3Key)
return v1_common.Fail(c, 500, "Failed to save document record", err)
}

return c.JSON(http.StatusCreated, UploadTeamMemberDocumentResponse{Url: fileURL})
}
20 changes: 17 additions & 3 deletions backend/internal/v1/v1_teams/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ func SetupRoutes(e *echo.Group, s interfaces.CoreServer) {
h := &Handler{server: s}

// Create middleware instances
authBoth := middleware.Auth(s.GetDB(),
permissions.PermStartupOwner, // Startup owners
authBoth := middleware.Auth(s.GetDB(),
permissions.PermStartupOwner, // Startup owners
permissions.PermViewAllProjects, // Investors
)
authOwner := middleware.Auth(s.GetDB(), permissions.PermStartupOwner)
Expand All @@ -25,10 +25,24 @@ func SetupRoutes(e *echo.Group, s interfaces.CoreServer) {
teamGet := team.Group("", authBoth, companyAccess)
teamGet.GET("", h.handleGetTeamMembers)
teamGet.GET("/:member_id", h.handleGetTeamMember)

// Modification routes - require startup owner permission
teamModify := team.Group("", authOwner, companyAccess)
teamModify.POST("", h.handleAddTeamMember)
teamModify.PUT("/:member_id", h.handleUpdateTeamMember)
teamModify.DELETE("/:member_id", h.handleDeleteTeamMember)
teamModify.POST("/:member_id/:type/document", h.handleUploadTeamMemberDocument, middleware.FileCheck(middleware.FileConfig{
MinSize: 1024, // 1KB minimum
MaxSize: 10 * 1024 * 1024, // 10MB maximum
AllowedTypes: []string{
"application/pdf",
"application/msword",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
"application/vnd.ms-excel",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"image/jpeg",
"image/png",
},
StrictValidation: true,
}))
}
4 changes: 4 additions & 0 deletions backend/internal/v1/v1_teams/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ type UpdateTeamMemberRequest struct {
LinkedinUrl string `json:"linkedin_url,omitempty" validate:"omitempty,url"`
}

type UploadTeamMemberDocumentResponse struct {
Url string `json:"url"`
}

// Response types
type TeamMemberResponse struct {
ID string `json:"id"`
Expand Down
7 changes: 7 additions & 0 deletions frontend/src/components/FileUpload/FileUpload.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export interface FileUploadProps {
subSection?: string;
accessToken?: string;
enableAutosave?: boolean;
limit?: number;
}

const FileUpload: React.FC<FileUploadProps> = ({
Expand All @@ -54,6 +55,7 @@ const FileUpload: React.FC<FileUploadProps> = ({
subSection,
accessToken,
enableAutosave = false,
limit = Infinity,
}) => {
const [isDragging, setIsDragging] = useState(false);
const [uploadedFiles, setUploadedFiles] = useState<UploadableFile[]>(initialFiles);
Expand Down Expand Up @@ -155,6 +157,11 @@ const FileUpload: React.FC<FileUploadProps> = ({
};

const handleFiles = (files: File[]) => {
if (files.length > limit) {
// truncate file list
files = files.slice(0, limit);
}

// check file types
const validFiles = files.filter((file) =>
['application/pdf', 'image/png', 'image/jpeg'].includes(file.type)
Expand Down
Loading
Loading