Skip to content

Commit

Permalink
Handle resume & founders agreement file upload in TeamMembers.tsx (#395)
Browse files Browse the repository at this point in the history
  • Loading branch information
juancwu authored Feb 4, 2025
2 parents ba59a5c + 2b3bbda commit 5448616
Show file tree
Hide file tree
Showing 10 changed files with 317 additions and 12 deletions.
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

0 comments on commit 5448616

Please sign in to comment.