diff --git a/server/internal/core/db/migrations/data/007_publink.sql b/server/internal/core/db/migrations/data/007_publink.sql index 0b0647c9..2c915a8d 100644 --- a/server/internal/core/db/migrations/data/007_publink.sql +++ b/server/internal/core/db/migrations/data/007_publink.sql @@ -6,14 +6,23 @@ CREATE TABLE publinks( created_by TEXT NOT NULL REFERENCES users(username) ON UPDATE CASCADE ON DELETE CASCADE, root UUID NOT NULL REFERENCES resources(id) ON UPDATE CASCADE ON DELETE CASCADE, accessed INT NOT NULL DEFAULT 0, - max_accesses INT, - password_hash TEXT, + max_accesses INT NOT NULL DEFAULT 0, + password_hash TEXT NOT NULL DEFAULT '', expires TIMESTAMP ); CREATE UNIQUE INDEX unique_publink ON publinks(name) WHERE deleted IS NULL; + +CREATE INDEX publinks_by_root ON publinks(root); + +CREATE INDEX publinks_by_creator ON publinks(created_by); + ---- create above / drop below ---- +DROP INDEX IF EXISTS publinks_by_creator; + +DROP INDEX IF EXISTS publinks_by_root; + DROP INDEX IF EXISTS unique_publink; DROP TABLE publinks; \ No newline at end of file diff --git a/server/internal/core/db/models.go b/server/internal/core/db/models.go index 7dcb8e10..229b10a9 100644 --- a/server/internal/core/db/models.go +++ b/server/internal/core/db/models.go @@ -24,8 +24,8 @@ type Publink struct { CreatedBy string Root uuid.UUID Accessed int32 - MaxAccesses pgtype.Int4 - PasswordHash pgtype.Text + MaxAccesses int32 + PasswordHash string Expires pgtype.Timestamp } diff --git a/server/internal/core/db/publink.sql.go b/server/internal/core/db/publink.sql.go index 60108153..52f0a26f 100644 --- a/server/internal/core/db/publink.sql.go +++ b/server/internal/core/db/publink.sql.go @@ -27,9 +27,9 @@ type CreatePublinkParams struct { Name string CreatedBy string Root uuid.UUID - PasswordHash pgtype.Text + PasswordHash string Expires pgtype.Timestamp - MaxAccesses pgtype.Int4 + MaxAccesses int32 } func (q *Queries) CreatePublink(ctx context.Context, arg CreatePublinkParams) error { @@ -74,3 +74,73 @@ func (q *Queries) MarkPublinkAccess(ctx context.Context, id int32) error { _, err := q.db.Exec(ctx, markPublinkAccess, id) return err } + +const publinksByCreator = `-- name: PublinksByCreator :many +SELECT id, name, created, deleted, created_by, root, accessed, max_accesses, password_hash, expires FROM publinks WHERE created_by = $1::text +` + +func (q *Queries) PublinksByCreator(ctx context.Context, username string) ([]Publink, error) { + rows, err := q.db.Query(ctx, publinksByCreator, username) + if err != nil { + return nil, err + } + defer rows.Close() + var items []Publink + for rows.Next() { + var i Publink + if err := rows.Scan( + &i.ID, + &i.Name, + &i.Created, + &i.Deleted, + &i.CreatedBy, + &i.Root, + &i.Accessed, + &i.MaxAccesses, + &i.PasswordHash, + &i.Expires, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const publinksByRoot = `-- name: PublinksByRoot :many +SELECT id, name, created, deleted, created_by, root, accessed, max_accesses, password_hash, expires FROM publinks WHERE root = $1::uuid +` + +func (q *Queries) PublinksByRoot(ctx context.Context, root uuid.UUID) ([]Publink, error) { + rows, err := q.db.Query(ctx, publinksByRoot, root) + if err != nil { + return nil, err + } + defer rows.Close() + var items []Publink + for rows.Next() { + var i Publink + if err := rows.Scan( + &i.ID, + &i.Name, + &i.Created, + &i.Deleted, + &i.CreatedBy, + &i.Root, + &i.Accessed, + &i.MaxAccesses, + &i.PasswordHash, + &i.Expires, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} diff --git a/server/internal/core/fs/publink.go b/server/internal/core/fs/publink.go index b1083e21..dd595516 100644 --- a/server/internal/core/fs/publink.go +++ b/server/internal/core/fs/publink.go @@ -5,39 +5,29 @@ import ( "github.com/jackc/pgx/v5/pgtype" "github.com/shroff/phylum/server/internal/core/db" + "github.com/shroff/phylum/server/internal/core/publink" "github.com/shroff/phylum/server/internal/core/util/crypt" ) -func (r Resource) CreatePublink(name, password string, maxAge time.Duration, accesses int) error { +func (r Resource) CreatePublink(name, password string, duration time.Duration, accesses int) error { if !r.hasPermission(PermissionShare | PermissionRead) { return ErrInsufficientPermissions } - passwordHash := pgtype.Text{} - expires := pgtype.Timestamp{} - maxAccesses := pgtype.Int4{} + passwordHash := "" if password != "" { hash, err := crypt.GenerateArgon2EncodedHash(password) if err != nil { return err } - passwordHash = pgtype.Text{ - String: hash, - Valid: true, - } + passwordHash = hash } - if maxAge != 0 { + expires := pgtype.Timestamp{} + if duration != 0 { expires = pgtype.Timestamp{ - Time: time.Now().Add(maxAge), - Valid: true, - } - } - - if accesses != 0 { - maxAccesses = pgtype.Int4{ - Int32: int32(accesses), + Time: time.Now().Add(duration), Valid: true, } } @@ -48,6 +38,38 @@ func (r Resource) CreatePublink(name, password string, maxAge time.Duration, acc Root: r.ID, PasswordHash: passwordHash, Expires: expires, - MaxAccesses: maxAccesses, + MaxAccesses: int32(accesses), }) } + +func (r Resource) ListPublinks() ([]publink.Info, error) { + res, err := r.f.db.PublinksByRoot(r.f.ctx, r.ID) + if err != nil { + return nil, err + } + + links := make([]publink.Info, len(res)) + for i, l := range res { + var deleted *time.Time + if l.Deleted.Valid { + deleted = &l.Deleted.Time + } + var expires *time.Time + if l.Expires.Valid { + expires = &l.Expires.Time + } + links[i] = publink.Info{ + ID: l.ID, + Name: l.Name, + Created: l.Created.Time, + Deleted: deleted, + CreatedBy: l.CreatedBy, + Root: l.Root, + Accessed: l.Accessed, + MaxAccesses: l.MaxAccesses, + Protected: l.PasswordHash != "", + Expires: expires, + } + } + return links, nil +} diff --git a/server/internal/core/publink/filesystem.go b/server/internal/core/publink/filesystem.go index caf6a55b..d607d3e6 100644 --- a/server/internal/core/publink/filesystem.go +++ b/server/internal/core/publink/filesystem.go @@ -2,11 +2,12 @@ package publink import ( "context" + "net/http" "github.com/google/uuid" "github.com/jackc/pgx/v5" "github.com/shroff/phylum/server/internal/core/db" - "github.com/shroff/phylum/server/internal/core/fs" + "github.com/shroff/phylum/server/internal/core/errors" "github.com/shroff/phylum/server/internal/core/storage" ) @@ -17,10 +18,13 @@ type filesystem struct { rootID uuid.UUID } +var errNotFound = errors.NewError(http.StatusNotFound, "", "") +var errResourceNotCollection = errors.NewError(http.StatusBadRequest, "", "") + func (f filesystem) ResourceByPath(path string) (Resource, error) { r, err := f.db.ResourceByPath(f.ctx, db.ResourceByPathParams{Root: f.rootID, Path: path}) if err == pgx.ErrNoRows || !r.Found { - err = fs.ErrResourceNotFound + err = errNotFound } if err != nil { return Resource{}, err diff --git a/server/internal/core/publink/info.go b/server/internal/core/publink/info.go new file mode 100644 index 00000000..0b82c836 --- /dev/null +++ b/server/internal/core/publink/info.go @@ -0,0 +1,20 @@ +package publink + +import ( + "time" + + "github.com/google/uuid" +) + +type Info struct { + ID int32 `json:"id"` + Name string `json:"name"` + Created time.Time `json:"created"` + Deleted *time.Time `json:"deleted"` + CreatedBy string `json:"creator"` + Root uuid.UUID `json:"root"` + Accessed int32 `json:"accessed"` + MaxAccesses int32 `json:"max_accesses"` + Protected bool `json:"protected"` + Expires *time.Time `json:"expires"` +} diff --git a/server/internal/core/publink/open.go b/server/internal/core/publink/open.go index f5debd2a..857fac82 100644 --- a/server/internal/core/publink/open.go +++ b/server/internal/core/publink/open.go @@ -6,14 +6,14 @@ import ( "time" "github.com/jackc/pgx/v5" - "github.com/shroff/phylum/server/internal/api/auth" "github.com/shroff/phylum/server/internal/core/db" "github.com/shroff/phylum/server/internal/core/errors" "github.com/shroff/phylum/server/internal/core/storage" "github.com/shroff/phylum/server/internal/core/util/crypt" ) -var ErrNotFound = errors.NewError(http.StatusNotFound, "publink_not_found", "publink not found") +var ErrUnauthorized = errors.NewError(http.StatusUnauthorized, "", "Unauthorized") +var ErrNotFound = errors.NewError(http.StatusNotFound, "", "Not Found") func OpenResource(ctx context.Context, name string, password string, path string) (Resource, error) { // Check exists @@ -26,26 +26,25 @@ func OpenResource(ctx context.Context, name string, password string, path string } // check password - if s.PasswordHash.Valid { - if ok, err := crypt.VerifyPassword(password, s.PasswordHash.String); err != nil { + if s.PasswordHash != "" { + if password == "" { + return Resource{}, ErrUnauthorized + } + if ok, err := crypt.VerifyPassword(password, s.PasswordHash); err != nil { return Resource{}, err } else if !ok { - return Resource{}, auth.ErrRequired + return Resource{}, ErrUnauthorized } } // check expiration - if s.Expires.Valid { - if s.Expires.Time.Before(time.Now()) { - return Resource{}, ErrNotFound - } + if s.Expires.Valid && s.Expires.Time.Before(time.Now()) { + return Resource{}, ErrNotFound } // check max accesses - if s.MaxAccesses.Valid { - if s.MaxAccesses.Int32 <= s.Accessed { - return Resource{}, ErrNotFound - } + if s.MaxAccesses > 0 && s.MaxAccesses <= s.Accessed { + return Resource{}, ErrNotFound } db.Get().MarkPublinkAccess(ctx, s.ID) diff --git a/server/internal/core/publink/resource.go b/server/internal/core/publink/resource.go index e02df4fe..4a95a3da 100644 --- a/server/internal/core/publink/resource.go +++ b/server/internal/core/publink/resource.go @@ -7,7 +7,6 @@ import ( "github.com/google/uuid" "github.com/shroff/phylum/server/internal/api/serve" "github.com/shroff/phylum/server/internal/core/db" - "github.com/shroff/phylum/server/internal/core/fs" ) type Resource struct { @@ -39,7 +38,7 @@ func (r Resource) OpenRead(start, length int64) (io.ReadCloser, error) { func (r Resource) ReadDir(recursive bool) ([]serve.Resource, error) { if !r.dir { - return nil, fs.ErrResourceNotCollection + return nil, errResourceNotCollection } children, err := r.f.db.ReadDir(r.f.ctx, db.ReadDirParams{ ID: r.id, diff --git a/server/sql/queries/publink.sql b/server/sql/queries/publink.sql index b87c0cd3..cf3ce154 100644 --- a/server/sql/queries/publink.sql +++ b/server/sql/queries/publink.sql @@ -9,7 +9,13 @@ INSERT INTO publinks(name, created_by, root, password_hash, expires, max_accesse @name::text, @created_by::text, @root::uuid, - sqlc.narg('password_hash')::text, + @password_hash::text, sqlc.narg('expires')::timestamp, - sqlc.narg('max_accesses')::int -); \ No newline at end of file + @max_accesses::int +); + +-- name: PublinksByRoot :many +SELECT * FROM publinks WHERE root = @root::uuid; + +-- name: PublinksByCreator :many +SELECT * FROM publinks WHERE created_by = @username::text; \ No newline at end of file