From 01d3b9405023ab2e42133d8deea04d14d10bd19c Mon Sep 17 00:00:00 2001 From: gamer147 Date: Mon, 29 Dec 2025 14:09:41 -0500 Subject: [PATCH] [FA-55] Finished aside from deactivation/integration events --- .../Extensions/AuthenticationExtensions.cs | 3 +- .../FictionArchive.Service.UserService.csproj | 1 + .../GraphQL/Mutation.cs | 2 +- .../GraphQL/Query.cs | 61 +++-- .../Models/DTOs/InvitedUserDto.cs | 7 + .../Models/DTOs/UserDto.cs | 1 + .../Models/Database/User.cs | 1 + .../Authentik/AuthentikAddUserRequest.cs | 12 +- .../Authentik/AuthentikClient.cs | 20 +- .../Authentik/AuthentikConfiguration.cs | 1 + .../Authentik/AuthentikUserResponse.cs | 16 +- .../Services/UserManagementService.cs | 14 ++ .../appsettings.json | 3 +- .../components/AuthenticationDisplay.svelte | 6 + .../src/lib/components/SettingsPage.svelte | 211 ++++++++++++++++++ .../src/lib/graphql/__generated__/graphql.ts | 60 +++-- .../lib/graphql/mutations/inviteUser.graphql | 14 ++ .../src/lib/graphql/queries/settings.graphql | 11 + .../src/pages/settings/index.astro | 8 + 19 files changed, 377 insertions(+), 75 deletions(-) create mode 100644 FictionArchive.Service.UserService/Models/DTOs/InvitedUserDto.cs create mode 100644 fictionarchive-web-astro/src/lib/components/SettingsPage.svelte create mode 100644 fictionarchive-web-astro/src/lib/graphql/mutations/inviteUser.graphql create mode 100644 fictionarchive-web-astro/src/lib/graphql/queries/settings.graphql create mode 100644 fictionarchive-web-astro/src/pages/settings/index.astro diff --git a/FictionArchive.Service.Shared/Extensions/AuthenticationExtensions.cs b/FictionArchive.Service.Shared/Extensions/AuthenticationExtensions.cs index 6f1c3e4..ad3707b 100644 --- a/FictionArchive.Service.Shared/Extensions/AuthenticationExtensions.cs +++ b/FictionArchive.Service.Shared/Extensions/AuthenticationExtensions.cs @@ -6,6 +6,7 @@ using Microsoft.IdentityModel.Tokens; using FictionArchive.Service.Shared.Constants; using FictionArchive.Service.Shared.Models.Authentication; using System.Linq; +using System.Security.Claims; namespace FictionArchive.Service.Shared.Extensions; @@ -78,7 +79,7 @@ public static class AuthenticationExtensions logger.LogDebug( "JWT token validated for subject: {Subject}", - context.Principal?.FindFirst("sub")?.Value ?? "unknown"); + context.Principal?.FindFirst(ClaimTypes.NameIdentifier)?.Value ?? "unknown"); return existingEvents?.OnTokenValidated?.Invoke(context) ?? Task.CompletedTask; } diff --git a/FictionArchive.Service.UserService/FictionArchive.Service.UserService.csproj b/FictionArchive.Service.UserService/FictionArchive.Service.UserService.csproj index ee73c28..56c63f6 100644 --- a/FictionArchive.Service.UserService/FictionArchive.Service.UserService.csproj +++ b/FictionArchive.Service.UserService/FictionArchive.Service.UserService.csproj @@ -22,6 +22,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/FictionArchive.Service.UserService/GraphQL/Mutation.cs b/FictionArchive.Service.UserService/GraphQL/Mutation.cs index 83afc5d..b994671 100644 --- a/FictionArchive.Service.UserService/GraphQL/Mutation.cs +++ b/FictionArchive.Service.UserService/GraphQL/Mutation.cs @@ -17,7 +17,7 @@ public class Mutation ClaimsPrincipal claimsPrincipal) { // Get the current user's OAuth provider ID from claims - var oAuthProviderId = claimsPrincipal.FindFirst("sub")?.Value; + var oAuthProviderId = claimsPrincipal.FindFirst(ClaimTypes.NameIdentifier)?.Value; if (string.IsNullOrEmpty(oAuthProviderId)) { throw new InvalidOperationException("Unable to determine current user identity"); diff --git a/FictionArchive.Service.UserService/GraphQL/Query.cs b/FictionArchive.Service.UserService/GraphQL/Query.cs index bf7ba30..d2f0254 100644 --- a/FictionArchive.Service.UserService/GraphQL/Query.cs +++ b/FictionArchive.Service.UserService/GraphQL/Query.cs @@ -2,53 +2,42 @@ using System.Security.Claims; using FictionArchive.Service.UserService.Models.DTOs; using FictionArchive.Service.UserService.Services; using HotChocolate.Authorization; +using HotChocolate.Data; namespace FictionArchive.Service.UserService.GraphQL; public class Query { [Authorize] - public async Task GetAvailableInvites( - UserManagementService userManagementService, + [UseProjection] + [UseFirstOrDefault] + public IQueryable GetCurrentUser( + UserServiceDbContext dbContext, ClaimsPrincipal claimsPrincipal) { - var oAuthProviderId = claimsPrincipal.FindFirst("sub")?.Value; + var oAuthProviderId = claimsPrincipal.FindFirst(ClaimTypes.NameIdentifier)?.Value; if (string.IsNullOrEmpty(oAuthProviderId)) { - return 0; + return Enumerable.Empty().AsQueryable(); } - var user = await userManagementService.GetUserByOAuthProviderIdAsync(oAuthProviderId); - return user?.AvailableInvites ?? 0; - } - - [Authorize] - public async Task GetCurrentUser( - UserManagementService userManagementService, - ClaimsPrincipal claimsPrincipal) - { - var oAuthProviderId = claimsPrincipal.FindFirst("sub")?.Value; - if (string.IsNullOrEmpty(oAuthProviderId)) - { - return null; - } - - var user = await userManagementService.GetUserByOAuthProviderIdAsync(oAuthProviderId); - if (user == null) - { - return null; - } - - return new UserDto - { - Id = user.Id, - CreatedTime = user.CreatedTime, - LastUpdatedTime = user.LastUpdatedTime, - Username = user.Username, - Email = user.Email, - Disabled = user.Disabled, - AvailableInvites = user.AvailableInvites, - InviterId = user.InviterId - }; + return dbContext.Users + .Where(u => u.OAuthProviderId == oAuthProviderId) + .Select(u => new UserDto + { + Id = u.Id, + CreatedTime = u.CreatedTime, + LastUpdatedTime = u.LastUpdatedTime, + Username = u.Username, + Email = u.Email, + Disabled = u.Disabled, + AvailableInvites = u.AvailableInvites, + InviterId = u.InviterId, + InvitedUsers = u.InvitedUsers.Select(iu => new InvitedUserDto + { + Username = iu.Username, + Email = iu.Email + }).ToList() + }); } } diff --git a/FictionArchive.Service.UserService/Models/DTOs/InvitedUserDto.cs b/FictionArchive.Service.UserService/Models/DTOs/InvitedUserDto.cs new file mode 100644 index 0000000..6a50ed3 --- /dev/null +++ b/FictionArchive.Service.UserService/Models/DTOs/InvitedUserDto.cs @@ -0,0 +1,7 @@ +namespace FictionArchive.Service.UserService.Models.DTOs; + +public class InvitedUserDto +{ + public required string Username { get; init; } + public required string Email { get; init; } +} diff --git a/FictionArchive.Service.UserService/Models/DTOs/UserDto.cs b/FictionArchive.Service.UserService/Models/DTOs/UserDto.cs index d5948ca..3a11c80 100644 --- a/FictionArchive.Service.UserService/Models/DTOs/UserDto.cs +++ b/FictionArchive.Service.UserService/Models/DTOs/UserDto.cs @@ -13,4 +13,5 @@ public class UserDto public bool Disabled { get; init; } public int AvailableInvites { get; init; } public Guid? InviterId { get; init; } + public List? InvitedUsers { get; init; } } diff --git a/FictionArchive.Service.UserService/Models/Database/User.cs b/FictionArchive.Service.UserService/Models/Database/User.cs index 02a100c..417fa2e 100644 --- a/FictionArchive.Service.UserService/Models/Database/User.cs +++ b/FictionArchive.Service.UserService/Models/Database/User.cs @@ -15,4 +15,5 @@ public class User : BaseEntity // Navigation properties public Guid? InviterId { get; set; } public User? Inviter { get; set; } + public ICollection InvitedUsers { get; set; } = new List(); } \ No newline at end of file diff --git a/FictionArchive.Service.UserService/Services/AuthenticationClient/Authentik/AuthentikAddUserRequest.cs b/FictionArchive.Service.UserService/Services/AuthenticationClient/Authentik/AuthentikAddUserRequest.cs index 5e885e4..6802595 100644 --- a/FictionArchive.Service.UserService/Services/AuthenticationClient/Authentik/AuthentikAddUserRequest.cs +++ b/FictionArchive.Service.UserService/Services/AuthenticationClient/Authentik/AuthentikAddUserRequest.cs @@ -1,21 +1,21 @@ -using System.Text.Json.Serialization; +using Newtonsoft.Json; namespace FictionArchive.Service.UserService.Services.AuthenticationClient.Authentik; public class AuthentikAddUserRequest { - [JsonPropertyName("username")] + [JsonProperty("username")] public required string Username { get; set; } - [JsonPropertyName("name")] + [JsonProperty("name")] public required string DisplayName { get; set; } - [JsonPropertyName("email")] + [JsonProperty("email")] public required string Email { get; set; } - [JsonPropertyName("is_active")] + [JsonProperty("is_active")] public bool IsActive { get; set; } = true; - [JsonPropertyName("type")] + [JsonProperty("type")] public string Type { get; } = "external"; } \ No newline at end of file diff --git a/FictionArchive.Service.UserService/Services/AuthenticationClient/Authentik/AuthentikClient.cs b/FictionArchive.Service.UserService/Services/AuthenticationClient/Authentik/AuthentikClient.cs index 4f3400b..c536e55 100644 --- a/FictionArchive.Service.UserService/Services/AuthenticationClient/Authentik/AuthentikClient.cs +++ b/FictionArchive.Service.UserService/Services/AuthenticationClient/Authentik/AuthentikClient.cs @@ -1,4 +1,6 @@ -using System.Net.Http.Json; +using System.Text; +using Microsoft.Extensions.Options; +using Newtonsoft.Json; namespace FictionArchive.Service.UserService.Services.AuthenticationClient.Authentik; @@ -6,11 +8,16 @@ public class AuthentikClient : IAuthenticationServiceClient { private readonly HttpClient _httpClient; private readonly ILogger _logger; + private readonly AuthentikConfiguration _configuration; - public AuthentikClient(HttpClient httpClient, ILogger logger) + public AuthentikClient( + HttpClient httpClient, + ILogger logger, + IOptions configuration) { _httpClient = httpClient; _logger = logger; + _configuration = configuration.Value; } public async Task CreateUserAsync(string username, string email, string displayName) @@ -25,7 +32,9 @@ public class AuthentikClient : IAuthenticationServiceClient try { - var response = await _httpClient.PostAsJsonAsync("/api/v3/core/users/", request); + var json = JsonConvert.SerializeObject(request); + var content = new StringContent(json, Encoding.UTF8, "application/json"); + var response = await _httpClient.PostAsync("/api/v3/core/users/", content); if (!response.IsSuccessStatusCode) { @@ -36,7 +45,8 @@ public class AuthentikClient : IAuthenticationServiceClient return null; } - var userResponse = await response.Content.ReadFromJsonAsync(); + var responseJson = await response.Content.ReadAsStringAsync(); + var userResponse = JsonConvert.DeserializeObject(responseJson); _logger.LogInformation("Successfully created user {Username} in Authentik with pk {Pk}", username, userResponse?.Pk); @@ -54,7 +64,7 @@ public class AuthentikClient : IAuthenticationServiceClient try { var response = await _httpClient.PostAsync( - $"/api/v3/core/users/{authentikUserId}/recovery_email/", + $"/api/v3/core/users/{authentikUserId}/recovery_email/?email_stage={_configuration.EmailStageId}", null); if (!response.IsSuccessStatusCode) diff --git a/FictionArchive.Service.UserService/Services/AuthenticationClient/Authentik/AuthentikConfiguration.cs b/FictionArchive.Service.UserService/Services/AuthenticationClient/Authentik/AuthentikConfiguration.cs index 00d5afc..aeb815c 100644 --- a/FictionArchive.Service.UserService/Services/AuthenticationClient/Authentik/AuthentikConfiguration.cs +++ b/FictionArchive.Service.UserService/Services/AuthenticationClient/Authentik/AuthentikConfiguration.cs @@ -4,4 +4,5 @@ public class AuthentikConfiguration { public string BaseUrl { get; set; } = string.Empty; public string ApiToken { get; set; } = string.Empty; + public string EmailStageId { get; set; } } diff --git a/FictionArchive.Service.UserService/Services/AuthenticationClient/Authentik/AuthentikUserResponse.cs b/FictionArchive.Service.UserService/Services/AuthenticationClient/Authentik/AuthentikUserResponse.cs index fb945be..6f2938e 100644 --- a/FictionArchive.Service.UserService/Services/AuthenticationClient/Authentik/AuthentikUserResponse.cs +++ b/FictionArchive.Service.UserService/Services/AuthenticationClient/Authentik/AuthentikUserResponse.cs @@ -1,27 +1,27 @@ -using System.Text.Json.Serialization; +using Newtonsoft.Json; namespace FictionArchive.Service.UserService.Services.AuthenticationClient.Authentik; public class AuthentikUserResponse { - [JsonPropertyName("pk")] + [JsonProperty("pk")] public int Pk { get; set; } - [JsonPropertyName("username")] + [JsonProperty("username")] public string Username { get; set; } = string.Empty; - [JsonPropertyName("name")] + [JsonProperty("name")] public string Name { get; set; } = string.Empty; - [JsonPropertyName("email")] + [JsonProperty("email")] public string Email { get; set; } = string.Empty; - [JsonPropertyName("is_active")] + [JsonProperty("is_active")] public bool IsActive { get; set; } - [JsonPropertyName("is_superuser")] + [JsonProperty("is_superuser")] public bool IsSuperuser { get; set; } - [JsonPropertyName("uid")] + [JsonProperty("uid")] public string Uid { get; set; } = string.Empty; } diff --git a/FictionArchive.Service.UserService/Services/UserManagementService.cs b/FictionArchive.Service.UserService/Services/UserManagementService.cs index cdddcb7..a6b409a 100644 --- a/FictionArchive.Service.UserService/Services/UserManagementService.cs +++ b/FictionArchive.Service.UserService/Services/UserManagementService.cs @@ -118,4 +118,18 @@ public class UserManagementService { return _dbContext.Users.AsQueryable(); } + + /// + /// Gets all users invited by a specific user. + /// + /// The ID of the user who sent the invites + /// List of users invited by the specified user + public async Task> GetInvitedByUserAsync(Guid inviterId) + { + return await _dbContext.Users + .AsQueryable() + .Where(u => u.InviterId == inviterId) + .OrderByDescending(u => u.CreatedTime) + .ToListAsync(); + } } \ No newline at end of file diff --git a/FictionArchive.Service.UserService/appsettings.json b/FictionArchive.Service.UserService/appsettings.json index 3eeb9d8..2182f2f 100644 --- a/FictionArchive.Service.UserService/appsettings.json +++ b/FictionArchive.Service.UserService/appsettings.json @@ -14,7 +14,8 @@ }, "Authentik": { "BaseUrl": "https://auth.orfl.xyz", - "ApiToken": "REPLACE_ME" + "ApiToken": "REPLACE_ME", + "EmailStageId": "10df0c18-8802-4ec7-852e-3cdd355514d3" }, "AllowedHosts": "*", "OIDC": { diff --git a/fictionarchive-web-astro/src/lib/components/AuthenticationDisplay.svelte b/fictionarchive-web-astro/src/lib/components/AuthenticationDisplay.svelte index c65d882..a58be93 100644 --- a/fictionarchive-web-astro/src/lib/components/AuthenticationDisplay.svelte +++ b/fictionarchive-web-astro/src/lib/components/AuthenticationDisplay.svelte @@ -44,6 +44,12 @@
+ + Settings + diff --git a/fictionarchive-web-astro/src/lib/components/SettingsPage.svelte b/fictionarchive-web-astro/src/lib/components/SettingsPage.svelte new file mode 100644 index 0000000..dc1a998 --- /dev/null +++ b/fictionarchive-web-astro/src/lib/components/SettingsPage.svelte @@ -0,0 +1,211 @@ + + +
+
+

Settings

+

Manage your account settings

+
+ + {#if fetching} +
+
Loading...
+
+ {:else if error} + + +

{error}

+ +
+
+ {:else} + + + Invites + + + + + + + + Invite a User + 0 ? 'default' : 'secondary'}> + {availableInvites} remaining + + + + +
+
+
+ + +
+
+ + +
+
+ + {#if formError} +

{formError}

+ {/if} + + {#if formSuccess} +

+ Invitation sent successfully! The user will receive an email to set up their account. +

+ {/if} + + +
+
+
+ + + + + Invited Users + + + {#if !currentUser?.invitedUsers?.length} +

+ You haven't invited anyone yet. +

+ {:else} +
+ {#each currentUser.invitedUsers as user} +
+
+

{user.username}

+

{user.email}

+
+
+ {/each} +
+ {/if} +
+
+
+
+ {/if} +
diff --git a/fictionarchive-web-astro/src/lib/graphql/__generated__/graphql.ts b/fictionarchive-web-astro/src/lib/graphql/__generated__/graphql.ts index 7ad2125..5f7f3b2 100644 --- a/fictionarchive-web-astro/src/lib/graphql/__generated__/graphql.ts +++ b/fictionarchive-web-astro/src/lib/graphql/__generated__/graphql.ts @@ -156,6 +156,27 @@ export type InstantFilterInput = { or?: InputMaybe>; }; +export type InvalidOperationError = Error & { + message: Scalars['String']['output']; +}; + +export type InviteUserError = InvalidOperationError; + +export type InviteUserInput = { + email: Scalars['String']['input']; + username: Scalars['String']['input']; +}; + +export type InviteUserPayload = { + errors: Maybe>; + userDto: Maybe; +}; + +export type InvitedUserDto = { + email: Scalars['String']['output']; + username: Scalars['String']['output']; +}; + export type JobKey = { group: Scalars['String']['output']; name: Scalars['String']['output']; @@ -215,7 +236,7 @@ export type Mutation = { deleteNovel: DeleteNovelPayload; fetchChapterContents: FetchChapterContentsPayload; importNovel: ImportNovelPayload; - registerUser: RegisterUserPayload; + inviteUser: InviteUserPayload; runJob: RunJobPayload; scheduleEventJob: ScheduleEventJobPayload; translateText: TranslateTextPayload; @@ -242,8 +263,8 @@ export type MutationImportNovelArgs = { }; -export type MutationRegisterUserArgs = { - input: RegisterUserInput; +export type MutationInviteUserArgs = { + input: InviteUserInput; }; @@ -422,11 +443,11 @@ export type PersonDtoSortInput = { export type Query = { chapter: Maybe; + currentUser: Array; jobs: Array; novels: Maybe; translationEngines: Array; translationRequests: Maybe; - users: Array; }; @@ -463,17 +484,6 @@ export type QueryTranslationRequestsArgs = { where?: InputMaybe; }; -export type RegisterUserInput = { - email: Scalars['String']['input']; - inviterOAuthProviderId?: InputMaybe; - oAuthProviderId: Scalars['String']['input']; - username: Scalars['String']['input']; -}; - -export type RegisterUserPayload = { - userDto: Maybe; -}; - export type RunJobError = JobPersistenceError; export type RunJobInput = { @@ -700,11 +710,13 @@ export type UnsignedIntOperationFilterInputType = { }; export type UserDto = { + availableInvites: Scalars['Int']['output']; createdTime: Scalars['Instant']['output']; disabled: Scalars['Boolean']['output']; email: Scalars['String']['output']; id: Scalars['UUID']['output']; - inviter: Maybe; + invitedUsers: Maybe>; + inviterId: Maybe; lastUpdatedTime: Scalars['Instant']['output']; username: Scalars['String']['output']; }; @@ -738,6 +750,13 @@ export type ImportNovelMutationVariables = Exact<{ export type ImportNovelMutation = { importNovel: { novelUpdateRequestedEvent: { novelUrl: string } | null } }; +export type InviteUserMutationVariables = Exact<{ + input: InviteUserInput; +}>; + + +export type InviteUserMutation = { inviteUser: { userDto: { id: any, username: string, email: string } | null, errors: Array<{ message: string }> | null } }; + export type GetChapterQueryVariables = Exact<{ novelId: Scalars['UnsignedInt']['input']; chapterOrder: Scalars['UnsignedInt']['input']; @@ -763,9 +782,16 @@ export type NovelsQueryVariables = Exact<{ export type NovelsQuery = { novels: { edges: Array<{ cursor: string, node: { id: any, url: string, name: string, description: string, rawStatus: NovelStatus, lastUpdatedTime: any, coverImage: { newPath: string | null } | null, chapters: Array<{ order: any, name: string }>, tags: Array<{ key: string, displayName: string, tagType: TagType }> } }> | null, pageInfo: { hasNextPage: boolean, endCursor: string | null } } | null }; +export type GetSettingsPageDataQueryVariables = Exact<{ [key: string]: never; }>; + + +export type GetSettingsPageDataQuery = { currentUser: Array<{ id: any, username: string, availableInvites: number, invitedUsers: Array<{ username: string, email: string }> | null }> }; + export const DeleteNovelDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeleteNovel"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"DeleteNovelInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"deleteNovel"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"boolean"}},{"kind":"Field","name":{"kind":"Name","value":"errors"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Error"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"message"}}]}}]}}]}}]}}]} as unknown as DocumentNode; export const ImportNovelDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"ImportNovel"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ImportNovelInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"importNovel"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"novelUpdateRequestedEvent"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"novelUrl"}}]}}]}}]}}]} as unknown as DocumentNode; +export const InviteUserDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"InviteUser"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"InviteUserInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"inviteUser"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"userDto"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"username"}},{"kind":"Field","name":{"kind":"Name","value":"email"}}]}},{"kind":"Field","name":{"kind":"Name","value":"errors"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"InvalidOperationError"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"message"}}]}}]}}]}}]}}]} as unknown as DocumentNode; export const GetChapterDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetChapter"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"novelId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UnsignedInt"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"chapterOrder"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UnsignedInt"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"chapter"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"novelId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"novelId"}}},{"kind":"Argument","name":{"kind":"Name","value":"chapterOrder"},"value":{"kind":"Variable","name":{"kind":"Name","value":"chapterOrder"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"order"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"body"}},{"kind":"Field","name":{"kind":"Name","value":"url"}},{"kind":"Field","name":{"kind":"Name","value":"revision"}},{"kind":"Field","name":{"kind":"Name","value":"createdTime"}},{"kind":"Field","name":{"kind":"Name","value":"lastUpdatedTime"}},{"kind":"Field","name":{"kind":"Name","value":"images"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"newPath"}}]}},{"kind":"Field","name":{"kind":"Name","value":"novelId"}},{"kind":"Field","name":{"kind":"Name","value":"novelName"}},{"kind":"Field","name":{"kind":"Name","value":"totalChapters"}},{"kind":"Field","name":{"kind":"Name","value":"prevChapterOrder"}},{"kind":"Field","name":{"kind":"Name","value":"nextChapterOrder"}}]}}]}}]} as unknown as DocumentNode; export const NovelDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"Novel"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UnsignedInt"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"novels"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"where"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"id"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"eq"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}]}}]}},{"kind":"Argument","name":{"kind":"Name","value":"first"},"value":{"kind":"IntValue","value":"1"}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"nodes"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"url"}},{"kind":"Field","name":{"kind":"Name","value":"rawLanguage"}},{"kind":"Field","name":{"kind":"Name","value":"rawStatus"}},{"kind":"Field","name":{"kind":"Name","value":"statusOverride"}},{"kind":"Field","name":{"kind":"Name","value":"externalId"}},{"kind":"Field","name":{"kind":"Name","value":"createdTime"}},{"kind":"Field","name":{"kind":"Name","value":"lastUpdatedTime"}},{"kind":"Field","name":{"kind":"Name","value":"author"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"externalUrl"}}]}},{"kind":"Field","name":{"kind":"Name","value":"source"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"key"}},{"kind":"Field","name":{"kind":"Name","value":"url"}}]}},{"kind":"Field","name":{"kind":"Name","value":"coverImage"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"newPath"}}]}},{"kind":"Field","name":{"kind":"Name","value":"tags"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"key"}},{"kind":"Field","name":{"kind":"Name","value":"displayName"}},{"kind":"Field","name":{"kind":"Name","value":"tagType"}}]}},{"kind":"Field","name":{"kind":"Name","value":"chapters"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"order"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"lastUpdatedTime"}},{"kind":"Field","name":{"kind":"Name","value":"images"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"newPath"}}]}}]}}]}}]}}]}}]} as unknown as DocumentNode; -export const NovelsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"Novels"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"first"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"after"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"where"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"NovelDtoFilterInput"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"order"}},"type":{"kind":"ListType","type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"NovelDtoSortInput"}}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"novels"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"first"},"value":{"kind":"Variable","name":{"kind":"Name","value":"first"}}},{"kind":"Argument","name":{"kind":"Name","value":"after"},"value":{"kind":"Variable","name":{"kind":"Name","value":"after"}}},{"kind":"Argument","name":{"kind":"Name","value":"where"},"value":{"kind":"Variable","name":{"kind":"Name","value":"where"}}},{"kind":"Argument","name":{"kind":"Name","value":"order"},"value":{"kind":"Variable","name":{"kind":"Name","value":"order"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"edges"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"cursor"}},{"kind":"Field","name":{"kind":"Name","value":"node"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"url"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"coverImage"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"newPath"}}]}},{"kind":"Field","name":{"kind":"Name","value":"rawStatus"}},{"kind":"Field","name":{"kind":"Name","value":"lastUpdatedTime"}},{"kind":"Field","name":{"kind":"Name","value":"chapters"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"order"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"tags"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"key"}},{"kind":"Field","name":{"kind":"Name","value":"displayName"}},{"kind":"Field","name":{"kind":"Name","value":"tagType"}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"pageInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"hasNextPage"}},{"kind":"Field","name":{"kind":"Name","value":"endCursor"}}]}}]}}]}}]} as unknown as DocumentNode; \ No newline at end of file +export const NovelsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"Novels"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"first"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"after"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"where"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"NovelDtoFilterInput"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"order"}},"type":{"kind":"ListType","type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"NovelDtoSortInput"}}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"novels"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"first"},"value":{"kind":"Variable","name":{"kind":"Name","value":"first"}}},{"kind":"Argument","name":{"kind":"Name","value":"after"},"value":{"kind":"Variable","name":{"kind":"Name","value":"after"}}},{"kind":"Argument","name":{"kind":"Name","value":"where"},"value":{"kind":"Variable","name":{"kind":"Name","value":"where"}}},{"kind":"Argument","name":{"kind":"Name","value":"order"},"value":{"kind":"Variable","name":{"kind":"Name","value":"order"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"edges"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"cursor"}},{"kind":"Field","name":{"kind":"Name","value":"node"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"url"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"coverImage"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"newPath"}}]}},{"kind":"Field","name":{"kind":"Name","value":"rawStatus"}},{"kind":"Field","name":{"kind":"Name","value":"lastUpdatedTime"}},{"kind":"Field","name":{"kind":"Name","value":"chapters"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"order"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"tags"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"key"}},{"kind":"Field","name":{"kind":"Name","value":"displayName"}},{"kind":"Field","name":{"kind":"Name","value":"tagType"}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"pageInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"hasNextPage"}},{"kind":"Field","name":{"kind":"Name","value":"endCursor"}}]}}]}}]}}]} as unknown as DocumentNode; +export const GetSettingsPageDataDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetSettingsPageData"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"currentUser"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"username"}},{"kind":"Field","name":{"kind":"Name","value":"availableInvites"}},{"kind":"Field","name":{"kind":"Name","value":"invitedUsers"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"username"}},{"kind":"Field","name":{"kind":"Name","value":"email"}}]}}]}}]}}]} as unknown as DocumentNode; \ No newline at end of file diff --git a/fictionarchive-web-astro/src/lib/graphql/mutations/inviteUser.graphql b/fictionarchive-web-astro/src/lib/graphql/mutations/inviteUser.graphql new file mode 100644 index 0000000..1a0fb57 --- /dev/null +++ b/fictionarchive-web-astro/src/lib/graphql/mutations/inviteUser.graphql @@ -0,0 +1,14 @@ +mutation InviteUser($input: InviteUserInput!) { + inviteUser(input: $input) { + userDto { + id + username + email + } + errors { + ... on InvalidOperationError { + message + } + } + } +} diff --git a/fictionarchive-web-astro/src/lib/graphql/queries/settings.graphql b/fictionarchive-web-astro/src/lib/graphql/queries/settings.graphql new file mode 100644 index 0000000..5dbb1c3 --- /dev/null +++ b/fictionarchive-web-astro/src/lib/graphql/queries/settings.graphql @@ -0,0 +1,11 @@ +query GetSettingsPageData { + currentUser { + id + username + availableInvites + invitedUsers { + username + email + } + } +} diff --git a/fictionarchive-web-astro/src/pages/settings/index.astro b/fictionarchive-web-astro/src/pages/settings/index.astro new file mode 100644 index 0000000..c8a4fc5 --- /dev/null +++ b/fictionarchive-web-astro/src/pages/settings/index.astro @@ -0,0 +1,8 @@ +--- +import AppLayout from '../../layouts/AppLayout.astro'; +import SettingsPage from '../../lib/components/SettingsPage.svelte'; +--- + + + +