Best practices for dynamic pages, multi organization and authentication

Hi,

I am trying to find a good way to have dynamic pages and authentication. I am currently on the WeWeb + Supabase stack and experimenting still, and wonder if anyone else has solved authentication/dynamic pages/organization in a smarter and simple way… I will try to simplify.

Database roles and user
I have organization, role, userRole (mapping) and user tables in Supabase.
One user can have several userRole in several organizations.

Authentication
In WeWeb, authentication is all well due to the auth plugin.
However, authenticated is not enough. The organization needs to be correct, AND the userRole needs to have this organization_id, user_id and a role.

Dynamic page slugs
In WeWeb i use dynamic values in the URL so that i can have:
organization/project/ and currently deploying slug, instead of id, for clean URLs.
This means I have to add the slug into all internal links A bit cumbersome to work with.

I am also storing the organization_id as a global variable (not local storage)

organization member checking
On every page load, I fetch organization, and organization + userRole of userId.
(Does this user have a role in this organization?) This way i ensure that if you don’t have a role in this organization you will be redirected.

I of course will have RLS, so the data should be safe; but I’m not sure if its enough in itself. If i made this single organization, then it shouldnt be a problem I think? But the navigation between organizations and pages seem a bit problematic to me. How can I make this easier?

Thank you!

Just a tip, you can add the users organization and roles to the JWT using Supabase Auth Hooks “Customize Access Token (JWT) Claims”, that should be a safe way of doing this.

So it will be available without doing an extra database lookup upon page load. Then you then get both organisation and userroles available in supabase to use in your RLS checks, there also without needing database lookups, very good for performance.

1 Like

Not just a tip. This is the solution! Thank you, I’ll dig into this a bit more.

This solves the authentication part.
Still a question with the slug / organization switching.

@thomlov I’m too low level for this. :frowning:

I can’t figure out how to set a claim that writes to raw_user_meta_data. I would have it in a function which triggers on insert userRole, or update userRole. I’ve been looking but I’m stuck. Any pointers?

Ideally, I think it would be something like this:

{
“organizations”: [
{
“organization_id”: 123,
“role”: 456
}
]
}

I actually did something else. I am currently storing the session_currentOrganization, then I have a app workflow that checks the session current org variable, if the organization_data has loaded, if the role has access to this organization.

In any case where everything is “OK” no call to the database happens. If something is “wrong” then it tries to fetch the correct data, if it fails, it redirects to a “select organization” page, which by clicking on an org will set the session_currentOrganization (also to local storage, actually)

This way, everything is smooth until its not - then some calls happen.
If nothing works there then you’re redirected and fix your own problem by selecting an org. Hopefully.

I also trashed the slugs for now for easier navigation. Might be handy later.

Heres how I set the claim in the “Customize Access Token (JWT) Claims” hook.

remember user must be blocked from changing his own org_id and roles by RLS.

DECLARE
    user_id UUID := (event->>'user_id')::uuid;
    claims jsonb := event->'claims';
    org_id BIGINT := null;
    roles_array TEXT[] := ARRAY[]::TEXT[];
BEGIN

    -- Fetch org_id from the profiles table
    SELECT organisation_id INTO org_id
    FROM public.profiles
    WHERE id = user_id;

    -- Fetch array of roles' short_ids for the user
    -- Ensure roles_array is at least initialized to an empty array
    SELECT ARRAY_AGG(r.short_id) INTO roles_array
    FROM public."userRoles" ur
    JOIN public.roles r ON ur."roleId" = r.id
    WHERE ur."userId" = user_id;

    -- If no roles are found, initialize roles_array to an empty TEXT array
    IF roles_array IS NULL THEN
        roles_array := ARRAY[]::TEXT[];
    END IF;

    -- Ensure 'app_metadata' exists in claims
    IF jsonb_typeof(claims->'app_metadata') IS NULL THEN
        claims := jsonb_set(claims, '{app_metadata}', '{}');
    END IF;

    -- Update claims with roles and org_id
    claims := jsonb_set(claims, '{app_metadata, roles}', to_jsonb(roles_array));
    claims := jsonb_set(claims, '{app_metadata, org_id}', 
                    CASE WHEN org_id IS NULL 
                    THEN 'null'::jsonb 
                    ELSE to_jsonb(org_id::text) 
                    END);

    -- Update the 'claims' object in the original event
    event := jsonb_set(event, '{claims}', claims);

    -- Return the modified event
    RETURN event;
END;