Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

create session implementation #72

Merged
merged 16 commits into from
Jul 15, 2024
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
![11 / 298](https://progress-bar.dev/11/?scale=298&suffix=%20/%20298&width=500)
![12 / 298](https://progress-bar.dev/12/?scale=298&suffix=%20/%20298&width=500)
### Server Only
![2 / 195](https://progress-bar.dev/2/?scale=195&suffix=%20/%20195&width=300)
### Client Only
![9 / 93](https://progress-bar.dev/9/?scale=93&suffix=%20/%2093&width=300)
![10 / 93](https://progress-bar.dev/10/?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
![11 / 52](https://progress-bar.dev/11/?scale=52&suffix=%20/%2052&width=120)
![12 / 52](https://progress-bar.dev/12/?scale=52&suffix=%20/%2052&width=120)

| Endpoint | Client | Server |
|:-:|:-:|:-:|
Expand Down Expand Up @@ -188,7 +188,7 @@ string emailAddressOrErrorMessage = userResponse.Result.Match(
| [Update Magic URL Session](https://appwrite.io/docs/references/1.5.x/client-rest/account#updateMagicURLSession) | ⬛ | ❌ |
| [Create OAuth2 Session](https://appwrite.io/docs/references/1.5.x/client-rest/account#createOAuth2Session) | ⬛ | ❌ |
| [Update Phone Session](https://appwrite.io/docs/references/1.5.x/client-rest/account#updatePhoneSession) | ⬛ | ❌ |
| [Create Session](https://appwrite.io/docs/references/1.5.x/client-rest/account#createSession) | | ❌ |
| [Create Session](https://appwrite.io/docs/references/1.5.x/client-rest/account#createSession) | | ❌ |
| [Get Session](https://appwrite.io/docs/references/1.5.x/client-rest/account#getSession) | ⬛ | ❌ |
| [Update Session](https://appwrite.io/docs/references/1.5.x/client-rest/account#updateSession) | ⬛ | ❌ |
| [Delete Session](https://appwrite.io/docs/references/1.5.x/client-rest/account#deleteSession) | ⬛ | ❌ |
Expand Down
17 changes: 17 additions & 0 deletions src/PinguApps.Appwrite.Client/Clients/AccountClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -182,4 +182,21 @@ public async Task<AppwriteResult<Token>> CreateEmailToken(CreateEmailTokenReques
return e.GetExceptionResponse<Token>();
}
}

/// <inheritdoc/>
public async Task<AppwriteResult<Session>> CreateSession(CreateSessionRequest request)
{
try
{
request.Validate(true);

var result = await _accountApi.CreateSession(request);

return result.GetApiResponse();
}
catch (Exception e)
{
return e.GetExceptionResponse<Session>();
}
}
}
8 changes: 8 additions & 0 deletions src/PinguApps.Appwrite.Client/Clients/IAccountClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -84,4 +84,12 @@ public interface IAccountClient
/// <param name="request">The request content</param>
/// <returns>The token</returns>
Task<AppwriteResult<Token>> CreateEmailToken(CreateEmailTokenRequest request);

/// <summary>
/// Use this endpoint to create a session from token. Provide the userId and secret parameters from the successful response of authentication flows initiated by token creation. For example, magic URL and phone login.
/// <para><see href="https://appwrite.io/docs/references/1.5.x/client-rest/account#createSession">Appwrite Docs</see></para>
/// </summary>
/// <param name="request">The request content</param>
/// <returns>The session</returns>
Task<AppwriteResult<Session>> CreateSession(CreateSessionRequest request);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
using System;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using PinguApps.Appwrite.Client.Internals;

namespace PinguApps.Appwrite.Client.Handlers;
internal class ClientCookieSessionHandler : DelegatingHandler
{
private readonly Lazy<IAppwriteClient> _appwriteClient;

public ClientCookieSessionHandler(Lazy<IAppwriteClient> appwriteClient)
{
_appwriteClient = appwriteClient;
}

private IAppwriteClient AppwriteClient => _appwriteClient.Value;

protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var result = await base.SendAsync(request, cancellationToken);

SaveSession(result);

return result;
}

private void SaveSession(HttpResponseMessage response)
{
if (response.IsSuccessStatusCode)
{
if (response.Headers.TryGetValues("Set-Cookie", out var values))
{
var sessionCookie = values.FirstOrDefault(x => x.StartsWith("a_session", StringComparison.OrdinalIgnoreCase) && !x.Contains("legacy", StringComparison.OrdinalIgnoreCase));

if (sessionCookie is null)
return;

var afterEquals = sessionCookie.IndexOf('=') + 1;
var semicolonIndex = sessionCookie.IndexOf(';', afterEquals);
var base64 = sessionCookie.Substring(afterEquals, semicolonIndex - afterEquals);

if (string.Equals(base64, "deleted", StringComparison.OrdinalIgnoreCase))
{
AppwriteClient.SetSession(null);
return;
}

var decodedBytes = Convert.FromBase64String(base64);
var decoded = Encoding.UTF8.GetString(decodedBytes);

try
{
var sessionData = JsonSerializer.Deserialize<CookieSessionData>(decoded);

if (sessionData is null || sessionData.Id is null || sessionData.Secret is null)
return;

AppwriteClient.SetSession(sessionData.Secret);
}
catch (JsonException)
{
}
}
}
}
}
11 changes: 11 additions & 0 deletions src/PinguApps.Appwrite.Client/Internals/CookieSessionData.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using System.Text.Json.Serialization;

namespace PinguApps.Appwrite.Client.Internals;
internal class CookieSessionData
{
[JsonPropertyName("id")]
public string Id { get; set; } = default!;

[JsonPropertyName("secret")]
public string Secret { get; set; } = default!;
}
3 changes: 3 additions & 0 deletions src/PinguApps.Appwrite.Client/Internals/IAccountApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,7 @@ internal interface IAccountApi : IBaseApi

[Post("/account/tokens/email")]
Task<IApiResponse<Token>> CreateEmailToken(CreateEmailTokenRequest request);

[Post("/account/sessions/token")]
Task<IApiResponse<Session>> CreateSession(CreateSessionRequest request);
}
23 changes: 17 additions & 6 deletions src/PinguApps.Appwrite.Client/ServiceCollectionExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Net.Http;
using Microsoft.Extensions.DependencyInjection;
using PinguApps.Appwrite.Client.Handlers;
using PinguApps.Appwrite.Client.Internals;
Expand All @@ -21,20 +22,23 @@ public static class ServiceCollectionExtensions
/// <returns>The service collection, enabling chaining</returns>
public static IServiceCollection AddAppwriteClient(this IServiceCollection services, string projectId, string endpoint = "https://cloud.appwrite.io/v1", RefitSettings? refitSettings = null)
{
services.AddSingleton(sp => new HeaderHandler(projectId));
services.AddSingleton(x => new HeaderHandler(projectId));
services.AddSingleton<ClientCookieSessionHandler>();

services.AddRefitClient<IAccountApi>(refitSettings)
.ConfigureHttpClient(x => x.BaseAddress = new Uri(endpoint))
.AddHttpMessageHandler<HeaderHandler>();
.AddHttpMessageHandler<HeaderHandler>()
.AddHttpMessageHandler<ClientCookieSessionHandler>();

services.AddSingleton<IAccountClient, AccountClient>();
services.AddSingleton<IAppwriteClient, AppwriteClient>();
services.AddSingleton(x => new Lazy<IAppwriteClient>(() => x.GetRequiredService<IAppwriteClient>()));

return services;
}

/// <summary>
/// Adds all necessary components for the Client SDK in a transient state. Best used on server-side to perform client SDK abilities on behalf of users
/// Adds all necessary components for the Client SDK such that session will not be remembered. Best used on server-side to perform client SDK abilities on behalf of users
/// </summary>
/// <param name="services">The service collection to add to</param>
/// <param name="projectId">Your Appwrite Project ID</param>
Expand All @@ -47,10 +51,17 @@ public static IServiceCollection AddAppwriteClientForServer(this IServiceCollect

services.AddRefitClient<IAccountApi>(refitSettings)
.ConfigureHttpClient(x => x.BaseAddress = new Uri(endpoint))
.AddHttpMessageHandler<HeaderHandler>();
.AddHttpMessageHandler<HeaderHandler>()
.ConfigurePrimaryHttpMessageHandler((handler, sp) =>
{
if (handler is HttpClientHandler clientHandler)
{
clientHandler.UseCookies = false;
}
});

services.AddTransient<IAccountClient, AccountClient>();
services.AddTransient<IAppwriteClient, AppwriteClient>();
services.AddSingleton<IAccountClient, AccountClient>();
services.AddSingleton<IAppwriteClient, AppwriteClient>();

return services;
}
Expand Down
20 changes: 16 additions & 4 deletions src/PinguApps.Appwrite.Playground/App.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,19 +21,31 @@ public async Task Run(string[] args)
{
//_client.SetSession(_session);

var request = new CreateEmailTokenRequest
var request = new CreateSessionRequest
{
Email = "[email protected]",
UserId = "664aac1a00113f82e620",
Phrase = true
Secret = "834938"
};

var result = await _server.Account.CreateEmailToken(request);
Console.WriteLine($"Session: {_client.Session}");

var result = await _client.Account.CreateSession(request);

Console.WriteLine($"Session: {_client.Session}");

result.Result.Switch(
account => Console.WriteLine(string.Join(',', account)),
appwriteError => Console.WriteLine(appwriteError.Message),
internalError => Console.WriteLine(internalError.Message)
);

Console.WriteLine("Getting Account...");

var account = await _client.Account.Get();

Console.WriteLine(account.Result.Match(
account => account.ToString(),
appwriteError => appwriteError.Message,
internalERror => internalERror.Message));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
using System;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace PinguApps.Appwrite.Shared.Converters;
internal class NullableDateTimeConverter : JsonConverter<DateTime?>
{
public override DateTime? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.String)
{
var stringValue = reader.GetString();
if (string.IsNullOrEmpty(stringValue))
{
return null;
}

if (DateTime.TryParse(stringValue, out var dateTime))
{
return dateTime;
}

throw new JsonException($"Unable to parse '{stringValue}' to DateTime.");
}

throw new JsonException("Unexpected token type.");
}

public override void Write(Utf8JsonWriter writer, DateTime? value, JsonSerializerOptions options)
{
if (value.HasValue)
{
writer.WriteStringValue(value.Value.ToString("o"));
}
}
}
23 changes: 23 additions & 0 deletions src/PinguApps.Appwrite.Shared/Requests/CreateSessionRequest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using System.Text.Json.Serialization;
using PinguApps.Appwrite.Shared.Requests.Validators;
using PinguApps.Appwrite.Shared.Utils;

namespace PinguApps.Appwrite.Shared.Requests;

/// <summary>
/// The request for creating a session
/// </summary>
public class CreateSessionRequest : BaseRequest<CreateSessionRequest, CreateSessionRequestValidator>
{
/// <summary>
/// User ID. Choose a custom ID or generate a random ID with <see cref="IdUtils.GenerateUniqueId(int)"/>. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can't start with a special char. Max length is 36 chars
/// </summary>
[JsonPropertyName("userId")]
public string UserId { get; set; } = IdUtils.GenerateUniqueId();

/// <summary>
/// Secret of a token generated by login methods. For example, the CreateMagicURLToken or CreatePhoneToken methods.
/// </summary>
[JsonPropertyName("secret")]
public string Secret { get; set; } = string.Empty;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using FluentValidation;

namespace PinguApps.Appwrite.Shared.Requests.Validators;
public class CreateSessionRequestValidator : AbstractValidator<CreateSessionRequest>
{
public CreateSessionRequestValidator()
{
RuleFor(x => x.UserId).NotEmpty().Matches("^[a-zA-Z0-9][a-zA-Z0-9._-]{0,35}$");
RuleFor(x => x.Secret).NotEmpty();
}
}
70 changes: 70 additions & 0 deletions src/PinguApps.Appwrite.Shared/Responses/Session.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
using PinguApps.Appwrite.Shared.Converters;

namespace PinguApps.Appwrite.Shared.Responses;

/// <summary>
/// An Appwrite Session object
/// </summary>
/// <param name="Id">Session ID</param>
/// <param name="CreatedAt">Session creation date in ISO 8601 format</param>
/// <param name="UpdatedAt">Session update date in ISO 8601 format</param>
/// <param name="UserId">User ID</param>
/// <param name="ExpiresAt">Session expiration date in ISO 8601 format</param>
/// <param name="Provider">Session Provider</param>
/// <param name="ProviderUserId">Session Provider User ID</param>
/// <param name="ProviderAccessToken">Session Provider Access Token</param>
/// <param name="ProviderAccessTokenExpiry">The date of when the access token expires in ISO 8601 format</param>
/// <param name="ProviderRefreshToken">Session Provider Refresh Token</param>
/// <param name="Ip">IP in use when the session was created</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 name</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>
/// <param name="Current">Returns true if this the current user session</param>
/// <param name="Factors">Returns a list of active session factors</param>
/// <param name="Secret">Secret used to authenticate the user. Only included if the request was made with an API key</param>
/// <param name="MfaUpdatedAt">Most recent date in ISO 8601 format when the session successfully passed MFA challenge</param>
public record Session(
[property: JsonPropertyName("$id")] string Id,
[property: JsonPropertyName("$createdAt")] DateTime CreatedAt,
[property: JsonPropertyName("$updatedAt")] DateTime UpdatedAt,
[property: JsonPropertyName("userId")] string UserId,
[property: JsonPropertyName("expire")] DateTime ExpiresAt,
[property: JsonPropertyName("provider")] string Provider,
[property: JsonPropertyName("providerUid")] string ProviderUserId,
[property: JsonPropertyName("providerAccessToken")] string ProviderAccessToken,
[property: JsonPropertyName("providerAccessTokenExpiry"), JsonConverter(typeof(NullableDateTimeConverter))] DateTime? ProviderAccessTokenExpiry,
[property: JsonPropertyName("providerRefreshToken")] string ProviderRefreshToken,
[property: JsonPropertyName("ip")] string Ip,
[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,
[property: JsonPropertyName("current")] bool Current,
[property: JsonPropertyName("factors")] IReadOnlyList<string> Factors,
[property: JsonPropertyName("secret")] string Secret,
[property: JsonPropertyName("mfaUpdatedAt"), JsonConverter(typeof(NullableDateTimeConverter))] DateTime? MfaUpdatedAt
);
Loading