Back

Auth State in Server-side

  • 0
  • Users
  • Accounts
  • Web
Meldiron
26 Jun, 2023, 08:29

Hey 👋 As you mentioned, above SSR example repo should let you do it. But to be honest, SSR is very very complex and doesn't give as much benefits.

What you can do is have 3 states: null | false | true. If null, show skeleton loaders (or hide fully) If false, show button X If true, show button Y

By default you set it to null, and after 1-2s it switches to proper boolean.

Such UI can be seen in many big providers such as DigitalOcean, Railway, or even Facebook. It is much faster solution and user doesn't usually care too much. In the end, those authentication requests are usually very optimized, and in Appwrite they can resolve within <0.1s.

Here is quick showcase of how such client-side-rendered UI looks in Railway:

TL;DR
The user wants to prevent displaying the reset password form if the token and secret are not available. There are discussions about organizing routes and layouts, but no clear solution is provided in the thread.
loup
26 Jun, 2023, 09:24

well right now if the userLoading is true I show the skeleton loeaders and after if user = true show button X and false button Y

loup
26 Jun, 2023, 09:25

But for example, to make private route, how can I do ?

loup
26 Jun, 2023, 09:25

Because we gonna see the private route (like during 0.5s) if we arent connected ?

loup
26 Jun, 2023, 09:27

Or I can make a loader

Meldiron
26 Jun, 2023, 09:27

You can do the same logic, a skeletonal loader instead of the outlet.

Something like (pseudo code):

TypeScript
<Header />

{ isLoading ? <Loader /> : <Outlet /> }

<Footer />
loup
26 Jun, 2023, 09:30

I guess I have to add this condition in all private page right ? I cant use middlesware file (I don't really know middleware but I thought I understood that it was used to make private routes) ?

loup
26 Jun, 2023, 09:30

Maybe middlesware only works on server-side ?

Meldiron
26 Jun, 2023, 09:33

Possibly, I am not too experience with Next.js. Cant you do that typical "use client" in there?

Meldiron
26 Jun, 2023, 09:34

You could also do this in the layout file. That layout would then have array of routes that needs to be protected, so it knows when to check and when not to

Meldiron
26 Jun, 2023, 09:38

(just wild guess from docs), you can do into your layout file and add getData method where you try to get account, and return something based on that. Then, you add use client at the top, and in yoru component you const data = await getData()

With that, loading state should be handeled by Next.js.. Soo maybe white page, maybe pending redirect, not sure. But it should be some out-of-the-box behaviour.

Refference: https://nextjs.org/docs/app/building-your-application/data-fetching/fetching

loup
26 Jun, 2023, 09:41

There is my user.tsx hook for get Auth (and other stuff like login, signup, etc...) :

TypeScript
"use client"

import React, {createContext, useContext, useState, useEffect } from 'react'
import { account, databases } from '@/utils/appwrite'

export interface UserState {
    user: any;
    userLoading: boolean;
    login: (email: string, password: string) => Promise<void>;
    logout: () => Promise<void>;
    signup: (email: string, name: string, username: string, password: string) => Promise<void>;
}

const defaultState: UserState = {
    user: null,
    userLoading: true,
    login: async () => {},
    logout: async () => {},
    signup: async () => {},
}

// create the context
const UserContext = createContext<UserState>(defaultState);

// create the provider component
export const UserProvider = ({ children } : { children: any }) => {
    const [ user, setUser ] = useState<null | any>(null);
    const [ userLoading, setUserLoading ] = useState(true);

    useEffect(() => {
        const chekcUser = async () => {
            try {
                const userRequest = await account.get();
                const userDetails = await databases.getDocument(String(process.env.NEXT_PUBLIC_APPWRITE_DATABASE_USERS), String(process.env.NEXT_PUBLIC_APPWRITE_COLLECTION_USER), userRequest.$id)
                setUser({
                    ...userRequest,
                    username: userDetails.username,
                    avatar: userDetails.avatar,
                    language: userDetails.language,
                })
            } catch (error) {
                setUser(null)
            } finally {
                setUserLoading(false)
            }
        }
        chekcUser();
    }, [])

    const login = async (email: string, password: string) => {
        try {
            await account.createEmailSession(email, password);
            const userRequest = await account.get();
            const userDetails = await databases.getDocument(String(process.env.NEXT_PUBLIC_APPWRITE_DATABASE_USERS), String(process.env.NEXT_PUBLIC_APPWRITE_COLLECTION_USER), userRequest.$id)
            setUser({
                ...userRequest,
                username: userDetails.username,
                avatar: userDetails.avatar,
                language: userDetails.language,
            })
        } catch (error) {
            throw error
        }
    }

    const logout = async () => {
        try {
            await account.deleteSession('current');
            setUser(null);
        } catch (error) {
            throw error
        }
    }

    const signup = async (
        email: string, 
        name: string, 
        username: string, 
        password: string
    ) => {
        try {
            const {$id} = await account.create('unique()', email, password, name)
            await account.createEmailSession(email, password);
            await databases.createDocument(String(process.env.NEXT_PUBLIC_APPWRITE_DATABASE_USERS), String(process.env.NEXT_PUBLIC_APPWRITE_COLLECTION_USER),$id, {
                "userId": $id,
                "username": username
            });
            await account.createVerification(process.env.NEXT_PUBLIC_URL + '/verifyEmail')
            const userRequest = await account.get();
            const userDetails = await databases.getDocument(String(process.env.NEXT_PUBLIC_APPWRITE_DATABASE_USERS), String(process.env.NEXT_PUBLIC_APPWRITE_COLLECTION_USER), userRequest.$id)
            setUser({
                ...userRequest,
                username: userDetails.username,
                avatar: userDetails.avatar,
                language: userDetails.language,
            })
        } catch (error) {
            throw error
        }
    }
    return (
        <UserContext.Provider value={{ user, userLoading, login, logout, signup }}>
            {children}
        </UserContext.Provider>
    )
}

// create the custom hook
export const useUser = () => useContext(UserContext);
loup
26 Jun, 2023, 09:41

(Sorry for the length, I want to give a max of context :D)

loup
26 Jun, 2023, 09:42

But as I use a context to have the state of the user, in layout.tsx, the value of user does not update (so false by default) since it is not in the context

Meldiron
26 Jun, 2023, 09:43

Hmm, it should work in layout, I think 🤔

If you do something like const userData = useUser(); and then useEffect(() => { console.log(userData); }, [userData]);, it should log everytime it changes

Meldiron
26 Jun, 2023, 09:43

Buut I might easily be wrong, I haven't worked with react too much

Meldiron
26 Jun, 2023, 09:44

If above works, from that useEffect you can change some kind of layout state (useState) between true/false so it knows if it should render outlet or not (or even redirect)

loup
26 Jun, 2023, 09:45

Considering this :

TypeScript
export const useUser = () => useContext(UserContext);

I guess useUser() hook update the value of user only in the UserContext

Meldiron
26 Jun, 2023, 09:46

Might be the case. Would there be anything wrogn with wrapping your whole app inside user context? I am thinking about having 2-layer layout.

First one on /*, where you add user context. Another one for /dashboard/*, that uses value from that user context.

This way you put all auth-protected pages under "dashboard" folder, and it will make them all protected by default, thanks to the second layout

loup
26 Jun, 2023, 09:50

U mean the RootLayout create the UserContext like as I currently have and another Layout for private route (like /account, /setting) and public route (for page who cant be seen by logged user like /login /signup) ?

Meldiron
26 Jun, 2023, 09:50

Yes, pretty much. But for simplicity, I would move /account and /settings to /dashboard/account /dashboard/settings. That way you only need to setup that /dashbaord layout to have protection, and you prevent code duplication

loup
26 Jun, 2023, 09:51

I dont have dashboard page actually but yeah I can find a name for all private route

Meldiron
26 Jun, 2023, 09:52

Name is up to you /user, /app.. If they go to that page without going to sub-page, you can redirect.. For example, going to /dashboard, you can simply send user to /dashboard/account (always)

loup
26 Jun, 2023, 10:38

its probably a stupid question but I have this problem. I dont wanna show the reset password form if the token and the secret are no available. For example ive got this link :

TypeScript
http://localhost:3000/resetPassword?userId=6491ce764d6de085bc87&secret=a4a063c0f067ef4344ab7ce4ef3e365f64bf3c235e4f03c769c40def255b20f62a499d427f59d6a0805d557ac15d5cb8ad1f5e6d9c167d3067b72f259928b13ddaaada1cf45363c09b1bc9ec1ea3585a398a7855b4a0aa7103b9e4e640510323b77e4068c2dafa967825387745823ac3588f211c14f13acb8843641fac6ed290&expire=2023-06-22+16%3A00%3A17.687

But how check if the token and the secret are available without launch the .updateRecovery() function

loup
26 Jun, 2023, 10:43

Because even if I the expire value is on the time, if the token is false, the form will be displayed

Reply

Reply to this thread by joining our Discord

Reply on Discord

Need support?

Join our Discord

Get community support by joining our Discord server.

Join Discord

Get premium support

Join Appwrite Pro and get email support from our team.

Learn more