diff --git a/src/TriunniaLabs.Arch.Sample.Server/Constants/Protectors.cs b/src/TriunniaLabs.Arch.Sample.Server/Constants/Protectors.cs
new file mode 100644
index 0000000..a76a1db
--- /dev/null
+++ b/src/TriunniaLabs.Arch.Sample.Server/Constants/Protectors.cs
@@ -0,0 +1,13 @@
+namespace TriunniaLabs.Arch.Sample.Server
+{
+ ///
+ /// Defines service key constants for protectors.
+ ///
+ public static class Protectors
+ {
+ ///
+ /// Gets the service key for protected byte array types.
+ ///
+ public const string ProtectedByteArrayServiceKey = nameof(ProtectedByteArrayServiceKey);
+ }
+}
diff --git a/src/TriunniaLabs.Arch.Sample.Server/Controllers/ApplicationApiSampleController.cs b/src/TriunniaLabs.Arch.Sample.Server/Controllers/ApplicationApiSampleController.cs
index 746550b..debb643 100644
--- a/src/TriunniaLabs.Arch.Sample.Server/Controllers/ApplicationApiSampleController.cs
+++ b/src/TriunniaLabs.Arch.Sample.Server/Controllers/ApplicationApiSampleController.cs
@@ -41,7 +41,7 @@ namespace TriunniaLabs.Arch.Sample.Server
}
///
- /// A sample method that shows how to work with the Keys Api.
+ /// A sample method that shows how to work with the Application Api.
///
internal async Task SampleAsync()
{
diff --git a/src/TriunniaLabs.Arch.Sample.Server/Controllers/CommentsApiSampleController.cs b/src/TriunniaLabs.Arch.Sample.Server/Controllers/CommentsApiSampleController.cs
new file mode 100644
index 0000000..c158fae
--- /dev/null
+++ b/src/TriunniaLabs.Arch.Sample.Server/Controllers/CommentsApiSampleController.cs
@@ -0,0 +1,60 @@
+using TriunniaLabs.Arch.Api.Comments.Client;
+using TriunniaLabs.Arch.Microservice;
+using TriunniaLabs.Memory.Protected;
+
+namespace TriunniaLabs.Arch.Sample.Server
+{
+ ///
+ /// A controller that shows how to work with the Comments Api.
+ ///
+ public class CommentsApiSampleController : ArchControllerBase
+ {
+ // Services
+ private readonly ICommentsApiClient _commentsApiClient;
+
+ ///
+ /// Constructor.
+ ///
+ /// Reference to the microservice context.
+ /// Reference to the Comments Api client.
+ public CommentsApiSampleController(IMicroserviceContext microserviceContext, ICommentsApiClient commentsApiClient) : base(microserviceContext)
+ {
+ _commentsApiClient = commentsApiClient ?? throw new ArgumentNullException(nameof(commentsApiClient));
+ }
+
+ ///
+ /// A sample method that shows how to work with the Comments Api.
+ ///
+ internal async Task SampleAsync()
+ {
+ // Comments are posted within a context. The context can be anything, like an article, forum category, another comment (in the case of a reply), etc.
+ var contextId = Guid.NewGuid().ToString().AsProtected(); // Id of the context to which the comment applies.
+ var contextType = 1; // Developer-defined type for the context
+ var commentType = 1; // Developer-defined type for the comment
+ var commentStatus = 1; // Developer-defined type for the comment's status
+ var commentText = "This is my comment".AsProtected(); // The encrypted text of the comment
+ var authorUserId = Guid.NewGuid().ToString().AsProtected(); // Id of the user who wrote the comment
+ var encryptionKey = "SampleEncryptionKey".AsProtected(); // Per-row encryption key for the entity
+ var commentId = Guid.NewGuid();
+
+ // Create a new comment in the system
+ await _commentsApiClient.CreateCommentAsync(contextId, contextType, commentType, commentStatus, commentText, authorUserId, encryptionKey);
+
+ // Get the specified comment
+ var comment = await _commentsApiClient.GetCommentAsync(contextId, contextType, commentId, encryptionKey);
+
+ // Get all comments within a context
+ var comments = await _commentsApiClient.GetCommentsAsync(contextId, contextType, encryptionKey);
+
+ // Update an existing comment
+ comment.CommentText = "This is my updated comment".AsProtected();
+ await _commentsApiClient.UpdateCommentAsync(comment, encryptionKey);
+
+ // Delete the specified comment from the context
+ await _commentsApiClient.DeleteCommentAsync(contextId, contextType, comment.CommentId);
+
+ // Delete the entire comment thread
+ await _commentsApiClient.DeleteCommentsAsync(contextId, contextType);
+ }
+ }
+}
diff --git a/src/TriunniaLabs.Arch.Sample.Server/Controllers/EmailApiRelaySampleController.cs b/src/TriunniaLabs.Arch.Sample.Server/Controllers/EmailApiRelaySampleController.cs
new file mode 100644
index 0000000..06cc95b
--- /dev/null
+++ b/src/TriunniaLabs.Arch.Sample.Server/Controllers/EmailApiRelaySampleController.cs
@@ -0,0 +1,42 @@
+using TriunniaLabs.Arch.Microservice;
+using TriunniaLabs.Arch.Relay.Email.Blazor;
+
+namespace TriunniaLabs.Arch.Sample.Server
+{
+ ///
+ /// A controller that shows how to work with the Email Api Relay Client.
+ /// This controller demonstrates using the Relay client from a Blazor page.
+ /// Relay clients contact their corresponding server endpoints that are brought in via TriunniaLabs.Arch.Relay.Email.Server.
+ /// Those server endpoints then contact the normal Email Api Client.
+ ///
+ public class EmailApiRelaySampleController : ArchControllerBase
+ {
+ // Services
+ private readonly IEmailApiRelayClient _emailApiRelayClient;
+
+ ///
+ /// Constructor.
+ ///
+ /// Reference to the microservice context.
+ /// Reference to the Email Api Relay client.
+ public EmailApiRelaySampleController(IMicroserviceContext microserviceContext, IEmailApiRelayClient emailApiRelayClient) : base(microserviceContext)
+ {
+ _emailApiRelayClient = emailApiRelayClient ?? throw new ArgumentNullException(nameof(emailApiRelayClient));
+ }
+
+ ///
+ /// A sample method that shows how to work with the Email Api.
+ ///
+ internal async Task SampleAsync()
+ {
+ var verifyToken = Guid.NewGuid(); // A verify token from the verification email
+
+ // Verifies the user's email address, given the verify token
+ await _emailApiRelayClient.VerifyEmailAddressAsync(verifyToken);
+
+ // Reports abuse of the email address, given the verify token.
+ // This would be called by the actual owner of the email address, who did not register the email in our system.
+ await _emailApiRelayClient.ReportAbuseAsync(verifyToken);
+ }
+ }
+}
diff --git a/src/TriunniaLabs.Arch.Sample.Server/Controllers/EmailApiSampleController.cs b/src/TriunniaLabs.Arch.Sample.Server/Controllers/EmailApiSampleController.cs
index b3fdec3..c4cdca2 100644
--- a/src/TriunniaLabs.Arch.Sample.Server/Controllers/EmailApiSampleController.cs
+++ b/src/TriunniaLabs.Arch.Sample.Server/Controllers/EmailApiSampleController.cs
@@ -5,7 +5,7 @@ using TriunniaLabs.Memory.Protected;
namespace TriunniaLabs.Arch.Sample.Server
{
///
- /// A controller that shows how to work the Email Api.
+ /// A controller that shows how to work with the Email Api.
///
public class EmailApiSampleController : ArchControllerBase
{
@@ -16,7 +16,7 @@ namespace TriunniaLabs.Arch.Sample.Server
/// Constructor.
///
/// Reference to the microservice context.
- /// Reference to the Tokens Api client.
+ /// Reference to the Email Api client.
public EmailApiSampleController(IMicroserviceContext microserviceContext, IEmailApiClient emailApiClient) : base(microserviceContext)
{
_emailApiClient = emailApiClient ?? throw new ArgumentNullException(nameof(emailApiClient));
@@ -45,9 +45,12 @@ namespace TriunniaLabs.Arch.Sample.Server
var emailResponse = await _emailApiClient.GetEmailAddressAsync(emailAddress.Hash, encryptionKey); // Get by hash
emailResponse = await _emailApiClient.GetEmailAddressAsync(emailId, encryptionKey); // Get by Id
- // Email verification procedure. This will send an email to the user with the verify token. Record its verification or abuse with the other methods.
+ // Email verification procedure. This will send an email to the user with the verify token.
await _emailApiClient.InitEmailVerifyAsync(emailId, encryptionKey, ipAddress);
var result = await _emailApiClient.CommitEmailVerifyAsync(verifyToken, ipAddress);
+
+ // Reports abuse of the email address, given the verify token.
+ // This would be called by the actual owner of the email address, who did not register the email in our system.
result = await _emailApiClient.ReportAbuseAsync(verifyToken);
// When using Postmark, receive their webhooks and forward to the Email Api with this call.
diff --git a/src/TriunniaLabs.Arch.Sample.Server/Controllers/EntityApiSampleController.cs b/src/TriunniaLabs.Arch.Sample.Server/Controllers/EntityApiSampleController.cs
new file mode 100644
index 0000000..9d76b92
--- /dev/null
+++ b/src/TriunniaLabs.Arch.Sample.Server/Controllers/EntityApiSampleController.cs
@@ -0,0 +1,100 @@
+using System.Text;
+using System.Text.Json;
+using TriunniaLabs.Arch.Api.Entity.Client;
+using TriunniaLabs.Arch.Microservice;
+using TriunniaLabs.Memory.Protected;
+
+namespace TriunniaLabs.Arch.Sample.Server
+{
+ ///
+ /// A controller that shows how to work with the Entity Api.
+ ///
+ public class EntityApiSampleController : ArchControllerBase
+ {
+ // Services
+ private readonly IEntityApiClient _entityApiClient;
+ private readonly IProtector _byteArrayProtector;
+
+ ///
+ /// Constructor.
+ ///
+ /// Reference to the microservice context.
+ /// Reference to the Entity Api client.
+ /// A custom protector for protecting byte[] data types.
+ public EntityApiSampleController(IMicroserviceContext microserviceContext, IEntityApiClient entityApiClient,
+ [FromKeyedServices(Protectors.ProtectedByteArrayServiceKey)] IProtector byteArrayProtector)
+ : base(microserviceContext)
+ {
+ _entityApiClient = entityApiClient ?? throw new ArgumentNullException(nameof(entityApiClient));
+ _byteArrayProtector = byteArrayProtector ?? throw new ArgumentNullException(nameof(byteArrayProtector));
+ }
+
+ ///
+ /// A sample method that shows how to work with the Entity Api.
+ ///
+ internal async Task SampleAsync()
+ {
+ var entity = new SampleEntity { Id = Guid.NewGuid(), Name = "MyEntity", Value = 1234 };
+ var encryptionKey = "SampleEncryptionKey".AsProtected(); // Per-row encryption key for the entity
+ var entityId = entity.Id.AsProtected();
+ var entityType = entity.GetType().FullName!.AsProtected();
+
+ // Serialize the entity to bytes if you want to do your own serialization (and protection)
+ var serialized = JsonSerializer.Serialize(entity);
+ var bytes = Encoding.UTF8.GetBytes(serialized);
+
+
+ // Create a new entity in the system with the specified id
+ await _entityApiClient.CreateEntityAsync(entityId, entity, _byteArrayProtector, encryptionKey);
+ await _entityApiClient.CreateEntityAsync(entityId, entityType, bytes, encryptionKey);
+ await _entityApiClient.CreateEntityAsync(entityId, bytes, encryptionKey);
+
+ // Update an existing entity in the system
+ entity.Value = 5678;
+ await _entityApiClient.UpdateEntityAsync(entityId, entity, _byteArrayProtector, encryptionKey);
+ await _entityApiClient.UpdateEntityAsync(entityId, entityType, bytes, encryptionKey);
+ await _entityApiClient.UpdateEntityAsync(entityId, bytes, encryptionKey);
+
+
+ // Get the entity and metadata, then deserialize
+ var entityResponse = await _entityApiClient.GetEntityAsync(entityId, entityType, encryptionKey);
+ entityResponse = await _entityApiClient.GetEntityAsync(entityId, encryptionKey);
+ entity = JsonSerializer.Deserialize(entityResponse!.Data.GetDecryptedValue());
+
+ // Get the deserialized entity immediately
+ entity = await _entityApiClient.GetEntityDeserializedAsync(entityId, encryptionKey, _byteArrayProtector);
+
+
+ // The Entity Api records and versions all changes to an entity. Get the history of all changes and their metadata with this call.
+ var history = await _entityApiClient.GetEntityHistoryAsync(entityId, entityType, encryptionKey);
+ history = await _entityApiClient.GetEntityHistoryAsync(entityId, encryptionKey);
+
+ // Or just get the history of the entities directly without any metadata
+ var deserializedHistory = await _entityApiClient.GetEntityHistoryDeserializedAsync(entityId, encryptionKey, _byteArrayProtector);
+
+ // Gets the archived version of the specified entity and its metadata
+ entityResponse = await _entityApiClient.GetEntityArchivedAsync(entityId, entityType, encryptionKey);
+ entityResponse = await _entityApiClient.GetEntityArchivedAsync(entityId, encryptionKey);
+
+ // Or just get the archived version without any metadata
+ entity = await _entityApiClient.GetEntityArchivedDeserializedAsync(entityId, encryptionKey, _byteArrayProtector);
+
+
+ // Mark the entity has deleted without actually deleting it
+ await _entityApiClient.SoftDeleteEntityAsync(entityId, entityType);
+ await _entityApiClient.SoftDeleteEntityAsync(entityId);
+
+
+ // Purge the entity from the system. This is a hard delete. It also deletes all archives and histories.
+ await _entityApiClient.PurgeEntityAsync(entityId, entityType);
+ await _entityApiClient.PurgeEntityAsync(entityId);
+ }
+
+ private class SampleEntity
+ {
+ public Guid Id { get; set; }
+ public string Name { get; set; }
+ public int Value { get; set; }
+ }
+ }
+}
diff --git a/src/TriunniaLabs.Arch.Sample.Server/Controllers/FilesApiSampleController.cs b/src/TriunniaLabs.Arch.Sample.Server/Controllers/FilesApiSampleController.cs
new file mode 100644
index 0000000..b4e363b
--- /dev/null
+++ b/src/TriunniaLabs.Arch.Sample.Server/Controllers/FilesApiSampleController.cs
@@ -0,0 +1,70 @@
+using TriunniaLabs.Arch.Api.Files.Client;
+using TriunniaLabs.Arch.Microservice;
+using TriunniaLabs.Memory.Protected;
+
+namespace TriunniaLabs.Arch.Sample.Server
+{
+ ///
+ /// A controller that shows how to work with the Files Api.
+ ///
+ public class FilesApiSampleController : ArchControllerBase
+ {
+ // Services
+ private readonly IFilesApiClient _filesApiClient;
+
+ ///
+ /// Constructor.
+ ///
+ /// Reference to the microservice context.
+ /// Reference to the Files Api client.
+ public FilesApiSampleController(IMicroserviceContext microserviceContext, IFilesApiClient filesApiClient) : base(microserviceContext)
+ {
+ _filesApiClient = filesApiClient ?? throw new ArgumentNullException(nameof(filesApiClient));
+ }
+
+ ///
+ /// A sample method that shows how to work with the Files Api.
+ ///
+ internal async Task SampleAsync()
+ {
+ var applicationId = MicroserviceContext.Settings.Application.ApplicationId; // Id of this application
+ var folder = "MySampleFolder".AsProtected(); // Files are stored in folders
+ var filename = "MySampleFile".AsProtected(); // The name of the file
+ var encryptionKey = "SampleEncryptionKey".AsProtected(); // Per-row encryption key for the file
+ var fileContents = new byte[] { 1, 2, 3, 4 };
+ using var fileStream = new MemoryStream(fileContents);
+
+ // Upload a file; overwrite if it exists Protect the stream to upload it.
+ var protectedStream = new Protected(fileStream, encryptionKey.GetDecryptedValue());
+ await _filesApiClient.UploadFileAsync(applicationId, folder, filename, protectedStream, encryptionKey);
+
+ // Download a file via streaming
+ protectedStream = await _filesApiClient.DownloadFileAsync(applicationId, folder, filename, encryptionKey);
+ using (var stream = protectedStream.GetDecryptedValueAsStream()) // Decrypt the stream for writing, if you want
+ {
+ // Save the stream to a file
+ }
+
+ // Download a file and get its bytes directly. Does not stream. Use for small files only.
+ var protectedFileBytes = await _filesApiClient.DownloadSmallFileAsync(applicationId, folder, filename, encryptionKey);
+
+ // Check if the file exists
+ var exists = await _filesApiClient.GetFileExistsAsync(applicationId, folder, filename);
+
+ // Gets the list of all files stored in the folder
+ var filenames = await _filesApiClient.GetFilesAsync(applicationId, folder);
+
+ // Get the size of the file, in bytes
+ var size = await _filesApiClient.GetFileSizeAsync(applicationId, folder, filename);
+
+ // Get the list of all folders in the application
+ var folders = await _filesApiClient.GetFoldersAsync(applicationId);
+
+ // Delete the specified file from the server
+ await _filesApiClient.DeleteFileAsync(applicationId, folder, filename);
+
+ // Delete the specified folder and all files within from the server, for the application
+ await _filesApiClient.DeleteFolderAsync(applicationId);
+ }
+ }
+}
diff --git a/src/TriunniaLabs.Arch.Sample.Server/Controllers/KeysApiSampleController.cs b/src/TriunniaLabs.Arch.Sample.Server/Controllers/KeysApiSampleController.cs
index 122be20..51cf180 100644
--- a/src/TriunniaLabs.Arch.Sample.Server/Controllers/KeysApiSampleController.cs
+++ b/src/TriunniaLabs.Arch.Sample.Server/Controllers/KeysApiSampleController.cs
@@ -5,7 +5,7 @@ using TriunniaLabs.Memory.Protected;
namespace TriunniaLabs.Arch.Sample.Server
{
///
- /// A controller that shows how to work the Keys Api.
+ /// A controller that shows how to work with the Keys Api.
///
public class KeysApiSampleController : ArchControllerBase
{
diff --git a/src/TriunniaLabs.Arch.Sample.Server/Controllers/TokensApiSampleController.cs b/src/TriunniaLabs.Arch.Sample.Server/Controllers/TokensApiSampleController.cs
index 434a8ca..9d8255b 100644
--- a/src/TriunniaLabs.Arch.Sample.Server/Controllers/TokensApiSampleController.cs
+++ b/src/TriunniaLabs.Arch.Sample.Server/Controllers/TokensApiSampleController.cs
@@ -5,7 +5,7 @@ using TriunniaLabs.Memory.Protected;
namespace TriunniaLabs.Arch.Sample.Server
{
///
- /// A controller that shows how to work the Tokens Api.
+ /// A controller that shows how to work with the Tokens Api.
///
public class TokensApiSampleController : ArchControllerBase
{
diff --git a/src/TriunniaLabs.Arch.Sample.Server/Controllers/UsersApiRelaySampleController.cs b/src/TriunniaLabs.Arch.Sample.Server/Controllers/UsersApiRelaySampleController.cs
new file mode 100644
index 0000000..9d15aa6
--- /dev/null
+++ b/src/TriunniaLabs.Arch.Sample.Server/Controllers/UsersApiRelaySampleController.cs
@@ -0,0 +1,101 @@
+using TriunniaLabs.Arch.Microservice;
+using TriunniaLabs.Arch.Microservice.Client;
+using TriunniaLabs.Arch.Relay.Users.Blazor;
+using TriunniaLabs.Memory.Protected;
+using TriunniaLabs.Blazor.ZeroKnowledge;
+
+namespace TriunniaLabs.Arch.Sample.Server
+{
+ ///
+ /// A controller that shows how to work with the Users Api Relay Client.
+ /// This controller demonstrates using the Relay client from a Blazor page.
+ /// Relay clients contact their corresponding server endpoints that are brought in via TriunniaLabs.Arch.Relay.Users.Server.
+ /// Those server endpoints then contact the normal Users Api Client.
+ ///
+ public class UsersApiRelaySampleController : ArchControllerBase
+ {
+ // Services
+ private readonly IUsersApiRelayClient _usersApiRelayClient;
+ private readonly IZeroKnowledgeService _zeroKnowledgeService;
+
+ ///
+ /// Constructor.
+ ///
+ /// Reference to the microservice context.
+ /// Reference to the Users Api Relay client.
+ /// Client-side logic for the zero-knowledge protocol in Blazor.
+ public UsersApiRelaySampleController(IMicroserviceContext microserviceContext, IUsersApiRelayClient usersApiRelayClient, IZeroKnowledgeService zeroKnowledgeService) : base(microserviceContext)
+ {
+ _usersApiRelayClient = usersApiRelayClient ?? throw new ArgumentNullException(nameof(usersApiRelayClient));
+ _zeroKnowledgeService = zeroKnowledgeService ?? throw new ArgumentNullException(nameof(zeroKnowledgeService));
+ }
+
+ ///
+ /// A sample method that shows how to work with the Users Api.
+ ///
+ internal async Task SampleAsync()
+ {
+ var encryptionKey = "SampleEncryptionKey".AsProtected(); // Per-row encryption key for the email address
+ var emailAddress = "sample@email.address".AsProtected(encryptionKey.GetDecryptedValue()); // Per-row encrypted email address
+ var checkEmailAddress = "sample@email.address".AsRelayProtected(); // Relay encrypted email address (lower security)
+ var ipAddress = IpAddress.AsProtected(); // User's ip address
+ var sessionToken = Guid.NewGuid().ToString().AsProtected(); // Authenticated session token
+ var resetToken = Guid.NewGuid().AsProtected(); // Password reset token
+ var deleteToken = Guid.NewGuid().AsProtected(); // Delete account token
+
+
+ // Construct origin graph -- the mechanism that verifies the user's password without knowing it
+ var originGraph = await _zeroKnowledgeService.GenerateOriginGraphAsync("password");
+
+ // Generate the key file, then save keyFileContents to a file. The file itself is password-protected via metadataPassword
+ var cycle = await _zeroKnowledgeService.GenerateHamiltonianCycleAsync(originGraph, "password");
+ var keyFileContents = await _zeroKnowledgeService.SerializeZeroKnowledgeMetadataAsync(originGraph, cycle, "metadataPassword");
+
+ // Read origin graph from key file
+ var (origin, cycle2) = await _zeroKnowledgeService.DeserializeZeroKnowledgeMetadataAsync(keyFileContents, "metadataPassword", "password");
+
+ // Set http headers on the client for authentication
+ _usersApiRelayClient.SetAuthorizationHeader("MySampleAuthScheme", sessionToken); // Authentication scheme must match appsettings.json
+
+
+ // Register a new user in the system
+ sessionToken = await _usersApiRelayClient.RegisterAsync(emailAddress, originGraph);
+
+ // Sign in to the system. It requires passing up to 12 challenges.
+ // The server knows the user's origin graph. It is asking the user to validate it by issuing challenges that the user answers.
+ // The Relay client does this procedure for you and returns the session token if the user authenticates successfully.
+ sessionToken = await _usersApiRelayClient.LoginAsync(emailAddress, originGraph, cycle);
+
+
+ // End the authenticated session; log the user out of the system
+ await _usersApiRelayClient.LogoutAsync();
+
+
+ // Check if the email address already exists in the system
+ var exists = await _usersApiRelayClient.CheckEmailAsync(checkEmailAddress);
+
+ // Get the user's email address and metadata
+ var emailResponse = await _usersApiRelayClient.GetEmailAsync();
+
+ // Update the user's email address
+ await _usersApiRelayClient.UpdateEmailAsync("updated.sample@email.address".AsProtected());
+
+ // Send a verification email for the user
+ await _usersApiRelayClient.SendVerificationEmailAsync();
+
+
+ // Validate a user's session token. Used to verify that a user is in fact signed in
+ var validated = _usersApiRelayClient.ValidateAsync(sessionToken);
+
+
+ // Password reset procedure
+ await _usersApiRelayClient.InitiatePasswordResetAsync(emailAddress); // Sends a verification email with resetToken
+ await _usersApiRelayClient.ValidatePasswordResetTokenAsync(emailAddress, resetToken);
+ await _usersApiRelayClient.CommitPasswordResetAsync(emailAddress, resetToken, originGraph); // Generate a new origin graph based on a new password for the user
+
+ // Delete account procedure
+ await _usersApiRelayClient.InitiateDeleteAccountAsync(); // Sends a confirmation email with deleteToken
+ await _usersApiRelayClient.CommitDeleteAccountAsync(deleteToken);
+ }
+ }
+}
diff --git a/src/TriunniaLabs.Arch.Sample.Server/Controllers/UsersApiSampleController.cs b/src/TriunniaLabs.Arch.Sample.Server/Controllers/UsersApiSampleController.cs
new file mode 100644
index 0000000..3159610
--- /dev/null
+++ b/src/TriunniaLabs.Arch.Sample.Server/Controllers/UsersApiSampleController.cs
@@ -0,0 +1,119 @@
+using TriunniaLabs.Arch.Api.Users.Client;
+using TriunniaLabs.Arch.Microservice;
+using TriunniaLabs.Arch.Microservice.Client;
+using TriunniaLabs.Memory.Protected;
+using TriunniaLabs.ZeroKnowledge.Client;
+
+namespace TriunniaLabs.Arch.Sample.Server
+{
+ ///
+ /// A controller that shows how to work with the Users Api.
+ ///
+ public class UsersApiSampleController : ArchControllerBase
+ {
+ // Services
+ private readonly IUsersApiClient _usersApiClient;
+ private readonly IZeroKnowledgeService _zeroKnowledgeService;
+
+ ///
+ /// Constructor.
+ ///
+ /// Reference to the microservice context.
+ /// Reference to the Users Api client.
+ /// Client-side logic for the zero-knowledge protocol.
+ public UsersApiSampleController(IMicroserviceContext microserviceContext, IUsersApiClient usersApiClient, IZeroKnowledgeService zeroKnowledgeService) : base(microserviceContext)
+ {
+ _usersApiClient = usersApiClient ?? throw new ArgumentNullException(nameof(usersApiClient));
+ _zeroKnowledgeService = zeroKnowledgeService ?? throw new ArgumentNullException(nameof(zeroKnowledgeService));
+ }
+
+ ///
+ /// A sample method that shows how to work with the Users Api.
+ ///
+ internal async Task SampleAsync()
+ {
+ var encryptionKey = "SampleEncryptionKey".AsProtected(); // Per-row encryption key for the email address
+ var emailAddress = "sample@email.address".AsProtected(encryptionKey.GetDecryptedValue()); // Per-row encrypted email address
+ var checkEmailAddress = "sample@email.address".AsRelayProtected(); // Relay encrypted email address (lower security)
+ var ipAddress = IpAddress.AsProtected(); // User's ip address
+ var sessionToken = Guid.NewGuid().ToString().AsProtected(); // Authenticated session token
+ var resetToken = Guid.NewGuid().AsProtected(); // Password reset token
+ var deleteToken = Guid.NewGuid().AsProtected(); // Delete account token
+
+
+ // Construct origin graph -- the mechanism that verifies the user's password without knowing it
+ var originGraph = _zeroKnowledgeService.GenerateOriginGraph("password");
+ var serializedOrigin = _zeroKnowledgeService.SerializeOriginGraph(originGraph);
+ var protectedOrigin = new Protected(serializedOrigin);
+
+ // Generate the key file, then save keyFileContents to a file. The file itself is password-protected via metadataPassword
+ var cycle = _zeroKnowledgeService.GenerateHamiltonianCycle(originGraph, "password");
+ var keyFileContents = _zeroKnowledgeService.SerializeZeroKnowledgeMetadata(originGraph, cycle, "metadataPassword");
+
+ // Read origin graph from key file
+ _zeroKnowledgeService.DeserializeZeroKnowledgeMetadata(keyFileContents, "metadataPassword", "password", out originGraph!, out cycle!);
+
+ // Set http headers on the client for authentication
+ _usersApiClient.SetAuthorizationHeader(sessionToken);
+ _usersApiClient.SetIpAddressOriginHeader(ipAddress);
+
+
+ // Register a new user in the system
+ sessionToken = await _usersApiClient.RegisterAsync(emailAddress, protectedOrigin);
+
+ // Sign in to the system. It requires passing up to 12 challenges.
+ // The server knows the user's origin graph. It is asking the user to validate it by issuing challenges that the user answers.
+ // Challenges have their own session token. Not to be confused with the user's session token.
+ await _usersApiClient.InitiateLoginAsync(emailAddress);
+ for (int i = 0; i < 12; i++)
+ {
+ // Create the challenge
+ var challengeResponse = await _usersApiClient.CreateChallengeAsync(emailAddress);
+ var challenge = new ZeroKnowledge.Client.ChallengeResponse { Seed = challengeResponse.Seed, SessionToken = challengeResponse.SessionToken };
+
+ // Answer the challenge
+ var answer = _zeroKnowledgeService.AnswerChallenge(challenge, originGraph, cycle);
+ sessionToken = await _usersApiClient.EvaluateAnswerAsync(emailAddress, challenge.SessionToken, answer.Isomorphism.ToArray(), answer.Cycle);
+ if (sessionToken is not null && !sessionToken.IsEmpty)
+ {
+ // Session token returned -- User is authenticated
+ }
+ }
+ // Authentication fails if the loop is finished after 12 tries with no session token
+
+ // End the authenticated session; log the user out of the system
+ await _usersApiClient.LogoutAsync();
+
+
+ // Check if the email address already exists in the system
+ var exists = await _usersApiClient.CheckEmailAsync(checkEmailAddress);
+
+ // Get the user's email address and metadata
+ var emailResponse = await _usersApiClient.GetEmailAsync();
+
+ // Update the user's email address
+ await _usersApiClient.UpdateEmailAsync("updated.sample@email.address".AsProtected());
+
+ // Send a verification email for the user
+ await _usersApiClient.SendVerificationEmailAsync();
+
+
+ // Get user metadata
+ var user = await _usersApiClient.GetUserAsync();
+
+
+ // Validate a user's session token. Used to verify that a user is in fact signed in
+ var userId = _usersApiClient.ValidateSessionAsync(sessionToken, ipAddress);
+
+
+ // Password reset procedure
+ await _usersApiClient.InitiatePasswordResetAsync(emailAddress); // Sends a verification email with resetToken
+ await _usersApiClient.ValidatePasswordResetTokenAsync(emailAddress, resetToken);
+ await _usersApiClient.CommitPasswordResetAsync(emailAddress, resetToken, protectedOrigin); // Generate a new origin graph based on a new password for the user
+
+ // Delete account procedure
+ await _usersApiClient.InitiateDeleteAccountAsync(); // Sends a confirmation email with deleteToken
+ await _usersApiClient.CommitDeleteAccountAsync(deleteToken);
+ }
+ }
+}
diff --git a/src/TriunniaLabs.Arch.Sample.Server/Models/ProtectorDefinitions.cs b/src/TriunniaLabs.Arch.Sample.Server/Models/ProtectorDefinitions.cs
new file mode 100644
index 0000000..1eb1425
--- /dev/null
+++ b/src/TriunniaLabs.Arch.Sample.Server/Models/ProtectorDefinitions.cs
@@ -0,0 +1,26 @@
+using TriunniaLabs.Memory.Protected;
+
+namespace TriunniaLabs.Arch.Sample.Server
+{
+ ///
+ /// Defines the protectors used by the application.
+ ///
+ public class ProtectorDefinitions
+ {
+ ///
+ /// Defines the protector for byte arrays.
+ ///
+ ///
+ /// In Program.cs, the call to AddTriunniaLabsMicroservice() will internally call AddTriunniaLabsMemoryProtection(),
+ /// which will scan types in the assemblies for any protector registrations such as this one and automatically create protectors
+ /// for them. The protector matching this definition can be obtained by injecting
+ /// [FromKeyedServices(Protectors.ProtectedByteArrayServiceKey)] IProtector.
+ ///
+ [HashAlgorithm(HmacSha512HashAlgorithm.ServiceKey)]
+ [EncryptionAlgorithm(AesEncryptionAlgorithm.ServiceKey, Order = 1)]
+ [EncryptionAlgorithm(GZipCompressionAlgorithm.ServiceKey, Order = 2)]
+ [StringConverter(Base65536StringConverter.ServiceKey)]
+ [Protector(Protectors.ProtectedByteArrayServiceKey)]
+ public Protected ByteArrayProtector { get; set; } = Protected.Empty;
+ }
+}
diff --git a/src/TriunniaLabs.Arch.Sample.Server/Program.cs b/src/TriunniaLabs.Arch.Sample.Server/Program.cs
index dabd878..c0cc976 100644
--- a/src/TriunniaLabs.Arch.Sample.Server/Program.cs
+++ b/src/TriunniaLabs.Arch.Sample.Server/Program.cs
@@ -12,6 +12,7 @@ using TriunniaLabs.Arch.Relay.Email.Server;
using TriunniaLabs.Arch.Relay.Users.Server;
using TriunniaLabs.Arch.Sample.Server;
using TriunniaLabs.Memory.Protected;
+using TriunniaLabs.ZeroKnowledge.Client;
// Start the ASP.NET application builder
var builder = WebApplication.CreateBuilder(args);
@@ -27,6 +28,7 @@ builder.Services.AddTriunniaLabsUsersApi(builder.Configuration, builder.Services
builder.Services.AddTriunniaLabsCommentsApi(builder.Configuration, builder.Services.AddStandardMessagePackFormatters);
builder.Services.AddTriunniaLabsEntityApi(builder.Configuration, builder.Services.AddStandardMessagePackFormatters);
builder.Services.AddTriunniaLabsFilesApi(builder.Configuration, builder.Services.AddStandardMessagePackFormatters);
+builder.Services.AddZeroKnowledge();
// Configuration
builder.Services.AddRouting(options => options.LowercaseUrls = true);
diff --git a/src/TriunniaLabs.Arch.Sample.Server/TriunniaLabs.Arch.Sample.Server.csproj b/src/TriunniaLabs.Arch.Sample.Server/TriunniaLabs.Arch.Sample.Server.csproj
index d4cc6d2..b8ac45e 100644
--- a/src/TriunniaLabs.Arch.Sample.Server/TriunniaLabs.Arch.Sample.Server.csproj
+++ b/src/TriunniaLabs.Arch.Sample.Server/TriunniaLabs.Arch.Sample.Server.csproj
@@ -15,8 +15,12 @@
+
+
+
+