Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions examples/with-auth/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
SESSION_SECRET = myverylonguniquesecretthatkeepsthingssafe
DISCORD_ID =
DISCORD_SECRET =
25 changes: 25 additions & 0 deletions examples/with-auth/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,31 @@ npm run dev
npm run dev -- --open
```

## Env Vars

Rename the example file and add your Discord OAuth credentials:

```bash
# rename example environment file
cp .env.example .env
```

Edit `.env` with your values:

```dotenv
DISCORD_ID=your-discord-client-id
DISCORD_SECRET=your-discord-client-secret
```

1. Create an application at [https://discord.com/developers/applications](https://discord.com/developers/applications) to obtain your client ID and secret.
2. In the app's **OAuth2 → Redirects** settings, add:

```text
http://localhost:3000/api/oauth/discord
```

For more details on the [start-oauth](https://github.com/thomasbuilds/start-oauth) integration, see the repository.

## Building

Solid apps are built with _presets_, which optimise your project for deployment to different environments.
Expand Down
5 changes: 4 additions & 1 deletion examples/with-auth/app.config.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import { defineConfig } from "@solidjs/start/config";
import tailwindcss from "@tailwindcss/vite";

export default defineConfig({});
export default defineConfig({
vite: { plugins: [tailwindcss()] }
});
18 changes: 10 additions & 8 deletions examples/with-auth/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,17 @@
"build": "vinxi build",
"start": "vinxi start"
},
"devDependencies": {
"@types/node": "^20.12.7"
},
"dependencies": {
"@solidjs/router": "^0.15.0",
"@solidjs/start": "^1.1.0",
"solid-js": "^1.9.5",
"unstorage": "1.10.2",
"vinxi": "^0.5.7"
"@solidjs/router": "^0.15.3",
"@solidjs/start": "^1.1.7",
"solid-js": "^1.9.7",
"start-oauth": "^1.2.4",
"unstorage": "1.16.1",
"vinxi": "^0.5.8"
},
"devDependencies": {
"@tailwindcss/vite": "^4.1.11",
"tailwindcss": "^4.1.11"
},
"engines": {
"node": ">=22"
Expand Down
40 changes: 1 addition & 39 deletions examples/with-auth/src/app.css
Original file line number Diff line number Diff line change
@@ -1,39 +1 @@
body {
font-family: Gordita, Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
}

a {
margin-right: 1rem;
}

main {
text-align: center;
padding: 1em;
margin: 0 auto;
}

h1 {
color: #335d92;
text-transform: uppercase;
font-size: 4rem;
font-weight: 100;
line-height: 1.1;
margin: 4rem auto;
max-width: 14rem;
}

p {
max-width: 14rem;
margin: 2rem auto;
line-height: 1.35;
}

@media (min-width: 480px) {
h1 {
max-width: none;
}

p {
max-width: none;
}
}
@import "tailwindcss";
20 changes: 14 additions & 6 deletions examples/with-auth/src/app.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,26 @@
// @refresh reload
import { Router } from "@solidjs/router";
import { type RouteDefinition, Router } from "@solidjs/router";
import { FileRoutes } from "@solidjs/start/router";
import { Suspense } from "solid-js";
import { querySession } from "./lib";
import Session from "./lib/Context";
import Nav from "./components/Nav";
import "./app.css";

export const route: RouteDefinition = {
preload: ({ location }) => querySession(location.pathname)
};

export default function App() {
return (
<Router
root={props => (
<>
<a href="/">Index</a>
<a href="/about">About</a>
<Suspense>{props.children}</Suspense>
</>
<Session>
<Suspense>
<Nav />
{props.children}
</Suspense>
</Session>
)}
>
<FileRoutes />
Expand Down
14 changes: 14 additions & 0 deletions examples/with-auth/src/components/Counter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { createSignal } from "solid-js";

export default function Counter() {
const [count, setCount] = createSignal(0);

return (
<button
class="w-[200px] rounded-full bg-gray-100 border-2 border-gray-300 focus:border-gray-400 active:border-gray-400 px-[2rem] py-[1rem]"
onclick={() => setCount(prev => prev + 1)}
>
Clicks: {count()}
</button>
);
}
37 changes: 37 additions & 0 deletions examples/with-auth/src/components/Nav.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { useMatch } from "@solidjs/router";
import { Show } from "solid-js";
import { useSession } from "~/lib/Context";

export default function Nav() {
const { signedIn, signOut } = useSession();
const isHome = useMatch(() => "/");
const isAbout = useMatch(() => "/about");

return (
<nav class="fixed top-0 left-0 w-full bg-sky-800 shadow-md z-50">
<ul class="container mx-auto flex items-center p-3">
<li
class={`mx-2 sm:mx-6 border-b-2 text-white ${
isHome() ? "border-sky-400" : "border-transparent hover:border-sky-500"
}`}
>
<a href="/">Home</a>
</li>
<li
class={`mx-2 sm:mx-6 border-b-2 text-white ${
isAbout() ? "border-sky-400 " : "border-transparent hover:border-sky-500"
}`}
>
<a href="/about">About</a>
</li>
<li class="ml-auto px-2 sm:px-6 text-white">
<Show when={signedIn()} fallback={<a href="/login">Login</a>}>
<button onclick={signOut} class="cursor-pointer">
Logout
</button>
</Show>
</li>
</ul>
</nav>
);
}
29 changes: 29 additions & 0 deletions examples/with-auth/src/lib/Context.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { createContext, useContext, type ParentProps } from "solid-js";
import { type AccessorWithLatest, useLocation, createAsync, useAction } from "@solidjs/router";
import { querySession, logout } from ".";
import type { Session } from "./server";

const Context = createContext<{
session: AccessorWithLatest<Session | null | undefined>;
signedIn: () => boolean;
signOut: () => Promise<never>;
}>();

export default function Session(props: ParentProps) {
const location = useLocation();
const session = createAsync(() => querySession(location.pathname), {
deferStream: true
});
const signOut = useAction(logout);
const signedIn = () => Boolean(session()?.id);

return (
<Context.Provider value={{ session, signedIn, signOut }}>{props.children}</Context.Provider>
);
}

export function useSession() {
const context = useContext(Context);
if (!context) throw new Error("useSession must be used within Session context");
return context;
}
54 changes: 22 additions & 32 deletions examples/with-auth/src/lib/db.ts
Original file line number Diff line number Diff line change
@@ -1,38 +1,28 @@
import { createStorage } from "unstorage";
import fsLiteDriver from "unstorage/drivers/fs-lite";

type User = {
interface User {
id: number;
username: string;
password: string;
};
email: string;
password?: string;
}

const storage = createStorage({
driver: fsLiteDriver({
base: "./.data"
})
});
storage.setItem("users:data", [{ id: 0, username: "kody", password: "twixrox" }]);
storage.setItem("users:counter", 1);
const storage = createStorage({ driver: fsLiteDriver({ base: "./.data" }) });

export const db = {
user: {
async create({ data }: { data: { username: string; password: string } }) {
const [{ value: users }, { value: index }] = await storage.getItems(["users:data", "users:counter"]);
const user = { ...data, id: index as number };
await Promise.all([
storage.setItem("users:data", [...(users as User[]), user]),
storage.setItem("users:counter", index as number + 1)
]);
return user;
},
async findUnique({ where: { username = undefined, id = undefined } }: { where: { username?: string; id?: number } }) {
const users = await storage.getItem("users:data") as User[];
if (id !== undefined) {
return users.find(user => user.id === id);
} else {
return users.find(user => user.username === username);
}
}
}
};
export async function createUser(data: Pick<User, "email" | "password">) {
const users = (await storage.getItem<User[]>("users:data")) ?? [];
const counter = (await storage.getItem<number>("users:counter")) ?? 1;
const user: User = { id: counter, ...data };
await Promise.all([
storage.setItem("users:data", [...users, user]),
storage.setItem("users:counter", counter + 1)
]);
return user;
}

export async function findUser({ email, id }: { email?: string; id?: number }) {
const users = (await storage.getItem<User[]>("users:data")) ?? [];
if (id) return users.find(u => u.id === id);
if (email) return users.find(u => u.email === email);
return undefined;
}
70 changes: 27 additions & 43 deletions examples/with-auth/src/lib/index.ts
Original file line number Diff line number Diff line change
@@ -1,53 +1,37 @@
import { action, query, redirect } from "@solidjs/router";
import { db } from "./db";
import {
getSession,
login,
logout as logoutSession,
register,
validatePassword,
validateUsername
} from "./server";
import { getSession, passwordSignIn } from "./server";

export const getUser = query(async () => {
"use server";
try {
const session = await getSession();
const userId = session.data.userId;
if (userId === undefined) throw new Error("User not found");
const user = await db.user.findUnique({ where: { id: userId } });
if (!user) throw new Error("User not found");
return { id: user.id, username: user.username };
} catch {
await logoutSession();
throw redirect("/login");
}
}, "user");
// Define which routes require authentication
const PROTECTED_ROUTES = ["/"];

const isProtectedRoute = (path: string) =>
PROTECTED_ROUTES.some(route =>
route.endsWith("/*")
? path.startsWith(route.slice(0, -2))
: path === route || path.startsWith(route + "/")
);

export const loginOrRegister = action(async (formData: FormData) => {
export const querySession = query(async (path: string) => {
"use server";
const username = String(formData.get("username"));
const password = String(formData.get("password"));
const loginType = String(formData.get("loginType"));
let error = validateUsername(username) || validatePassword(password);
if (error) return new Error(error);
const { data } = await getSession();
if (path === "/login" && data.id) return redirect("/");
if (data.id) return data;
if (isProtectedRoute(path)) throw redirect(`/login?redirect=${path}`);
return null;
}, "session");

try {
const user = await (loginType !== "login"
? register(username, password)
: login(username, password));
const session = await getSession();
await session.update(d => {
d.userId = user.id;
});
} catch (err) {
return err as Error;
}
return redirect("/");
export const passwdSignIn = action(async (formData: FormData) => {
"use server";
const email = formData.get("email");
const password = formData.get("password");
if (typeof email !== "string" || typeof password !== "string")
return new Error("Email and password are required");
return await passwordSignIn(email.trim().toLowerCase(), password);
});

export const logout = action(async () => {
"use server";
await logoutSession();
return redirect("/login");
const session = await getSession();
await session.update({ id: undefined });
throw redirect("/login", { revalidate: "session" });
});
Loading