Update API controllers (#2159)
This commit is contained in:
@@ -253,12 +253,18 @@ public class SharingController(
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// List the shared folders the caller has been granted access to, each with the wrapped VEK the caller unwraps
|
||||
/// with its private key.
|
||||
/// Upload a new revision of a shared-folder manifest. Allowed for the owner and for every user holding a
|
||||
/// <c>shared</c> grant on it — all members write the same manifest, guarded by the optimistic revision check
|
||||
/// (a stale writer gets <see cref="VaultStatus.Outdated"/> and must re-pull + merge, like the root manifest).
|
||||
/// Blob bytes are uploaded beforehand via <c>POST /v2/Vault/blobs</c> into the pusher's own store; the manifest
|
||||
/// snapshot endpoints resolve referenced blobs across member stores.
|
||||
/// </summary>
|
||||
/// <returns>The shared folders available to the caller.</returns>
|
||||
[HttpGet("shared-with-me")]
|
||||
public async Task<IActionResult> SharedWithMe()
|
||||
/// <param name="manifestId">The shared folder manifest id.</param>
|
||||
/// <param name="model">The update request.</param>
|
||||
/// <param name="clientHeader">The client identifier header.</param>
|
||||
/// <returns>The update response.</returns>
|
||||
[HttpPost("folders/{manifestId:guid}")]
|
||||
public async Task<IActionResult> UpdateFolder(Guid manifestId, [FromBody] UpdateSharedFolderRequest model, [FromHeader(Name = "X-AliasVault-Client")] string? clientHeader)
|
||||
{
|
||||
await using var context = await dbContextFactory.CreateDbContextAsync();
|
||||
var me = await GetCurrentUserAsync();
|
||||
@@ -267,44 +273,57 @@ public class SharingController(
|
||||
return Unauthorized();
|
||||
}
|
||||
|
||||
var grants = await context.VaultKeys
|
||||
.Where(x => x.UserId == me.Id && x.KeyType == AuthHelper.VaultKeyTypeShared)
|
||||
.ToListAsync();
|
||||
|
||||
var response = new SharedWithMeResponse();
|
||||
if (grants.Count == 0)
|
||||
var manifest = await context.VaultManifests.FirstOrDefaultAsync(x => x.ManifestId == manifestId && !x.IsRoot);
|
||||
var canWrite = manifest != null && (manifest.OwnerUserId == me.Id || await context.VaultKeys.AnyAsync(k => k.UserId == me.Id && k.KeyType == AuthHelper.VaultKeyTypeShared && k.VaultManifestId == manifestId));
|
||||
if (manifest == null || !canWrite)
|
||||
{
|
||||
return Ok(response);
|
||||
return NotFound(ApiErrorCodeHelper.CreateValidationErrorResponse(ApiErrorCode.SHARED_MANIFEST_NOT_FOUND, 404));
|
||||
}
|
||||
|
||||
var manifestIds = grants.Where(g => g.VaultManifestId != null).Select(g => g.VaultManifestId!.Value).ToList();
|
||||
var manifestsById = await context.VaultManifests
|
||||
.Where(m => manifestIds.Contains(m.ManifestId))
|
||||
.ToDictionaryAsync(m => m.ManifestId);
|
||||
|
||||
var ownerIds = manifestsById.Values.Select(m => m.OwnerUserId).Distinct().ToList();
|
||||
var ownerUsernamesById = await context.AliasVaultUsers
|
||||
.Where(u => ownerIds.Contains(u.Id))
|
||||
.ToDictionaryAsync(u => u.Id, u => u.UserName);
|
||||
|
||||
foreach (var g in grants)
|
||||
var newRevision = model.CurrentRevision + 1;
|
||||
if (manifest.RevisionNumber >= newRevision)
|
||||
{
|
||||
if (g.VaultManifestId is null || !manifestsById.TryGetValue(g.VaultManifestId.Value, out var manifest))
|
||||
return Ok(new UpdateSharedFolderResponse { Status = VaultStatus.Outdated, NewRevisionNumber = manifest.RevisionNumber });
|
||||
}
|
||||
|
||||
var strategy = context.Database.CreateExecutionStrategy();
|
||||
return await strategy.ExecuteAsync<IActionResult>(async () =>
|
||||
{
|
||||
await using var tx = await context.Database.BeginTransactionAsync();
|
||||
|
||||
// Referenced blob bytes must already exist in some member's store (uploaded via POST /v2/Vault/blobs).
|
||||
if (model.BlobReferences.Count > 0)
|
||||
{
|
||||
continue;
|
||||
var refHashes = model.BlobReferences.Select(r => r.Hash).Distinct().ToList();
|
||||
var presentHashes = await context.VaultBlobObjects.Where(b => refHashes.Contains(b.Hash)).Select(b => b.Hash).Distinct().ToListAsync();
|
||||
if (refHashes.Except(presentHashes).Any())
|
||||
{
|
||||
await tx.RollbackAsync();
|
||||
return BadRequest(ApiErrorCodeHelper.CreateValidationErrorResponse(ApiErrorCode.VAULT_NOT_UP_TO_DATE, 400));
|
||||
}
|
||||
}
|
||||
|
||||
response.Folders.Add(new SharedWithMeItem
|
||||
{
|
||||
ManifestId = manifest.ManifestId,
|
||||
Name = manifest.Name,
|
||||
OwnerUserId = manifest.OwnerUserId,
|
||||
OwnerUsername = ownerUsernamesById.GetValueOrDefault(manifest.OwnerUserId),
|
||||
WrappedVek = g.WrappedVek,
|
||||
WrapScheme = g.WrapScheme,
|
||||
});
|
||||
}
|
||||
// Archive the current revision, then update the row in place. TODO: apply a retention policy to
|
||||
// shared-manifest history (currently only the root manifest history is pruned).
|
||||
context.VaultManifestsHistory.Add(VaultManifestsHistory.CreateFrom(manifest));
|
||||
|
||||
return Ok(response);
|
||||
manifest.ManifestBlob = model.ManifestBlob;
|
||||
manifest.ManifestCiphertextHash = model.ManifestCiphertextHash;
|
||||
manifest.Version = model.Version;
|
||||
manifest.RevisionNumber = newRevision;
|
||||
manifest.FileSize = FileHelper.Base64StringToKilobytes(model.ManifestBlob);
|
||||
manifest.Client = clientHeader;
|
||||
manifest.UpdatedAt = timeProvider.UtcNow;
|
||||
|
||||
foreach (var dto in model.BlobReferences)
|
||||
{
|
||||
context.VaultBlobReferences.Add(new VaultBlobReference { ManifestId = manifest.ManifestId, RevisionNumber = newRevision, BlobHash = dto.Hash });
|
||||
}
|
||||
|
||||
await context.SaveChangesAsync();
|
||||
await tx.CommitAsync();
|
||||
|
||||
return Ok(new UpdateSharedFolderResponse { Status = VaultStatus.Ok, NewRevisionNumber = newRevision });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,8 +73,9 @@ public class VaultController(
|
||||
return Unauthorized();
|
||||
}
|
||||
|
||||
// Current revision per logical manifest.
|
||||
var manifestRevisions = await context.VaultManifests
|
||||
// Current revision per logical manifest: everything the user owns plus manifests shared with them,
|
||||
// so revision-based pull detection covers shared folders too.
|
||||
var ownedManifestRevisions = await context.VaultManifests
|
||||
.Where(x => x.OwnerUserId == user.Id && x.StorageFormat == ManifestFormat)
|
||||
.Select(x => new ManifestRevision { ManifestId = x.ManifestId, IsRoot = x.IsRoot, Revision = x.RevisionNumber })
|
||||
.ToListAsync();
|
||||
@@ -83,6 +84,14 @@ public class VaultController(
|
||||
// IsRoot=false, owned by another user) must never make a not-yet-migrated user look migrated, or the client
|
||||
// would push without CreateVaultKey and the upload would fail with VAULT_KEY_NOT_FOUND.
|
||||
var isMigrated = ownedManifestRevisions.Any(x => x.IsRoot);
|
||||
|
||||
var manifestRevisions = ownedManifestRevisions;
|
||||
var grantedManifestIds = await GetGrantedManifestIdsAsync(context, user.Id);
|
||||
manifestRevisions.AddRange(await context.VaultManifests
|
||||
.Where(x => grantedManifestIds.Contains(x.ManifestId) && x.StorageFormat == ManifestFormat)
|
||||
.Select(x => new ManifestRevision { ManifestId = x.ManifestId, IsRoot = false, Revision = x.RevisionNumber })
|
||||
.ToListAsync());
|
||||
|
||||
// Latest revision per bucket kind.
|
||||
var bucketRevisions = await context.VaultDataBuckets
|
||||
.Where(x => x.OwnerUserId == user.Id)
|
||||
@@ -118,7 +127,7 @@ public class VaultController(
|
||||
// Current revision per logical manifest (one row per manifest in the VaultManifests table).
|
||||
var latestManifests = await context.VaultManifests
|
||||
.Where(x => x.OwnerUserId == user.Id && x.StorageFormat == ManifestFormat)
|
||||
.Select(x => new { x.ManifestId, x.IsRoot, x.ManifestBlob, x.ManifestCiphertextHash, x.RevisionNumber })
|
||||
.Select(x => new { x.ManifestId, x.IsRoot, x.Name, x.ManifestBlob, x.ManifestCiphertextHash, x.RevisionNumber })
|
||||
.ToListAsync();
|
||||
|
||||
if (latestManifests.Count == 0)
|
||||
@@ -178,12 +187,17 @@ public class VaultController(
|
||||
{
|
||||
ManifestId = m.ManifestId,
|
||||
IsRoot = m.IsRoot,
|
||||
Name = m.Name,
|
||||
Blob = m.ManifestBlob,
|
||||
CiphertextHash = m.ManifestCiphertextHash,
|
||||
Revision = m.RevisionNumber,
|
||||
BlobReferences = refsByManifest.TryGetValue(m.ManifestId, out var refs) ? refs : [],
|
||||
}).ToList();
|
||||
|
||||
// Append manifests shared with this user by other owners, each carrying the wrapped VEK the caller
|
||||
// unwraps with its private key.
|
||||
manifests.AddRange(await BuildSharedWithMeManifestsAsync(context, user.Id));
|
||||
|
||||
return Ok(new GetResponse
|
||||
{
|
||||
Status = VaultStatus.Ok,
|
||||
@@ -212,8 +226,9 @@ public class VaultController(
|
||||
return Unauthorized();
|
||||
}
|
||||
|
||||
// The caller can fetch a manifest it owns, or one another user granted to it (a shared folder).
|
||||
var latest = await context.VaultManifests
|
||||
.Where(x => x.OwnerUserId == user.Id && x.StorageFormat == ManifestFormat && x.ManifestId == manifestId)
|
||||
.Where(x => x.StorageFormat == ManifestFormat && x.ManifestId == manifestId && (x.OwnerUserId == user.Id || context.VaultKeys.Any(k => k.UserId == user.Id && k.KeyType == AuthHelper.VaultKeyTypeShared && k.VaultManifestId == x.ManifestId)))
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
if (latest == null)
|
||||
@@ -221,24 +236,36 @@ public class VaultController(
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var blobRefs = await context.VaultBlobReferences
|
||||
.Where(r => r.ManifestId == latest.ManifestId && r.RevisionNumber == latest.RevisionNumber)
|
||||
.Join(
|
||||
context.VaultBlobObjects.Where(b => b.OwnerUserId == user.Id),
|
||||
r => r.BlobHash,
|
||||
b => b.Hash,
|
||||
(r, b) => new BlobReference { Hash = b.Hash, Category = b.Category })
|
||||
.ToListAsync();
|
||||
var blobRefs = (await context.VaultBlobReferences
|
||||
.Where(r => r.ManifestId == latest.ManifestId && r.RevisionNumber == latest.RevisionNumber)
|
||||
.Join(context.VaultBlobObjects, r => r.BlobHash, b => b.Hash, (r, b) => new { b.Hash, b.Category })
|
||||
.Distinct()
|
||||
.ToListAsync())
|
||||
.Select(x => new BlobReference { Hash = x.Hash, Category = x.Category })
|
||||
.ToList();
|
||||
|
||||
return Ok(new Manifest
|
||||
var manifest = new Manifest
|
||||
{
|
||||
ManifestId = latest.ManifestId,
|
||||
IsRoot = latest.IsRoot,
|
||||
Name = latest.Name,
|
||||
Blob = latest.ManifestBlob,
|
||||
CiphertextHash = latest.ManifestCiphertextHash,
|
||||
Revision = latest.RevisionNumber,
|
||||
BlobReferences = blobRefs,
|
||||
});
|
||||
};
|
||||
|
||||
// Not the owner: enrich with the caller's grant (wrapped VEK + owner identity), same shape as the snapshot.
|
||||
if (latest.OwnerUserId != user.Id)
|
||||
{
|
||||
var grant = await context.VaultKeys.FirstOrDefaultAsync(k => k.UserId == user.Id && k.KeyType == AuthHelper.VaultKeyTypeShared && k.VaultManifestId == latest.ManifestId);
|
||||
var ownerUsername = await context.AliasVaultUsers.Where(u => u.Id == latest.OwnerUserId).Select(u => u.UserName).FirstOrDefaultAsync();
|
||||
manifest.WrappedVek = grant?.WrappedVek;
|
||||
manifest.WrapScheme = grant?.WrapScheme;
|
||||
manifest.OwnerUsername = ownerUsername;
|
||||
}
|
||||
|
||||
return Ok(manifest);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -565,6 +592,46 @@ public class VaultController(
|
||||
})
|
||||
.ToListAsync();
|
||||
|
||||
// Hashes not in the caller's own store may belong to a shared folder: any blob referenced by the current
|
||||
// revision of a manifest the caller can access (granted to them, or a manifest they own that another member
|
||||
// pushed blobs for) is downloadable regardless of which member's store holds the ciphertext.
|
||||
var missing = wanted.Except(rows.Select(r => r.Hash), StringComparer.Ordinal).ToList();
|
||||
if (missing.Count > 0)
|
||||
{
|
||||
var accessibleManifests = await context.VaultManifests
|
||||
.Where(m => m.StorageFormat == ManifestFormat && !m.IsRoot && (m.OwnerUserId == user.Id || context.VaultKeys.Any(k => k.UserId == user.Id && k.KeyType == AuthHelper.VaultKeyTypeShared && k.VaultManifestId == m.ManifestId)))
|
||||
.Select(m => new { m.ManifestId, m.RevisionNumber })
|
||||
.ToListAsync();
|
||||
var accessibleIds = accessibleManifests.Select(m => m.ManifestId).ToList();
|
||||
var currentRevisionById = accessibleManifests.ToDictionary(m => m.ManifestId, m => m.RevisionNumber);
|
||||
|
||||
if (accessibleIds.Count > 0)
|
||||
{
|
||||
var referencedHashes = (await context.VaultBlobReferences
|
||||
.Where(r => accessibleIds.Contains(r.ManifestId) && missing.Contains(r.BlobHash))
|
||||
.Select(r => new { r.ManifestId, r.RevisionNumber, r.BlobHash })
|
||||
.ToListAsync())
|
||||
.Where(r => currentRevisionById.TryGetValue(r.ManifestId, out var rev) && rev == r.RevisionNumber)
|
||||
.Select(r => r.BlobHash)
|
||||
.Distinct()
|
||||
.ToList();
|
||||
|
||||
if (referencedHashes.Count > 0)
|
||||
{
|
||||
var sharedRows = await context.VaultBlobObjects
|
||||
.Where(b => referencedHashes.Contains(b.Hash))
|
||||
.Select(b => new Blob
|
||||
{
|
||||
Hash = b.Hash,
|
||||
Category = b.Category,
|
||||
EncryptedDataBase64 = Convert.ToBase64String(b.EncryptedData),
|
||||
})
|
||||
.ToListAsync();
|
||||
rows.AddRange(sharedRows.GroupBy(b => b.Hash, StringComparer.Ordinal).Select(g => g.First()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Ok(rows);
|
||||
}
|
||||
|
||||
@@ -601,6 +668,79 @@ public class VaultController(
|
||||
return newRev;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The ids of manifests other users have granted to <paramref name="userId"/> via a <c>shared</c> VaultKey row.
|
||||
/// </summary>
|
||||
private static async Task<List<Guid>> GetGrantedManifestIdsAsync(AliasServerDbContext context, string userId)
|
||||
{
|
||||
return await context.VaultKeys
|
||||
.Where(k => k.UserId == userId && k.KeyType == AuthHelper.VaultKeyTypeShared && k.VaultManifestId != null)
|
||||
.Select(k => k.VaultManifestId!.Value)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds the manifest DTOs for every shared folder granted to <paramref name="userId"/> by other users: the
|
||||
/// encrypted manifest blob plus the grant's wrapped VEK, wrap scheme, and owner identity. Blob references are
|
||||
/// taken straight from the manifest's current revision, unscoped by store owner (see DownloadBlobs).
|
||||
/// </summary>
|
||||
private static async Task<List<Manifest>> BuildSharedWithMeManifestsAsync(AliasServerDbContext context, string userId)
|
||||
{
|
||||
var grants = await context.VaultKeys
|
||||
.Where(k => k.UserId == userId && k.KeyType == AuthHelper.VaultKeyTypeShared && k.VaultManifestId != null)
|
||||
.ToListAsync();
|
||||
if (grants.Count == 0)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
var manifestIds = grants.Select(g => g.VaultManifestId!.Value).ToList();
|
||||
var manifestsById = await context.VaultManifests
|
||||
.Where(m => manifestIds.Contains(m.ManifestId) && m.StorageFormat == ManifestFormat)
|
||||
.ToDictionaryAsync(m => m.ManifestId);
|
||||
|
||||
var ownerIds = manifestsById.Values.Select(m => m.OwnerUserId).Distinct().ToList();
|
||||
var ownerUsernamesById = await context.AliasVaultUsers
|
||||
.Where(u => ownerIds.Contains(u.Id))
|
||||
.ToDictionaryAsync(u => u.Id, u => u.UserName);
|
||||
|
||||
var refRows = await context.VaultBlobReferences
|
||||
.Where(r => manifestIds.Contains(r.ManifestId))
|
||||
.Join(context.VaultBlobObjects, r => r.BlobHash, b => b.Hash, (r, b) => new { r.ManifestId, r.RevisionNumber, b.Hash, b.Category })
|
||||
.ToListAsync();
|
||||
|
||||
var result = new List<Manifest>();
|
||||
foreach (var grant in grants)
|
||||
{
|
||||
if (!manifestsById.TryGetValue(grant.VaultManifestId!.Value, out var manifestRow))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var blobRefs = refRows
|
||||
.Where(r => r.ManifestId == manifestRow.ManifestId && r.RevisionNumber == manifestRow.RevisionNumber)
|
||||
.GroupBy(r => r.Hash, StringComparer.Ordinal)
|
||||
.Select(g => new BlobReference { Hash = g.Key, Category = g.First().Category })
|
||||
.ToList();
|
||||
|
||||
result.Add(new Manifest
|
||||
{
|
||||
ManifestId = manifestRow.ManifestId,
|
||||
IsRoot = false,
|
||||
Name = manifestRow.Name,
|
||||
Blob = manifestRow.ManifestBlob,
|
||||
CiphertextHash = manifestRow.ManifestCiphertextHash,
|
||||
Revision = manifestRow.RevisionNumber,
|
||||
BlobReferences = blobRefs,
|
||||
OwnerUsername = ownerUsernamesById.GetValueOrDefault(manifestRow.OwnerUserId),
|
||||
WrappedVek = grant.WrappedVek,
|
||||
WrapScheme = grant.WrapScheme,
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Upserts a batch of encrypted blob objects for a user in one round-trip. Existing blobs (same hash) only get
|
||||
/// their LastReferencedAt bumped, unless <paramref name="overwrite"/> is set (KEK/VEK migration) in which case
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
//-----------------------------------------------------------------------
|
||||
// <copyright file="SharedWithMeItem.cs" company="aliasvault">
|
||||
// Copyright (c) aliasvault. All rights reserved.
|
||||
// Licensed under the AGPLv3 license. See LICENSE.md file in the project root for full license information.
|
||||
// </copyright>
|
||||
//-----------------------------------------------------------------------
|
||||
|
||||
namespace AliasVault.Shared.Models.WebApi.V2.Sharing;
|
||||
|
||||
/// <summary>
|
||||
/// One shared folder the caller has been granted access to. The caller unwraps <see cref="WrappedVek"/> with the
|
||||
/// private key matching the public key it was wrapped for, then uses the resulting VEK to decrypt the folder manifest.
|
||||
/// </summary>
|
||||
public class SharedWithMeItem
|
||||
{
|
||||
/// <summary>Gets or sets the shared folder manifest id.</summary>
|
||||
public required Guid ManifestId { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the folder's display name.</summary>
|
||||
public string? Name { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the owner's user id.</summary>
|
||||
public required string OwnerUserId { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the owner's username.</summary>
|
||||
public string? OwnerUsername { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the folder VEK wrapped with the caller's public key (base64).</summary>
|
||||
public required string WrappedVek { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the wrap scheme of the grant (e.g. "x25519-sealedbox").</summary>
|
||||
public required string WrapScheme { get; set; }
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
//-----------------------------------------------------------------------
|
||||
// <copyright file="SharedWithMeResponse.cs" company="aliasvault">
|
||||
// Copyright (c) aliasvault. All rights reserved.
|
||||
// Licensed under the AGPLv3 license. See LICENSE.md file in the project root for full license information.
|
||||
// </copyright>
|
||||
//-----------------------------------------------------------------------
|
||||
|
||||
namespace AliasVault.Shared.Models.WebApi.V2.Sharing;
|
||||
|
||||
/// <summary>
|
||||
/// Response for GET /v2/Sharing/shared-with-me. Lists every shared folder the caller holds a grant for.
|
||||
/// </summary>
|
||||
public class SharedWithMeResponse
|
||||
{
|
||||
/// <summary>Gets or sets the shared folders the caller has access to.</summary>
|
||||
public List<SharedWithMeItem> Folders { get; set; } = [];
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
//-----------------------------------------------------------------------
|
||||
// <copyright file="UpdateSharedFolderRequest.cs" company="aliasvault">
|
||||
// Copyright (c) aliasvault. All rights reserved.
|
||||
// Licensed under the AGPLv3 license. See LICENSE.md file in the project root for full license information.
|
||||
// </copyright>
|
||||
//-----------------------------------------------------------------------
|
||||
|
||||
namespace AliasVault.Shared.Models.WebApi.V2.Sharing;
|
||||
|
||||
using AliasVault.Shared.Models.WebApi.V2.Vault;
|
||||
|
||||
/// <summary>
|
||||
/// Request for POST /v2/Sharing/folders/{manifestId}. Uploads a new revision of a shared-folder manifest. Allowed
|
||||
/// for the manifest owner and for every user holding a <c>shared</c> grant on it; concurrency is guarded by the
|
||||
/// same optimistic revision check the root manifest upload uses.
|
||||
/// </summary>
|
||||
public class UpdateSharedFolderRequest
|
||||
{
|
||||
/// <summary>Gets or sets the encrypted folder manifest blob (AES-GCM ciphertext under the folder VEK, base64).</summary>
|
||||
public required string ManifestBlob { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the SHA-256 (hex) of the manifest ciphertext, for storage-layer integrity verification.</summary>
|
||||
public string? ManifestCiphertextHash { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the vault data model version string.</summary>
|
||||
public required string Version { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the manifest revision the client last synced; the new revision must be exactly one above it.</summary>
|
||||
public required long CurrentRevision { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the blob references of this manifest revision. Blob bytes are uploaded beforehand via POST /v2/Vault/blobs.</summary>
|
||||
public List<BlobReference> BlobReferences { get; set; } = [];
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
//-----------------------------------------------------------------------
|
||||
// <copyright file="UpdateSharedFolderResponse.cs" company="aliasvault">
|
||||
// Copyright (c) aliasvault. All rights reserved.
|
||||
// Licensed under the AGPLv3 license. See LICENSE.md file in the project root for full license information.
|
||||
// </copyright>
|
||||
//-----------------------------------------------------------------------
|
||||
|
||||
namespace AliasVault.Shared.Models.WebApi.V2.Sharing;
|
||||
|
||||
using AliasVault.Shared.Models.Enums;
|
||||
|
||||
/// <summary>
|
||||
/// Response for POST /v2/Sharing/folders/{manifestId}.
|
||||
/// </summary>
|
||||
public class UpdateSharedFolderResponse
|
||||
{
|
||||
/// <summary>Gets or sets the outcome: Ok when stored, Outdated when the client's revision is stale (re-pull and merge).</summary>
|
||||
public required VaultStatus Status { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the manifest's current server revision after this call.</summary>
|
||||
public required long NewRevisionNumber { get; set; }
|
||||
}
|
||||
@@ -35,4 +35,20 @@ public class Manifest
|
||||
|
||||
/// <summary>Gets or sets the blob references this manifest revision needs (so the client can detect cache misses).</summary>
|
||||
public List<BlobReference> BlobReferences { get; set; } = [];
|
||||
|
||||
/// <summary>Gets or sets the plaintext display name of a shared-folder manifest. Null for the root manifest.</summary>
|
||||
public string? Name { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the username of the manifest owner. Set only on manifests granted to the caller by another user.</summary>
|
||||
public string? OwnerUsername { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the manifest VEK wrapped with the caller's public key. Set only on manifests granted to the
|
||||
/// caller by another user; the caller unwraps it with their private key. Null on manifests the caller owns
|
||||
/// (the owner keeps their own copy of the folder VEK inside their root vault).
|
||||
/// </summary>
|
||||
public string? WrappedVek { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the wrap scheme of <see cref="WrappedVek"/> (e.g. "rsa-oaep"). Null when <see cref="WrappedVek"/> is null.</summary>
|
||||
public string? WrapScheme { get; set; }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user