Skip to content

Commit

Permalink
Merge pull request #72 from PinguApps/42-create-session
Browse files Browse the repository at this point in the history
create session implementation
  • Loading branch information
pingu2k4 authored Jul 15, 2024
2 parents 2575002 + be7db6b commit 7848520
Show file tree
Hide file tree
Showing 19 changed files with 882 additions and 28 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
![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

0 comments on commit 7848520

Please sign in to comment.