Skip to content

Commit

Permalink
Don't use GQL queries, use alternate page
Browse files Browse the repository at this point in the history
Instead of GQL queries to check if the user already exists, we put the
logic into LoginController. This works: users can now receive multiple
invitation links, and only see the registration page if they have not
registered yet. If they have registered before, they're taken to an
alternate URL that logs them in immediately.

Still to do: ensure that the acceptInvitation backend doesn't throw an
error if the user is already a member of the project, but just silently
returns success if we're already in the desired state.
  • Loading branch information
rmunn committed Sep 25, 2024
1 parent 05eda53 commit d253132
Show file tree
Hide file tree
Showing 5 changed files with 50 additions and 25 deletions.
12 changes: 10 additions & 2 deletions backend/LexBoxApi/Controllers/LoginController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
using System.Security.Claims;
using Microsoft.AspNetCore.Authentication.Google;
using LexBoxApi.Services.Email;
using LexData.Entities;

namespace LexBoxApi.Controllers;

Expand All @@ -36,7 +37,8 @@ public class LoginController(
[AllowAnyAudience]
public async Task<ActionResult> LoginRedirect(
string jwt, // This is required because auth looks for a jwt in the query string
string returnTo)
string returnTo,
string? returnToIfEmailExists)
{
var user = loggedInContext.User;
// A RegisterAccount token means there's no user account yet, so checking UpdatedDate makes no sense
Expand All @@ -51,7 +53,13 @@ public async Task<ActionResult> LoginRedirect(

await HttpContext.SignInAsync(User,
new AuthenticationProperties { IsPersistent = true });
return Redirect(returnTo);
string destination = returnTo;
if (returnToIfEmailExists is not null && user.Email is not null)
{
var dbUser = await lexBoxDbContext.Users.FindByEmailOrUsername(user.Email);
if (dbUser is not null) destination = returnToIfEmailExists!;
}
return Redirect(destination);

Check warning

Code scanning / CodeQL

URL redirection from remote source Medium

Untrusted URL redirection due to
user-provided value
.
Untrusted URL redirection due to
user-provided value
.
}

[HttpGet("google")]
Expand Down
21 changes: 0 additions & 21 deletions backend/LexBoxApi/GraphQL/LexQueries.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
using HotChocolate.Authorization;
using HotChocolate.Resolvers;
using LexBoxApi.Auth;
using LexBoxApi.Auth.Attributes;
Expand Down Expand Up @@ -202,26 +201,6 @@ public IQueryable<User> Users(LexBoxDbContext context)
};
}

[AllowAnonymous]
public async Task<MeDto?> UserById(LexBoxDbContext context, LoggedInContext loggedInContext, Guid userId)
{
var registeringUser = loggedInContext.User;
// Only admins can look up users other than themselves via this query
if (!registeringUser.IsAdmin && registeringUser.Id != userId)
{
throw new UnauthorizedAccessException();
}
var user = await context.Users.FindAsync(userId);
if (user == null) return null;
return new MeDto
{
Id = user.Id,
Name = user.Name,
Email = user.Email,
Locale = user.LocalizationCode
};
}

public async Task<OrgMemberDto?> OrgMemberById(LexBoxDbContext context, IPermissionService permissionService, Guid orgId, Guid userId)
{
// Only site admins and org admins are allowed to run this query
Expand Down
3 changes: 2 additions & 1 deletion backend/LexBoxApi/Services/EmailService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -168,10 +168,11 @@ private async Task SendInvitationEmail(

var queryString = QueryString.Create("email", emailAddress);
var returnTo = new UriBuilder { Path = "/acceptInvitation", Query = queryString.Value }.Uri.PathAndQuery;
var returnToIfEmailExists = new UriBuilder { Path = "/acceptInvitationImmediately", Query = queryString.Value }.Uri.PathAndQuery;
var registerLink = _linkGenerator.GetUriByAction(httpContext,
"LoginRedirect",
"Login",
new { jwt, returnTo });
new { jwt, returnTo, returnToIfEmailExists });

ArgumentException.ThrowIfNullOrEmpty(registerLink);
if (isProjectInvitation)
Expand Down
1 change: 0 additions & 1 deletion frontend/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -426,7 +426,6 @@ type Query {
orgById(orgId: UUID!): OrgById
users(skip: Int take: Int where: UserFilterInput orderBy: [UserSortInput!]): UsersCollectionSegment @authorize(policy: "AdminRequiredPolicy")
me: MeDto
userById(userId: UUID!): MeDto @authorize(policy: "AllowAnyAudiencePolicy")
orgMemberById(orgId: UUID! userId: UUID!): OrgMemberDto
meAuth: LexAuthUser!
testingThrowsError: LexAuthUser!
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<script lang="ts">
import { TitlePage } from '$lib/layout';

Check failure on line 2 in frontend/src/routes/(unauthenticated)/acceptInvitationImmediately/+page.svelte

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

frontend/src/routes/(unauthenticated)/acceptInvitationImmediately/+page.svelte#L2

[@typescript-eslint/no-unused-vars] 'TitlePage' is defined but never used.
import t from '$lib/i18n';

Check failure on line 3 in frontend/src/routes/(unauthenticated)/acceptInvitationImmediately/+page.svelte

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

frontend/src/routes/(unauthenticated)/acceptInvitationImmediately/+page.svelte#L3

[@typescript-eslint/no-unused-vars] 't' is defined but never used.
import { goto } from '$app/navigation';
import { acceptInvitation } from '$lib/user';
import type { Token } from '$lib/forms/ProtectedForm.svelte';
import { env } from '$env/dynamic/public';
import { Turnstile } from 'svelte-turnstile';
import { onMount } from 'svelte';
import { getSearchParamValues } from '$lib/util/query-params';
type AcceptInvitationImmediatelyPageQueryParams = {
name: string;
email: string;
};
const siteKey = env.PUBLIC_TURNSTILE_SITE_KEY;
let resolveToken: (token: string) => void;
let turnstileToken = new Promise<string>((resolve, _) => { resolveToken = resolve });
function deliverToken({ detail: { token } }: CustomEvent<Token>): void {
resolveToken(token);
}
onMount(async () => {
const urlValues = getSearchParamValues<AcceptInvitationImmediatelyPageQueryParams>();
let token = await turnstileToken;
const registerResponse = await acceptInvitation('x', 0, urlValues.name ?? 'x', urlValues.email, 'x', token);
console.log(registerResponse);
if (!registerResponse.error) await goto('/home', { invalidateAll: true }); // invalidate so we get the user from the server
//TODO: Display errors on page, if any
});
</script>

<section class="mt-8 flex justify-center md:justify-end">
<Turnstile {siteKey} on:turnstile-callback={deliverToken} />
</section>

0 comments on commit d253132

Please sign in to comment.