S
SolidJS4mo ago
zobweyt

Redirecting on protected routes

Hey! Is there any recommended way to do route protecting on the server with SSR? For example, I need to redirect users from protected routes to the previous URL, if exists on router. Here's my current implementation:
type Session = { token: string | undefined };

const OPTIONS: SessionConfig = {
password: process.env.SESSION_SECRET,
};

export const getSession = async () => {
return await useSession<Session>(OPTIONS);
};

export const redirectUnauthenticated = cache(async (url: string) => {
"use server";
const session = await getSession();
if (session.data.token === undefined) {
throw redirect(url);
}
return {};
}, "redirectUnauthenticated");

export const createRedirectUnauthenticated = (url: string = "/login") => {
createAsync(() => redirectUnauthenticated(url));
};

export default function Page() {
createRedirectUnauthenticated("/login?redirect=/profile");

return ...
}
type Session = { token: string | undefined };

const OPTIONS: SessionConfig = {
password: process.env.SESSION_SECRET,
};

export const getSession = async () => {
return await useSession<Session>(OPTIONS);
};

export const redirectUnauthenticated = cache(async (url: string) => {
"use server";
const session = await getSession();
if (session.data.token === undefined) {
throw redirect(url);
}
return {};
}, "redirectUnauthenticated");

export const createRedirectUnauthenticated = (url: string = "/login") => {
createAsync(() => redirectUnauthenticated(url));
};

export default function Page() {
createRedirectUnauthenticated("/login?redirect=/profile");

return ...
}
However, this implementation is not the best because it shows the Page to an unauthorized user for a few milliseconds before redirecting. In addition, if using the snippet below for the Page route, it won't show the Page to user for a few milliseconds before redirecting, but it can run the preload at any state of my app, which will break all redirects and always redirect to /login?redirect=/profile instead of the ?redirect parameter:
export const route = {
preload: () => createRedirectUnauthenticated("/login?redirect=/profile"),
} satisfies RouteDefinition;
export const route = {
preload: () => createRedirectUnauthenticated("/login?redirect=/profile"),
} satisfies RouteDefinition;
Before, I tried using useNavigate, but the code needs to be run on the client in this case. However, I need to perform this check on the server first. Also, as I mentioned before, I want always to redirect the unauthorized user to the previous URL. I tried doing it with event?.router?.previousUrl from getRequestEvent() in redirectUnauthenticated function, but it's always returns undefied. How can I solve this issue correctly?
11 Replies
zobweyt
zobweyt4mo ago
GitHub
solid-start/examples/with-auth at main · solidjs/solid-start
SolidStart, the Solid app framework. Contribute to solidjs/solid-start development by creating an account on GitHub.
GitHub
swapify/swapify_web/src/lib/auth/auth-services.ts at main · Christo...
Contribute to Christopher2K/swapify development by creating an account on GitHub.
Madaxen86
Madaxen864mo ago
The first link you’ve shared is the way to go. You can add createAsync(…,{deferStream: true }) this should prevent the page from streaming before the promise has resolved and should also prevent the page from being shown for a few milliseconds.
zobweyt
zobweyt4mo ago
createAsync(…,{deferStream: true })
it did not help with this issue
zobweyt
zobweyt4mo ago
zobweyt
zobweyt4mo ago
The first link you’ve shared is the way to go.
but even there the age will be shown for a few milliseconds it actually shows the page for a few milliseconds when it's first time invoked then it's cached but isn't there a better way to do it? feels like such a simple feature
Madaxen86
Madaxen864mo ago
To be more strict you can wrap your children in a Show component:

const isLoggedIn = createAsync(redirectUnauthnticated);

return <Show when={isLoggenIn}
fallback=“…loading”>

>
</Show>

const isLoggedIn = createAsync(redirectUnauthnticated);

return <Show when={isLoggenIn}
fallback=“…loading”>

>
</Show>
zobweyt
zobweyt4mo ago
so in solid start it’s not possible to protect the routes? only the content of them? i’m not sure that this is gonna work because this code is run on the client and isLoggedIn would be undefined there all the time
export const isAuthenticated = cache(async () => {
"use server";
const session = await getSession();

return session.data.auth?.token !== undefined;
}, "isAuthenticated");
export const isAuthenticated = cache(async () => {
"use server";
const session = await getSession();

return session.data.auth?.token !== undefined;
}, "isAuthenticated");
export type AuthContextValue = () => Awaited<ReturnType<typeof isAuthenticated> | undefined>;

export const AuthContext = createContext<AuthContextValue>(() => undefined);

export const AuthProvider: ParentComponent = (props) => {
const isAuthed = createAsync(() => isAuthenticated(), {deferStream: true});

return <AuthContext.Provider value={isAuthed}>{props.children}</AuthContext.Provider>;
};

export const useAuth = () => useContext(AuthContext);
export type AuthContextValue = () => Awaited<ReturnType<typeof isAuthenticated> | undefined>;

export const AuthContext = createContext<AuthContextValue>(() => undefined);

export const AuthProvider: ParentComponent = (props) => {
const isAuthed = createAsync(() => isAuthenticated(), {deferStream: true});

return <AuthContext.Provider value={isAuthed}>{props.children}</AuthContext.Provider>;
};

export const useAuth = () => useContext(AuthContext);
export default function Protected(props: RouteSectionProps) {
const navigate = useNavigate();
const isAuthed = useAuth();
console.log(isAuthed());

if (!isAuthed()) {
navigate(`/login?redirect=${props.location.pathname}`)
}

return (
<Show when={isAuthed()}>
{props.children};
</Show>
);
}
export default function Protected(props: RouteSectionProps) {
const navigate = useNavigate();
const isAuthed = useAuth();
console.log(isAuthed());

if (!isAuthed()) {
navigate(`/login?redirect=${props.location.pathname}`)
}

return (
<Show when={isAuthed()}>
{props.children};
</Show>
);
}
now, if I first open protected as an unauthorized user, i will be redirected to /login?redirect=/protected, but if I sign in on the login page, it will redirect me and isAuthed won't be updated. so circular redirection is possible
Madaxen86
Madaxen864mo ago
The default config of solid start uses streaming. So the server will start streaming the response (content of the page) before the async data has resolved. You can change that if you want but only for the whole app: https://docs.solidjs.com/solid-start/reference/server/create-handler It will also work on the client as solid will create an api route which is then called by createAsnyc. So that code runs only on the server and is safe.
zobweyt
zobweyt4mo ago
yeah, it works, but now there's a problem with redirection
export const login = action(async (form: FormData) => {
"use server";

const email = String(form.get("email"));
const password = String(form.get("password"));

const { data, error } = await getToken(email, password);

if (data) {
await updateAuth({ token: data.access_token, expires_at: data.expires_at });
}

if (error) {
return error;
}

throw redirect(String(form.get("redirect") || "/"));
});

export const logout = action(async () => {
"use server";

await resetSession();
throw redirect("/");
});
export const login = action(async (form: FormData) => {
"use server";

const email = String(form.get("email"));
const password = String(form.get("password"));

const { data, error } = await getToken(email, password);

if (data) {
await updateAuth({ token: data.access_token, expires_at: data.expires_at });
}

if (error) {
return error;
}

throw redirect(String(form.get("redirect") || "/"));
});

export const logout = action(async () => {
"use server";

await resetSession();
throw redirect("/");
});
so here are my actions and if i go to the protected route when i'm not authorized, it will: 1. redirect me to the /login?redirect=/protected (logs false) 2. i'll sign in, and it will redirect me to the /protected (logs false) 3. since isAuthed hasn't been updated for some reason, it will redirect me back to /login?redirect=/protected, which will redirect me to the / 4. if i again try to reach /protected, it'll work fine (logs true) so the problem is with redirects so it works now!!
export const redirectUnauthorized = cache(async (url: string) => {
"use server";
const session = await getSession();

if (!session.data.auth) {
throw redirect(url);
}

return true;
}, "redirectUnauthorized");
export const redirectUnauthorized = cache(async (url: string) => {
"use server";
const session = await getSession();

if (!session.data.auth) {
throw redirect(url);
}

return true;
}, "redirectUnauthorized");
// (protected).tsx

import { createAsync, RouteSectionProps } from "@solidjs/router";
import { Show } from "solid-js";
import { redirectUnauthorized } from "~/lib/auth/services";

export default function Index(props: RouteSectionProps) {
const isAuthorized = createAsync(() => redirectUnauthorized(`/login?redirect=${props.location.pathname}`));

return <Show when={isAuthorized()}>{props.children}</Show>;
}
// (protected).tsx

import { createAsync, RouteSectionProps } from "@solidjs/router";
import { Show } from "solid-js";
import { redirectUnauthorized } from "~/lib/auth/services";

export default function Index(props: RouteSectionProps) {
const isAuthorized = createAsync(() => redirectUnauthorized(`/login?redirect=${props.location.pathname}`));

return <Show when={isAuthorized()}>{props.children}</Show>;
}
thanks a lot!!
Madaxen86
Madaxen864mo ago
In your login action you need to call revalidate(redirectUnauthorized.key) once login is successful. This will update the cached value. (Also in logout obviously…)
zobweyt
zobweyt4mo ago
it updates already without doing it
Want results from more Discord servers?
Add your server