refactor: account model overhaul

This commit is contained in:
2026-01-01 18:29:52 +00:00
parent ad7d7c6a1b
commit 88492dd876
49 changed files with 1559 additions and 573 deletions

View File

@@ -5,21 +5,22 @@ import (
"time"
"github.com/get-drexa/drexa/internal/account"
"github.com/get-drexa/drexa/internal/drive"
"github.com/get-drexa/drexa/internal/httperr"
"github.com/get-drexa/drexa/internal/nullable"
"github.com/get-drexa/drexa/internal/reqctx"
"github.com/get-drexa/drexa/internal/user"
"github.com/get-drexa/drexa/internal/virtualfs"
"github.com/gofiber/fiber/v2"
"github.com/google/uuid"
"github.com/uptrace/bun"
)
type HTTPHandler struct {
sharingService *Service
accountService *account.Service
vfs *virtualfs.VirtualFS
db *bun.DB
sharingService *Service
accountService *account.Service
driveService *drive.Service
vfs *virtualfs.VirtualFS
db *bun.DB
optionalAuthMiddleware fiber.Handler
}
@@ -39,12 +40,13 @@ type patchShareRequest struct {
ExpiresAt nullable.Time `json:"expiresAt" example:"2025-01-15T00:00:00Z"`
}
func NewHTTPHandler(sharingService *Service, accountService *account.Service, vfs *virtualfs.VirtualFS, db *bun.DB, optionalAuthMiddleware fiber.Handler) *HTTPHandler {
func NewHTTPHandler(sharingService *Service, accountService *account.Service, driveService *drive.Service, vfs *virtualfs.VirtualFS, db *bun.DB, optionalAuthMiddleware fiber.Handler) *HTTPHandler {
return &HTTPHandler{
sharingService: sharingService,
accountService: accountService,
vfs: vfs,
db: db,
sharingService: sharingService,
accountService: accountService,
driveService: driveService,
vfs: vfs,
db: db,
optionalAuthMiddleware: optionalAuthMiddleware,
}
}
@@ -57,7 +59,7 @@ func (h *HTTPHandler) RegisterShareConsumeRoutes(r fiber.Router) *virtualfs.Scop
return &virtualfs.ScopedRouter{Router: g}
}
func (h *HTTPHandler) RegisterShareManagementRoutes(api *account.ScopedRouter) {
func (h *HTTPHandler) RegisterShareManagementRoutes(api *virtualfs.ScopedRouter) {
g := api.Group("/shares")
g.Post("/", h.createShare)
g.Get("/:shareID", h.getShare)
@@ -76,33 +78,23 @@ func (h *HTTPHandler) shareMiddleware(c *fiber.Ctx) error {
return httperr.Internal(err)
}
// a share can be public or shared to specific accounts
// if latter, the accountId query param is expected and the route should be authenticated
// then the correct account is found using the authenticated user and the accountId query param
// finally, the account scope is resolved for the share
// otherwise, consumerAccount will be nil to attempt to resolve a public scope for the share
var consumerAccount *account.Account
qAccountID := c.Query("accountId")
if qAccountID != "" {
consumerAccountID, err := uuid.Parse(qAccountID)
u, _ := reqctx.AuthenticatedUser(c).(*user.User)
if u != nil {
drive, err := h.driveService.DriveByID(c.Context(), h.db, share.DriveID)
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": "invalid account ID",
})
return httperr.Internal(err)
}
u, _ := reqctx.AuthenticatedUser(c).(*user.User)
if u == nil {
return c.SendStatus(fiber.StatusUnauthorized)
}
consumerAccount, err = h.accountService.AccountByID(c.Context(), h.db, u.ID, consumerAccountID)
consumerAccount, err = h.accountService.FindUserAccountInOrg(c.Context(), h.db, drive.OrgID, u.ID)
if err != nil {
if errors.Is(err, account.ErrAccountNotFound) {
return c.SendStatus(fiber.StatusNotFound)
consumerAccount = nil
} else {
return httperr.Internal(err)
}
return httperr.Internal(err)
} else if consumerAccount.Status != account.StatusActive {
consumerAccount = nil
}
}
@@ -132,19 +124,28 @@ func (h *HTTPHandler) shareMiddleware(c *fiber.Ctx) error {
// @Tags shares
// @Accept json
// @Produce json
// @Param accountID path string true "Account ID" format(uuid)
// @Param driveID path string true "Drive ID" format(uuid)
// @Param shareID path string true "Share ID"
// @Success 200 {object} Share "Share details"
// @Failure 401 {string} string "Not authenticated"
// @Failure 404 {string} string "Share not found"
// @Security BearerAuth
// @Router /accounts/{accountID}/shares/{shareID} [get]
// @Router /drives/{driveID}/shares/{shareID} [get]
func (h *HTTPHandler) getShare(c *fiber.Ctx) error {
shareID := c.Params("shareID")
share, err := h.sharingService.FindShareByPublicID(c.Context(), h.db, shareID)
if err != nil {
if errors.Is(err, ErrShareNotFound) {
return c.SendStatus(fiber.StatusNotFound)
}
return httperr.Internal(err)
}
drive, _ := reqctx.CurrentDrive(c).(*drive.Drive)
if drive == nil || share.DriveID != drive.ID {
return c.SendStatus(fiber.StatusNotFound)
}
return c.JSON(share)
}
@@ -154,14 +155,14 @@ func (h *HTTPHandler) getShare(c *fiber.Ctx) error {
// @Tags shares
// @Accept json
// @Produce json
// @Param accountID path string true "Account ID" format(uuid)
// @Param driveID path string true "Drive ID" format(uuid)
// @Param request body createShareRequest true "Share details"
// @Success 200 {object} Share "Created share"
// @Failure 400 {object} map[string]string "Invalid request, items not in same directory, or root directory cannot be shared"
// @Failure 401 {string} string "Not authenticated"
// @Failure 404 {object} map[string]string "One or more items not found"
// @Security BearerAuth
// @Router /accounts/{accountID}/shares [post]
// @Router /drives/{driveID}/shares [post]
func (h *HTTPHandler) createShare(c *fiber.Ctx) error {
scope, ok := scopeFromCtx(c)
if !ok {
@@ -173,6 +174,11 @@ func (h *HTTPHandler) createShare(c *fiber.Ctx) error {
return c.SendStatus(fiber.StatusUnauthorized)
}
drive, _ := reqctx.CurrentDrive(c).(*drive.Drive)
if drive == nil {
return c.SendStatus(fiber.StatusUnauthorized)
}
var req createShareRequest
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
@@ -207,7 +213,7 @@ func (h *HTTPHandler) createShare(c *fiber.Ctx) error {
opts.ExpiresAt = *req.ExpiresAt
}
share, err := h.sharingService.CreateShare(c.Context(), tx, acc.ID, opts)
share, err := h.sharingService.CreateShare(c.Context(), tx, drive.ID, acc.ID, opts)
if err != nil {
if errors.Is(err, ErrNotSameParent) {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "items must be in the same directory"})
@@ -232,7 +238,7 @@ func (h *HTTPHandler) createShare(c *fiber.Ctx) error {
// @Tags shares
// @Accept json
// @Produce json
// @Param accountID path string true "Account ID" format(uuid)
// @Param driveID path string true "Drive ID" format(uuid)
// @Param shareID path string true "Share ID"
// @Param request body patchShareRequest true "Share details"
// @Success 200 {object} Share "Updated share"
@@ -240,7 +246,7 @@ func (h *HTTPHandler) createShare(c *fiber.Ctx) error {
// @Failure 401 {string} string "Not authenticated"
// @Failure 404 {string} string "Share not found"
// @Security BearerAuth
// @Router /accounts/{accountID}/shares/{shareID} [patch]
// @Router /drives/{driveID}/shares/{shareID} [patch]
func (h *HTTPHandler) updateShare(c *fiber.Ctx) error {
shareID := c.Params("shareID")
@@ -252,6 +258,16 @@ func (h *HTTPHandler) updateShare(c *fiber.Ctx) error {
return httperr.Internal(err)
}
drive, _ := reqctx.CurrentDrive(c).(*drive.Drive)
if drive == nil || share.DriveID != drive.ID {
return c.SendStatus(fiber.StatusNotFound)
}
acc, _ := reqctx.CurrentAccount(c).(*account.Account)
if acc == nil || (acc.Role != account.RoleAdmin && share.CreatedByAccountID != acc.ID) {
return c.SendStatus(fiber.StatusNotFound)
}
var req patchShareRequest
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
@@ -291,16 +307,34 @@ func (h *HTTPHandler) updateShare(c *fiber.Ctx) error {
// @Summary Delete share
// @Description Delete a share link, revoking access for all users
// @Tags shares
// @Param accountID path string true "Account ID" format(uuid)
// @Param driveID path string true "Drive ID" format(uuid)
// @Param shareID path string true "Share ID"
// @Success 204 {string} string "Share deleted"
// @Failure 401 {string} string "Not authenticated"
// @Failure 404 {string} string "Share not found"
// @Security BearerAuth
// @Router /accounts/{accountID}/shares/{shareID} [delete]
// @Router /drives/{driveID}/shares/{shareID} [delete]
func (h *HTTPHandler) deleteShare(c *fiber.Ctx) error {
shareID := c.Params("shareID")
share, err := h.sharingService.FindShareByPublicID(c.Context(), h.db, shareID)
if err != nil {
if errors.Is(err, ErrShareNotFound) {
return c.SendStatus(fiber.StatusNotFound)
}
return httperr.Internal(err)
}
drive, _ := reqctx.CurrentDrive(c).(*drive.Drive)
if drive == nil || share.DriveID != drive.ID {
return c.SendStatus(fiber.StatusNotFound)
}
acc, _ := reqctx.CurrentAccount(c).(*account.Account)
if acc == nil || (acc.Role != account.RoleAdmin && share.CreatedByAccountID != acc.ID) {
return c.SendStatus(fiber.StatusNotFound)
}
tx, err := h.db.BeginTx(c.Context(), nil)
if err != nil {
return httperr.Internal(err)

View File

@@ -59,7 +59,7 @@ func NewService(vfs *virtualfs.VirtualFS) (*Service, error) {
// CreateShare creates a share record for its allowed items.
// A share is a partial share of a directory: the share root is always the common parent directory of all items.
func (s *Service) CreateShare(ctx context.Context, db bun.IDB, accountID uuid.UUID, opts CreateShareOptions) (*Share, error) {
func (s *Service) CreateShare(ctx context.Context, db bun.IDB, driveID uuid.UUID, createdByAccountID uuid.UUID, opts CreateShareOptions) (*Share, error) {
if len(opts.Items) == 0 {
return nil, ErrShareNoItems
}
@@ -87,12 +87,13 @@ func (s *Service) CreateShare(ctx context.Context, db bun.IDB, accountID uuid.UU
now := time.Now()
sh := &Share{
ID: id,
AccountID: accountID,
PublicID: pid,
SharedDirectoryID: sharedDirectoryID,
CreatedAt: now,
UpdatedAt: now,
ID: id,
DriveID: driveID,
CreatedByAccountID: createdByAccountID,
PublicID: pid,
SharedDirectoryID: sharedDirectoryID,
CreatedAt: now,
UpdatedAt: now,
}
if !opts.ExpiresAt.IsZero() {
@@ -165,11 +166,11 @@ func (s *Service) FindShareByPublicID(ctx context.Context, db bun.IDB, publicID
return sh, nil
}
func (s *Service) ListShares(ctx context.Context, db bun.IDB, accountID uuid.UUID, opts ListSharesOptions) ([]Share, error) {
func (s *Service) ListShares(ctx context.Context, db bun.IDB, driveID uuid.UUID, opts ListSharesOptions) ([]Share, error) {
var shares []Share
q := db.NewSelect().Model(&shares).
Where("account_id = ?", accountID)
Where("drive_id = ?", driveID)
if !opts.IncludesExpired {
q = q.Where("expires_at IS NULL OR expires_at > NOW()")
@@ -260,7 +261,7 @@ func (s *Service) ResolveScopeForShare(ctx context.Context, db bun.IDB, consumer
}
scope := &virtualfs.Scope{
AccountID: share.AccountID,
DriveID: share.DriveID,
RootNodeID: share.SharedDirectoryID,
}

View File

@@ -12,8 +12,9 @@ import (
type Share struct {
bun.BaseModel `bun:"node_shares"`
ID uuid.UUID `bun:",pk,type:uuid" json:"-"`
AccountID uuid.UUID `bun:"account_id,notnull,type:uuid" json:"-"`
ID uuid.UUID `bun:",pk,type:uuid" json:"-"`
DriveID uuid.UUID `bun:"drive_id,notnull,type:uuid" json:"-"`
CreatedByAccountID uuid.UUID `bun:"created_by_account_id,notnull,type:uuid" json:"-"`
// Unique share identifier (public ID)
PublicID string `bun:"public_id,notnull" json:"id" example:"kRp2XYTq9A55"`
SharedDirectoryID uuid.UUID `bun:"shared_directory_id,notnull,type:uuid" json:"-"`