Skip to content

Commit

Permalink
Merge pull request #116 from PinguApps/85-feat-account-list-logs
Browse files Browse the repository at this point in the history
Implemented list logs
  • Loading branch information
pingu2k4 authored Aug 6, 2024
2 parents 03d34de + 0f599b4 commit 0deb6d7
Show file tree
Hide file tree
Showing 13 changed files with 769 additions and 11 deletions.
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,11 +139,11 @@ string emailAddressOrErrorMessage = userResponse.Result.Match(

## ⌛ Progress
### Server & Client
![17 / 288](https://progress-bar.dev/17/?scale=288&suffix=%20/%20288&width=500)
![18 / 288](https://progress-bar.dev/18/?scale=288&suffix=%20/%20288&width=500)
### Server Only
![2 / 195](https://progress-bar.dev/2/?scale=195&suffix=%20/%20195&width=300)
### Client Only
![15 / 93](https://progress-bar.dev/15/?scale=93&suffix=%20/%2093&width=300)
![16 / 93](https://progress-bar.dev/16/?scale=93&suffix=%20/%2093&width=300)

### 🔑 Key
| Icon | Definition |
Expand All @@ -153,7 +153,7 @@ string emailAddressOrErrorMessage = userResponse.Result.Match(
|| There is currently no intention to implement the endpoint for the given SDK type (client or server) |

### Account
![17 / 52](https://progress-bar.dev/17/?scale=52&suffix=%20/%2052&width=120)
![18 / 52](https://progress-bar.dev/18/?scale=52&suffix=%20/%2052&width=120)

| Endpoint | Client | Server |
|:-:|:-:|:-:|
Expand All @@ -163,7 +163,7 @@ string emailAddressOrErrorMessage = userResponse.Result.Match(
| [List Identities](https://appwrite.io/docs/references/1.5.x/client-rest/account#listIdentities) |||
| [Delete Identity](https://appwrite.io/docs/references/1.5.x/client-rest/account#deleteIdentity) |||
| [Create JWT](https://appwrite.io/docs/references/1.5.x/client-rest/account#createJWT) |||
| [List Logs](https://appwrite.io/docs/references/1.5.x/client-rest/account#listLogs) | ||
| [List Logs](https://appwrite.io/docs/references/1.5.x/client-rest/account#listLogs) | ||
| [Update MFA](https://appwrite.io/docs/references/1.5.x/client-rest/account#updateMFA) |||
| [Add Authenticator](https://appwrite.io/docs/references/1.5.x/client-rest/account#createMfaAuthenticator) |||
| [Verify Authenticator](https://appwrite.io/docs/references/1.5.x/client-rest/account#updateMfaAuthenticator) |||
Expand Down
19 changes: 19 additions & 0 deletions src/PinguApps.Appwrite.Client/Clients/AccountClient.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using PinguApps.Appwrite.Client.Clients;
Expand All @@ -8,6 +9,7 @@
using PinguApps.Appwrite.Shared;
using PinguApps.Appwrite.Shared.Requests;
using PinguApps.Appwrite.Shared.Responses;
using PinguApps.Appwrite.Shared.Utils;

namespace PinguApps.Appwrite.Client;

Expand Down Expand Up @@ -278,4 +280,21 @@ public async Task<AppwriteResult<Jwt>> CreateJwt()
return e.GetExceptionResponse<Jwt>();
}
}

/// <inheritdoc/>
public async Task<AppwriteResult<LogsList>> ListLogs(List<Query>? queries = null)
{
try
{
var queryStrings = queries?.Select(x => x.GetQueryString()) ?? [];

var result = await _accountApi.ListLogs(Session, queryStrings);

return result.GetApiResponse();
}
catch (Exception e)
{
return e.GetExceptionResponse<LogsList>();
}
}
}
9 changes: 9 additions & 0 deletions src/PinguApps.Appwrite.Client/Clients/IAccountClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using PinguApps.Appwrite.Shared;
using PinguApps.Appwrite.Shared.Requests;
using PinguApps.Appwrite.Shared.Responses;
using PinguApps.Appwrite.Shared.Utils;

namespace PinguApps.Appwrite.Client;

Expand Down Expand Up @@ -132,4 +133,12 @@ public interface IAccountClient
/// </summary>
/// <returns>The JWT</returns>
Task<AppwriteResult<Jwt>> CreateJwt();

/// <summary>
/// Get the list of latest security activity logs for the currently logged in user. Each log returns user IP address, location and date and time of log.
/// <para><see href="https://appwrite.io/docs/references/1.5.x/client-rest/account#listLogs">Appwrite Docs</see></para>
/// </summary>
/// <param name="queries">Array of query strings generated using the Query class provided by the SDK. <see href="https://appwrite.io/docs/queries">Learn more about queries</see>. Only supported methods are limit and offset</param>
/// <returns>The Logs List</returns>
Task<AppwriteResult<LogsList>> ListLogs(List<Query>? queries = null);
}
4 changes: 4 additions & 0 deletions src/PinguApps.Appwrite.Client/Internals/IAccountApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,8 @@ internal interface IAccountApi : IBaseApi

[Post("/account/jwt")]
Task<IApiResponse<Jwt>> CreateJwt([Header("x-appwrite-session")] string? session);

[Get("/account/logs")]
[QueryUriFormat(System.UriFormat.Unescaped)]
Task<IApiResponse<LogsList>> ListLogs([Header("x-appwrite-session")] string? session, [Query(CollectionFormat.Multi), AliasAs("queries[]")] IEnumerable<string> queries);
}
9 changes: 2 additions & 7 deletions src/PinguApps.Appwrite.Playground/App.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
using Microsoft.Extensions.Configuration;
using PinguApps.Appwrite.Client;
using PinguApps.Appwrite.Server.Servers;
using PinguApps.Appwrite.Shared.Requests;
using PinguApps.Appwrite.Shared.Utils;

namespace PinguApps.Appwrite.Playground;
internal class App
Expand All @@ -21,12 +21,7 @@ public async Task Run(string[] args)
{
_client.SetSession(_session);

var request = new CreateEmailVerificationRequest
{
Url = "https://localhost:5001/abc123"
};

var response = await _client.Account.CreateJwt();
var response = await _client.Account.ListLogs([Query.Limit(2)]);

Console.WriteLine(response.Result.Match(
account => account.ToString(),
Expand Down
52 changes: 52 additions & 0 deletions src/PinguApps.Appwrite.Shared/Responses/LogModel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
using System;
using System.Text.Json.Serialization;

namespace PinguApps.Appwrite.Shared.Responses;

/// <summary>
/// Ann Appwrite Log object
/// </summary>
/// <param name="Event">Event name</param>
/// <param name="UserId">User ID</param>
/// <param name="UserEmail">User Email</param>
/// <param name="UserName">User Name</param>
/// <param name="Mode">API mode when event triggered</param>
/// <param name="Ip">IP session in use when the session was created</param>
/// <param name="Time">Log creation date in ISO 8601 format</param>
/// <param name="OsCode">Operating system code name. View list of <see href="https://github.com/appwrite/appwrite/blob/master/docs/lists/os.json">Available Options</see></param>
/// <param name="OsName">Operating system name</param>
/// <param name="OsVersion">Operating system version</param>
/// <param name="ClientType">Client type</param>
/// <param name="ClientCode">Client code name. View list of <see href="https://github.com/appwrite/appwrite/blob/master/docs/lists/clients.json">Available Options</see></param>
/// <param name="ClientName">Client name</param>
/// <param name="ClientVersion">Client version</param>
/// <param name="ClientEngine">Client engine name</param>
/// <param name="ClientEngineVersion">Client engine version</param>
/// <param name="DeviceName">Device name</param>
/// <param name="DeviceBrand">Device brand name</param>
/// <param name="DeviceModel">Device model name</param>
/// <param name="CountryCode">Country two-character ISO 3166-1 alpha code</param>
/// <param name="CountryName">Country name</param>
public record LogModel(
[property: JsonPropertyName("event")] string Event,
[property: JsonPropertyName("userId")] string UserId,
[property: JsonPropertyName("userEmail")] string UserEmail,
[property: JsonPropertyName("userName")] string UserName,
[property: JsonPropertyName("mode")] string Mode,
[property: JsonPropertyName("ip")] string Ip,
[property: JsonPropertyName("time")] DateTime Time,
[property: JsonPropertyName("osCode")] string OsCode,
[property: JsonPropertyName("osName")] string OsName,
[property: JsonPropertyName("osVersion")] string OsVersion,
[property: JsonPropertyName("clientType")] string ClientType,
[property: JsonPropertyName("clientCode")] string ClientCode,
[property: JsonPropertyName("clientName")] string ClientName,
[property: JsonPropertyName("clientVersion")] string ClientVersion,
[property: JsonPropertyName("clientEngine")] string ClientEngine,
[property: JsonPropertyName("clientEngineVersion")] string ClientEngineVersion,
[property: JsonPropertyName("deviceName")] string DeviceName,
[property: JsonPropertyName("deviceBrand")] string DeviceBrand,
[property: JsonPropertyName("deviceModel")] string DeviceModel,
[property: JsonPropertyName("countryCode")] string CountryCode,
[property: JsonPropertyName("countryName")] string CountryName
);
14 changes: 14 additions & 0 deletions src/PinguApps.Appwrite.Shared/Responses/LogsList.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using System.Collections.Generic;
using System.Text.Json.Serialization;

namespace PinguApps.Appwrite.Shared.Responses;

/// <summary>
/// An Appwrite Logs List object
/// </summary>
/// <param name="Total">Total number of logs documents that matched your query.</param>
/// <param name="Logs">List of logs. Can be one of: <see cref="LogModel"/></param>
public record LogsList(
[property: JsonPropertyName("total")] int Total,
[property: JsonPropertyName("logs")] List<LogModel> Logs
);
88 changes: 88 additions & 0 deletions src/PinguApps.Appwrite.Shared/Utils/Query.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace PinguApps.Appwrite.Shared.Utils;
public class Query
{
[JsonPropertyName("method")]
public string Method { get; private set; }

[JsonPropertyName("attribute")]
public string? Attribute { get; private set; }

[JsonPropertyName("values")]
public List<object>? Values { get; private set; }

private Query(string method, string? attribute, object? values)
{
Method = method;
Attribute = attribute;

if (values is IEnumerable<object> objects)
{
Values = objects.ToList();
}
else if (values is ICollection valuesList)
{
Values = [.. valuesList];
}
else if (values is not null)
{
Values = [values];
}
}

public string GetQueryString() => Uri.EscapeUriString(JsonSerializer.Serialize(this));

public static Query Equal(string attribute, object value) => new("equal", attribute, value);

public static Query NotEqual(string attribute, object value) => new("notEqual", attribute, value);

public static Query LessThan(string attribute, object value) => new("lessThan", attribute, value);

public static Query LessThanEqual(string attribute, object value) => new("lessThanEqual", attribute, value);

public static Query GreaterThan(string attribute, object value) => new("greaterThan", attribute, value);

public static Query GreaterThanEqual(string attribute, object value) => new("greaterThanEqual", attribute, value);

public static Query Search(string attribute, object value) => new("search", attribute, value);

public static Query IsNull(string attribute) => new("isNull", attribute, null);

public static Query IsNotNull(string attribute) => new("isNotNull", attribute, null);

public static Query StartsWith(string attribute, object value) => new("startsWith", attribute, value);

public static Query EndsWith(string attribute, object value) => new("endsWith", attribute, value);

public static Query Between(string attribute, string start, string end) => new("between", attribute, new List<string> { start, end });

public static Query Between(string attribute, int start, int end) => new("between", attribute, new List<int> { start, end });

public static Query Between(string attribute, double start, double end) => new("between", attribute, new List<double> { start, end });

public static Query Select(List<string> attributes) => new("select", null, attributes);

public static Query CursorAfter(string documentId) => new("cursorAfter", null, documentId);

public static Query CursorBefore(string documentId) => new("cursorBefore", null, documentId);

public static Query OrderAsc(string attribute) => new("orderAsc", attribute, null);

public static Query OrderDesc(string attribute) => new("orderDesc", attribute, null);

public static Query Limit(int limit) => new("limit", null, limit);

public static Query Offset(int offset) => new("offset", null, offset);

public static Query Contains(string attribute, object value) => new("contains", attribute, value);

public static Query Or(List<Query> queries) => new("or", null, queries);

public static Query And(List<Query> queries) => new("and", null, queries);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
using System.Net;
using PinguApps.Appwrite.Shared.Tests;
using PinguApps.Appwrite.Shared.Utils;
using RichardSzalay.MockHttp;

namespace PinguApps.Appwrite.Client.Tests.Clients.Account;
public partial class AccountClientTests
{
[Fact]
public async Task ListLogs_ShouldReturnSuccess_WhenApiCallSucceeds()
{
// Arrange
_mockHttp.Expect(HttpMethod.Get, $"{Constants.Endpoint}/account/logs")
.ExpectedHeaders(true)
.Respond(Constants.AppJson, Constants.LogsListResponse);

_appwriteClient.SetSession(Constants.Session);

// Act
var result = await _appwriteClient.Account.ListLogs();

// Assert
Assert.True(result.Success);
}

[Fact]
public async Task ListLogs_ShouldProvideQueries_WhenQueriesProvided()
{
// Arrange
var query = Query.Limit(5);

_mockHttp.Expect(HttpMethod.Get, $"{Constants.Endpoint}/account/logs")
.ExpectedHeaders(true)
.WithQueryString($"queries[]={query.GetQueryString()}")
.Respond(Constants.AppJson, Constants.LogsListResponse);

_appwriteClient.SetSession(Constants.Session);

// Act
var result = await _appwriteClient.Account.ListLogs([query]);

// Assert
Assert.True(result.Success);
}

[Fact]
public async Task ListLogs_ShouldHandleException_WhenApiCallFails()
{
// Arrange
_mockHttp.Expect(HttpMethod.Get, $"{Constants.Endpoint}/account/logs")
.ExpectedHeaders(true)
.Respond(HttpStatusCode.BadRequest, Constants.AppJson, Constants.AppwriteError);

_appwriteClient.SetSession(Constants.Session);

// Act
var result = await _appwriteClient.Account.ListLogs();

// Assert
Assert.True(result.IsError);
Assert.True(result.IsAppwriteError);
}

[Fact]
public async Task ListLogs_ShouldReturnErrorResponse_WhenExceptionOccurs()
{
// Arrange
_mockHttp.Expect(HttpMethod.Get, $"{Constants.Endpoint}/account/logs")
.ExpectedHeaders(true)
.Throw(new HttpRequestException("An error occurred"));

_appwriteClient.SetSession(Constants.Session);

// Act
var result = await _appwriteClient.Account.ListLogs();

// Assert
Assert.False(result.Success);
Assert.True(result.IsInternalError);
Assert.Equal("An error occurred", result.Result.AsT2.Message);
}
}
10 changes: 10 additions & 0 deletions tests/PinguApps.Appwrite.Client.Tests/Utils/ResponseUtilsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,16 @@ public async Task GetApiResponse_FailureButNullErrorContent_ThrowsException()
Assert.Throws<Exception>(() => mockApiResponse.Object.GetApiResponse());
}

[Fact]
public void GetApiResponse_FailureButNullError_ThrowsException()
{
var mockApiResponse = new Mock<IApiResponse<string>>();
mockApiResponse.SetupGet(r => r.IsSuccessStatusCode).Returns(false);
mockApiResponse.SetupGet(x => x.Error).Returns((ApiException)null!);

Assert.Throws<Exception>(() => mockApiResponse.Object.GetApiResponse());
}

[Fact]
public void GetExceptionResponse_ReturnsInternalError()
{
Expand Down
Loading

0 comments on commit 0deb6d7

Please sign in to comment.