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
5 changes: 4 additions & 1 deletion examples/with-auth/.env.example
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
SESSION_SECRET = myverylonguniquesecretthatkeepsthingssafe
# Generate using `openssl rand -hex 32`
SESSION_SECRET = myverylongsessionsecretkeythatishouldchange

# Get credentials by creating an application at https://discord.com/developers/applications
DISCORD_ID =
DISCORD_SECRET =
60 changes: 23 additions & 37 deletions examples/with-auth/README.md
Original file line number Diff line number Diff line change
@@ -1,55 +1,41 @@
# SolidStart
# SolidStart Template

Everything you need to build a Solid project, powered by [`solid-start`](https://start.solidjs.com);
The **with-auth** example demonstrates native, context-based authentication featuring OAuth and email-password login.

## Creating a project
## Installation

```bash
# create a new project in the current directory
npm init solid@latest
Generate the **with-auth** template using your preferred package manager

# create a new project in my-app
npm init solid@latest my-app
```bash
# using npm
npm create solid@latest -- -s -t with-auth
```

## Developing

Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:

```bash
npm run dev

# or start the server and open the app in a new browser tab
npm run dev -- --open
# using pnpm
pnpm create solid@latest -s -t with-auth
```

## Env Vars

Rename the example file and add your Discord OAuth credentials:

```bash
# rename example environment file
cp .env.example .env
# using bun
bun create solid@latest --s --t with-auth
```

Edit `.env` with your values:
## Configuration

```dotenv
DISCORD_ID=your-discord-client-id
DISCORD_SECRET=your-discord-client-secret
```
1. Rename `.env.example` to `.env`

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:
2. For Discord OAuth2 to work, update `.env` with your credentials:

```text
http://localhost:3000/api/oauth/discord
```dotenv
DISCORD_ID=your-discord-client-id
DISCORD_SECRET=your-discord-client-secret
```

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.
- Create a Discord application at [discord.com/developers/applications](https://discord.com/developers/applications) to get your Client ID and Secret.
- In the app's **OAuth2 → URL Generator** or **Redirects** section, add the following redirect URI:
```
http://localhost:3000/api/oauth/discord
```

By default, `npm run build` will generate a Node app that you can run with `npm start`. To use a different preset, add it to the `devDependencies` in `package.json` and specify in your `app.config.js`.
3. To configure additional providers, refer to the [start-oauth](https://github.com/thomasbuilds/start-oauth#README) documentation
2 changes: 2 additions & 0 deletions examples/with-auth/app.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,7 @@ import { defineConfig } from "@solidjs/start/config";
import tailwindcss from "@tailwindcss/vite";

export default defineConfig({
ssr: true, // false for client-side rendering only
server: { preset: "" }, // your deployment
vite: { plugins: [tailwindcss()] }
});
11 changes: 6 additions & 5 deletions examples/with-auth/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,17 @@
"start": "vinxi start"
},
"dependencies": {
"@solidjs/meta": "^0.29.4",
"@solidjs/router": "^0.15.3",
"@solidjs/start": "^1.1.7",
"solid-js": "^1.9.7",
"start-oauth": "^1.2.4",
"unstorage": "1.16.1",
"solid-js": "^1.9.9",
"start-oauth": "^1.3.0",
"unstorage": "1.17.1",
"vinxi": "^0.5.8"
},
"devDependencies": {
"@tailwindcss/vite": "^4.1.11",
"tailwindcss": "^4.1.11"
"@tailwindcss/vite": "^4.1.13",
"tailwindcss": "^4.1.13"
},
"engines": {
"node": ">=22"
Expand Down
Binary file removed examples/with-auth/public/favicon.ico
Binary file not shown.
92 changes: 92 additions & 0 deletions examples/with-auth/public/favicon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
16 changes: 16 additions & 0 deletions examples/with-auth/src/app.css
Original file line number Diff line number Diff line change
@@ -1 +1,17 @@
@import "tailwindcss";

#app {
user-select: none;
}

main {
@apply flex flex-col items-center justify-center min-h-screen bg-gray-50 gap-8 px-4;
}

h1 {
@apply uppercase text-6xl text-sky-700 font-thin;
}

button {
cursor: pointer;
}
22 changes: 13 additions & 9 deletions examples/with-auth/src/app.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
// @refresh reload
import { type RouteDefinition, Router } from "@solidjs/router";
import { FileRoutes } from "@solidjs/start/router";
import { MetaProvider } from "@solidjs/meta";
import { Suspense } from "solid-js";
import { querySession } from "./lib";
import Session from "./lib/Context";
import { querySession } from "./auth";
import Auth from "./components/Context";
import Nav from "./components/Nav";
import ErrorNotification from "./components/Error";
import "./app.css";

export const route: RouteDefinition = {
Expand All @@ -15,12 +16,15 @@ export default function App() {
return (
<Router
root={props => (
<Session>
<Suspense>
<Nav />
{props.children}
</Suspense>
</Session>
<MetaProvider>
<Auth>
<Suspense>
<Nav />
{props.children}
<ErrorNotification />
</Suspense>
</Auth>
</MetaProvider>
)}
>
<FileRoutes />
Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { action, query, redirect } from "@solidjs/router";
import { getSession, passwordSignIn } from "./server";
import { getSession, passwordLogin } from "./server";

// Define which routes require authentication
// Define routes that require being logged in
const PROTECTED_ROUTES = ["/"];

const isProtectedRoute = (path: string) =>
const isProtected = (path: string) =>
PROTECTED_ROUTES.some(route =>
route.endsWith("/*")
? path.startsWith(route.slice(0, -2))
Expand All @@ -16,17 +16,17 @@ export const querySession = query(async (path: string) => {
const { data } = await getSession();
if (path === "/login" && data.id) return redirect("/");
if (data.id) return data;
if (isProtectedRoute(path)) throw redirect(`/login?redirect=${path}`);
if (isProtected(path)) throw redirect(`/login?redirect=${path}`);
return null;
}, "session");

export const passwdSignIn = action(async (formData: FormData) => {
export const formLogin = 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);
return await passwordLogin(email.trim().toLowerCase(), password);
});

export const logout = action(async () => {
Expand Down
74 changes: 74 additions & 0 deletions examples/with-auth/src/auth/server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { redirect } from "@solidjs/router";
import { useSession } from "vinxi/http";
import { getRandomValues, subtle, timingSafeEqual } from "crypto";
import { createUser, findUser } from "./db";

export interface Session {
id: number;
email: string;
}

export const getSession = () =>
useSession<Session>({
password: process.env.SESSION_SECRET!
});

export async function createSession(user: Session, redirectTo?: string) {
const validDest = redirectTo?.[0] === "/" && redirectTo[1] !== "/";
const session = await getSession();
await session.update(user);
return redirect(validDest ? redirectTo : "/");
}

async function createHash(password: string) {
const salt = getRandomValues(new Uint8Array(16));
const saltHex = Buffer.from(salt).toString("hex");
const key = await subtle.deriveBits(
{
name: "PBKDF2",
salt,
iterations: 100_000,
hash: "SHA-512"
},
await subtle.importKey("raw", new TextEncoder().encode(password), "PBKDF2", false, [
"deriveBits"
]),
512
);
const hash = Buffer.from(key).toString("hex");
return `${saltHex}:${hash}`;
}

async function checkPassword(storedPassword: string, providedPassword: string) {
const [storedSalt, storedHash] = storedPassword.split(":");
if (!storedSalt || !storedHash) throw new Error("Invalid stored password format");
const key = await subtle.deriveBits(
{
name: "PBKDF2",
salt: Buffer.from(storedSalt, "hex"),
iterations: 100_000,
hash: "SHA-512"
},
await subtle.importKey("raw", new TextEncoder().encode(providedPassword), "PBKDF2", false, [
"deriveBits"
]),
512
);
const hash = Buffer.from(key);
const stored = Buffer.from(storedHash, "hex");
if (stored.length !== hash.length || !timingSafeEqual(stored, hash))
throw new Error("Invalid email or password");
}

export async function passwordLogin(email: string, password: string) {
let user = await findUser({ email });
if (!user)
user = await createUser({
email,
password: await createHash(password)
});
else if (!user.password)
throw new Error("Account exists via OAuth. Sign in with your OAuth provider");
else await checkPassword(user.password, password);
return createSession(user);
}
Loading
Loading