feat: initial commit
commit
f31b1a72fb
@ -0,0 +1 @@
|
||||
SESSION_SECRET=CHANGEME
|
@ -0,0 +1,26 @@
|
||||
|
||||
dist
|
||||
.solid
|
||||
.output
|
||||
.vercel
|
||||
.netlify
|
||||
netlify
|
||||
/keepForLater
|
||||
/postgres-backup.sql.gz
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
|
||||
# IDEs and editors
|
||||
/.idea
|
||||
.project
|
||||
.classpath
|
||||
*.launch
|
||||
.settings/
|
||||
|
||||
# Temp
|
||||
gitignore
|
||||
|
||||
# System Files
|
||||
.DS_Store
|
||||
Thumbs.db
|
@ -0,0 +1,39 @@
|
||||
# SolidStart
|
||||
|
||||
## Setting up the project for devs
|
||||
|
||||
```bash
|
||||
npm install
|
||||
mkdir -p ../solid-directus-app/database/;
|
||||
mkdir ../solid-directus-app/uploads/;
|
||||
podman unshare chown -R 1000:1000 ../solid-directus-app/database/
|
||||
podman unshare chown -R 1000:1000 ../solid-directus-app/uploads/
|
||||
```
|
||||
|
||||
## Developing
|
||||
|
||||
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
|
||||
|
||||
```bash
|
||||
docker-compose up -d
|
||||
npm run dev
|
||||
|
||||
# or start the server and open the app in a new browser tab
|
||||
npm run dev -- --open
|
||||
```
|
||||
|
||||
docker-compose ps -q database
|
||||
|
||||
## Backing up the database
|
||||
|
||||
```bash
|
||||
docker exec $(docker-compose ps -q database) /bin/bash \
|
||||
-c "/usr/bin/pg_dump -U \$POSTGRES_USER \$POSTGRES_DB" \
|
||||
| gzip -9 > postgres-backup.sql.gz
|
||||
```
|
||||
|
||||
## Building
|
||||
|
||||
Solid apps are built with _adapters_, which optimise your project for deployment to different environments.
|
||||
|
||||
By default, `npm run build` will generate a Node app that you can run with `node build`. To use a different adapter, add it to the `devDependencies` in `package.json` and specify in your `vite.config.js`.
|
@ -0,0 +1,64 @@
|
||||
version: "3"
|
||||
services:
|
||||
database:
|
||||
container_name: database
|
||||
image: postgis/postgis:13-master
|
||||
volumes:
|
||||
- ./initdb:/docker-entrypoint-initdb.d:Z
|
||||
- ../solid-directus-app/database:/var/lib/postgresql/data:Z
|
||||
networks:
|
||||
- directus
|
||||
environment:
|
||||
POSTGRES_USER: "directus"
|
||||
POSTGRES_PASSWORD: "directus"
|
||||
POSTGRES_DB: "directus"
|
||||
|
||||
cache:
|
||||
container_name: cache
|
||||
image: redis:6
|
||||
networks:
|
||||
- directus
|
||||
|
||||
directus:
|
||||
container_name: directus
|
||||
image: directus/directus:latest
|
||||
ports:
|
||||
- 8055:8055
|
||||
volumes:
|
||||
# By default, uploads are stored in /directus/uploads
|
||||
# Always make sure your volumes matches the storage root when using
|
||||
# local driver
|
||||
- ../solid-directus-app/uploads:/directus/uploads:Z
|
||||
# Make sure to also mount the volume when using SQLite
|
||||
# - ./database:/directus/database
|
||||
# If you want to load extensions from the host
|
||||
# - ./extensions:/directus/extensions
|
||||
networks:
|
||||
- directus
|
||||
depends_on:
|
||||
- cache
|
||||
- database
|
||||
environment:
|
||||
KEY: "255d861b-5ea1-5996-9aa3-922530ec40b1"
|
||||
SECRET: "6116487b-cda1-52c2-b5b5-c8022c45e263"
|
||||
|
||||
DB_CLIENT: "pg"
|
||||
DB_HOST: "database"
|
||||
DB_PORT: "5432"
|
||||
DB_DATABASE: "directus"
|
||||
DB_USER: "directus"
|
||||
DB_PASSWORD: "directus"
|
||||
|
||||
CACHE_ENABLED: "true"
|
||||
CACHE_STORE: "redis"
|
||||
CACHE_REDIS: "redis://cache:6379"
|
||||
|
||||
ADMIN_EMAIL: "admin@example.com"
|
||||
ADMIN_PASSWORD: "d1r3ctu5"
|
||||
|
||||
# Make sure to set this in production
|
||||
# (see https://docs.directus.io/configuration/config-options/#general)
|
||||
# PUBLIC_URL: 'https://directus.example.com'
|
||||
|
||||
networks:
|
||||
directus:
|
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,34 @@
|
||||
{
|
||||
"name": "my-app",
|
||||
"scripts": {
|
||||
"dev": "solid-start dev",
|
||||
"build": "solid-start build",
|
||||
"start": "solid-start start",
|
||||
"reformat": "prettier --write ."
|
||||
},
|
||||
"type": "module",
|
||||
"devDependencies": {
|
||||
"autoprefixer": "^10.4.2",
|
||||
"cookie": "^0.5.0",
|
||||
"daisyui": "^2.14.3",
|
||||
"postcss": "^8.4.6",
|
||||
"prettier": "^2.6.2",
|
||||
"solid-app-router": "^0.3.2",
|
||||
"solid-js": "^1.3.17",
|
||||
"solid-meta": "^0.27.3",
|
||||
"solid-start": "v0.1.0-alpha.80",
|
||||
"solid-start-node": "next",
|
||||
"tailwindcss": "^3.0.24",
|
||||
"typescript": "^4.6.4",
|
||||
"vite": "^2.9.9"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
},
|
||||
"dependencies": {
|
||||
"@urql/exchange-auth": "^0.1.7",
|
||||
"graphql": "^16.4.0",
|
||||
"turbo-solid": "^1.0.0",
|
||||
"urql": "^2.2.0"
|
||||
}
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
Binary file not shown.
After Width: | Height: | Size: 664 B |
@ -0,0 +1,72 @@
|
||||
import { useLocation } from "solid-app-router";
|
||||
import {
|
||||
createComputed,
|
||||
createContext,
|
||||
createEffect,
|
||||
createResource,
|
||||
useContext,
|
||||
} from "solid-js";
|
||||
import { createStore } from "solid-js/store";
|
||||
import server, { redirect } from "solid-start/server";
|
||||
import { useRefreshy } from "~/util/refreshy";
|
||||
import { awaitJson, createRC } from "~/util";
|
||||
import { getSessionData } from "./server";
|
||||
|
||||
export const authData = (
|
||||
options_: { redirectNoAuth?: string; redirectWithAuth?: string } = {}
|
||||
) =>
|
||||
function () {
|
||||
const [refreshy] = useRefreshy();
|
||||
|
||||
const fetcher = refreshy(
|
||||
server(async (options: typeof options_) => {
|
||||
const data = await getSessionData(createRC(server.request));
|
||||
|
||||
if (options.redirectNoAuth && !data?.username) {
|
||||
throw redirect(options.redirectNoAuth);
|
||||
}
|
||||
|
||||
if (options.redirectWithAuth && data?.username) {
|
||||
throw redirect(options.redirectWithAuth);
|
||||
}
|
||||
|
||||
return { username: data.username };
|
||||
})
|
||||
);
|
||||
|
||||
return createResource(() => fetcher(options_), { deferStream: true });
|
||||
};
|
||||
|
||||
/*
|
||||
const AuthContext = createContext();
|
||||
export const AuthProvider = (props) => {
|
||||
const [session, { refetch }] = createResource(server(() => getSessionData()))
|
||||
const [state, setState] = createStore([{
|
||||
session: {}
|
||||
}])
|
||||
|
||||
const location = useLocation();
|
||||
|
||||
createEffect(function() {
|
||||
const url = location.pathname;
|
||||
refetch()
|
||||
})
|
||||
|
||||
createEffect(function() {
|
||||
setState(0,'session',session())
|
||||
})
|
||||
//createComputed(session)
|
||||
|
||||
return <AuthContext.Provider value={state}>
|
||||
{props.children}
|
||||
</AuthContext.Provider>
|
||||
}
|
||||
|
||||
export const useAuth = () => {
|
||||
const authContext = useContext(AuthContext)
|
||||
|
||||
console.log(authContext)
|
||||
|
||||
return authContext[0];
|
||||
}
|
||||
*/
|
@ -0,0 +1,194 @@
|
||||
import { createCookieSessionStorage } from "solid-start/session";
|
||||
import server, { json, redirect } from "solid-start/server";
|
||||
import { gql, OperationResult } from "urql";
|
||||
import { Session } from "solid-start/session/sessions";
|
||||
import { awaitJson, createRC, RC } from "~/util";
|
||||
import { createDirectusSystemClient } from "~/server/directus";
|
||||
|
||||
const storage = createCookieSessionStorage({
|
||||
cookie: {
|
||||
name: "RJ_session",
|
||||
// secure doesn't work on localhost for Safari
|
||||
// https://web.dev/when-to-use-local-https/
|
||||
secure: true,
|
||||
secrets: ["hello"],
|
||||
sameSite: "lax",
|
||||
path: "/",
|
||||
maxAge: 60 * 60 * 24 * 30,
|
||||
httpOnly: true,
|
||||
},
|
||||
});
|
||||
|
||||
export const login = async (
|
||||
username,
|
||||
password,
|
||||
redirectTo = "/" as string | false
|
||||
) => {
|
||||
const client = createDirectusSystemClient();
|
||||
|
||||
let result: OperationResult;
|
||||
|
||||
try {
|
||||
result = await client
|
||||
.mutation(
|
||||
gql`
|
||||
mutation {
|
||||
auth_login(email: "${username}", password: "${password}") {
|
||||
access_token
|
||||
refresh_token
|
||||
expires
|
||||
}
|
||||
}
|
||||
`
|
||||
)
|
||||
.toPromise();
|
||||
|
||||
if (result.error) {
|
||||
console.dir(result.error);
|
||||
throw new Error(result.error.response);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
|
||||
if (!result) return;
|
||||
|
||||
const session = await storage.getSession();
|
||||
session.set("username", username);
|
||||
setAuthInSession(session, result.data.auth_login);
|
||||
|
||||
const response = {
|
||||
headers: {
|
||||
"Set-Cookie": await storage.commitSession(session),
|
||||
},
|
||||
};
|
||||
|
||||
throw redirectTo ? redirect(redirectTo, response) : json({}, response);
|
||||
};
|
||||
|
||||
export const isLoggedIn = (session: Session) => {
|
||||
return session.has("accessToken");
|
||||
};
|
||||
|
||||
const setAuthInSession = (
|
||||
session: Session,
|
||||
{ access_token, refresh_token, expires }
|
||||
) => {
|
||||
session.set("accessToken", access_token);
|
||||
session.set("refreshToken", refresh_token);
|
||||
session.set("tokenTime", new Date().getTime());
|
||||
if (expires != null) {
|
||||
session.set("tokenExpires", expires);
|
||||
}
|
||||
console.log("new session data", session.data);
|
||||
};
|
||||
|
||||
export const refreshAuthToken = async (rc: RC) => {
|
||||
const session = await getSession(rc);
|
||||
|
||||
if (!isLoggedIn(session)) {
|
||||
console.log("no refresh token");
|
||||
return;
|
||||
}
|
||||
|
||||
const refreshToken = session.get("refreshToken");
|
||||
const tokenTime = session.get("tokenTime");
|
||||
const tokenExpires = session.get("tokenExpires");
|
||||
|
||||
// console.log(tokenTime, Number.isInteger(tokenTime))
|
||||
// console.log(tokenExpires, Number.isInteger(tokenExpires))
|
||||
|
||||
if (new Date().getTime() < tokenTime + tokenExpires / 2) {
|
||||
console.log("not expired");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("token gets refreshed", refreshToken);
|
||||
|
||||
let result: OperationResult;
|
||||
const client = createDirectusSystemClient();
|
||||
|
||||
try {
|
||||
result = await client
|
||||
.mutation(
|
||||
gql`
|
||||
mutation {
|
||||
auth_refresh(refresh_token: "${refreshToken}") {
|
||||
access_token
|
||||
refresh_token
|
||||
expires
|
||||
}
|
||||
}
|
||||
|
||||
`
|
||||
)
|
||||
.toPromise();
|
||||
|
||||
if (result.error) {
|
||||
console.log("code", result.error.graphQLErrors[0].extensions.code);
|
||||
console.log(result.error.graphQLErrors[0].extensions.graphqlErrors);
|
||||
console.dir(result.error);
|
||||
throw new Error(result.error.response);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
|
||||
if (!result?.data?.auth_refresh) return;
|
||||
|
||||
setAuthInSession(session, result.data.auth_refresh);
|
||||
|
||||
const newCookie = await storage.commitSession(session);
|
||||
|
||||
if (rc.responseHeaders) {
|
||||
rc.responseHeaders.set("Set-Cookie", newCookie);
|
||||
}
|
||||
|
||||
return json(
|
||||
{},
|
||||
{
|
||||
headers: {
|
||||
"Set-Cookie": newCookie,
|
||||
},
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export const serveRefreshAuthToken = server(() =>
|
||||
refreshAuthToken(createRC(server.request, server.responseHeaders))
|
||||
);
|
||||
|
||||
export const getSession = async (rc: RC) => {
|
||||
if (rc.context.session) {
|
||||
return rc.context.session;
|
||||
}
|
||||
|
||||
const session = await storage.getSession(rc.request.headers.get("Cookie"));
|
||||
rc.context.session = session;
|
||||
|
||||
return session;
|
||||
};
|
||||
|
||||
export const getSessionData = async (rc: RC) => {
|
||||
const session = await getSession(rc);
|
||||
|
||||
return session.data;
|
||||
};
|
||||
|
||||
export const logout = async function (
|
||||
rc: RC,
|
||||
redirectTo = "/" as string | false
|
||||
) {
|
||||
const session = await getSession(rc);
|
||||
|
||||
const response = {
|
||||
headers: {
|
||||
"Set-Cookie": await storage.destroySession(session),
|
||||
},
|
||||
};
|
||||
|
||||
throw redirectTo ? redirect(redirectTo, response) : json({}, response);
|
||||
};
|
||||
export const serveLogout = server(() =>
|
||||
logout(createRC(server.request), false)
|
||||
);
|
@ -0,0 +1,13 @@
|
||||
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(count() + 1)}
|
||||
>
|
||||
Clicks: {count}
|
||||
</button>
|
||||
);
|
||||
}
|
@ -0,0 +1,4 @@
|
||||
import { hydrate } from "solid-js/web";
|
||||
import { StartClient } from "solid-start/entry-client";
|
||||
|
||||
hydrate(() => <StartClient />, document);
|
@ -0,0 +1,18 @@
|
||||
import {
|
||||
StartServer,
|
||||
createHandler,
|
||||
renderAsync,
|
||||
renderStream,
|
||||
} from "solid-start/entry-server";
|
||||
import { inlineServerModules } from "solid-start/server";
|
||||
import { refreshAuthToken } from "./auth/server";
|
||||
import { createRefreshMiddleware } from "./util/refreshy";
|
||||
import { createRC } from "./util";
|
||||
|
||||
export default createHandler(
|
||||
inlineServerModules,
|
||||
createRefreshMiddleware((req, resHeaders) =>
|
||||
refreshAuthToken(createRC(req, resHeaders))
|
||||
),
|
||||
renderAsync((context) => <StartServer context={context} />)
|
||||
);
|
@ -0,0 +1,3 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
@ -0,0 +1,41 @@
|
||||
// @refresh reload
|
||||
import { Suspense } from "solid-js";
|
||||
import { Links, Meta, Routes, Scripts } from "solid-start/root";
|
||||
import { ErrorBoundary } from "solid-start/error-boundary";
|
||||
import "./index.css";
|
||||
import { RefreshFunction, RefreshyProvider } from "./util/refreshy";
|
||||
import { serveRefreshAuthToken } from "./auth/server";
|
||||
|
||||
const refresh: RefreshFunction = async () => {
|
||||
console.log("refresh jwt");
|
||||
await serveRefreshAuthToken();
|
||||
|
||||
return Math.round(Math.random() * 1000);
|
||||
};
|
||||
|
||||
export default function Root() {
|
||||
return (
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<Meta />
|
||||
<Links />
|
||||
</head>
|
||||
<body class="antialiased">
|
||||
<ErrorBoundary>
|
||||
<Suspense>
|
||||
<RefreshyProvider ttl={5} refresh={refresh}>
|
||||
<Suspense
|
||||
fallback={<div class="w-96 mx-auto my-5">fetching</div>}
|
||||
>
|
||||
<Routes />
|
||||
</Suspense>
|
||||
</RefreshyProvider>
|
||||
</Suspense>
|
||||
</ErrorBoundary>
|
||||
<Scripts />
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
import { Link } from "solid-app-router";
|
||||
|
||||
export default function NotFound() {
|
||||
return (
|
||||
<main class="text-center mx-auto text-gray-700 p-4">
|
||||
<h1 class="max-6-xs text-6xl text-sky-700 font-thin uppercase my-16">
|
||||
Not Found
|
||||
</h1>
|
||||
<p class="mt-8">
|
||||
Visit{" "}
|
||||
<Link
|
||||
href="https://solidjs.com"
|
||||
target="_blank"
|
||||
class="text-sky-600 hover:underline"
|
||||
>
|
||||
solidjs.com
|
||||
</Link>{" "}
|
||||
to learn how to build Solid apps.
|
||||
</p>
|
||||
<p class="my-4">
|
||||
<Link href="/" class="text-sky-600 hover:underline">
|
||||
Home
|
||||
</Link>
|
||||
{" - "}
|
||||
<Link href="/about" class="text-sky-600 hover:underline">
|
||||
About Page
|
||||
</Link>
|
||||
</p>
|
||||
</main>
|
||||
);
|
||||
}
|
@ -0,0 +1,82 @@
|
||||
import { Link, useRouteData } from "solid-app-router";
|
||||
import { createEffect, createResource, For, Show } from "solid-js";
|
||||
import server from "solid-start/server";
|
||||
import { login, serveLogout } from "~/auth/server";
|
||||
import { authData } from "~/auth/client";
|
||||
import { composeRouteData, useServer } from "~/util";
|
||||
import { serveTodos } from "~/server/todos";
|
||||
import { useRefreshy } from "~/util/refreshy";
|
||||
|
||||
export const routeData = () => {
|
||||
const [refreshy] = useRefreshy();
|
||||
|
||||
return composeRouteData({
|
||||
auth: authData(),
|
||||
todos: () => createResource(refreshy(() => serveTodos())),
|
||||
})();
|
||||
};
|
||||
|
||||
type Todo = { title: string };
|
||||
|
||||
export default function Home() {
|
||||
const [data, refetch] = useRouteData();
|
||||
|
||||
const onLogin = useServer(
|
||||
server(async () => {
|
||||
await login("admin@example.com", "d1r3ctu5", false);
|
||||
}),
|
||||
refetch
|
||||
);
|
||||
|
||||
const onLogout = useServer(serveLogout, refetch);
|
||||
|
||||
return (
|
||||
<main class="text-center mx-auto container text-gray-700">
|
||||
<div class="my-7 flex items-center gap-4 justify-end">
|
||||
<Show when={data?.auth?.username}>
|
||||
<div>Hey {data.auth.username}</div>
|
||||
</Show>
|
||||
|
||||
<Show
|
||||
when={data?.auth?.username}
|
||||
fallback={
|
||||
<>
|
||||
<button class="btn btn-sm" onClick={onLogin}>
|
||||
Login instant
|
||||
</button>
|
||||
<Link class="btn btn-sm" href="/login">
|
||||
Login form
|
||||
</Link>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<button class="btn btn-sm" onClick={onLogout}>
|
||||
Logout
|
||||
</button>
|
||||
</Show>
|
||||
|
||||
<Link class="btn btn-sm" href="/jwtTest">
|
||||
JwtTest
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<Show
|
||||
when={data?.auth?.username}
|
||||
fallback={<div class="text-xl">Login first</div>}
|
||||
>
|
||||
<Show when={data?.todos}>
|
||||
<div class="text-xl">Directus data:</div>
|
||||
<div class="mx-auto flex flex-col gap-3">
|
||||
<For each={data?.todos}>
|
||||
{(item: Todo) => (
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card-body">{item.title}</div>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</Show>
|
||||
</Show>
|
||||
</main>
|
||||
);
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
import { useRouteData } from "solid-app-router";
|
||||
import { createResource } from "solid-js";
|
||||
import { authData } from "~/auth/client";
|
||||
import { serveJwtData } from "~/server/jwtData";
|
||||
import { composeRouteData } from "~/util";
|
||||
import { useRefreshy } from "~/util/refreshy";
|
||||
|
||||
export const routeData = () => {
|
||||
const [refreshy, refreshData] = useRefreshy();
|
||||
|
||||
const gety = refreshy((name) => serveJwtData(name));
|
||||
|
||||
return composeRouteData({
|
||||
// TODO: Fix the redirect, it only works with ssr
|
||||
auth: authData({ redirectNoAuth: "/login" }),
|
||||
john: () => createResource(() => gety("john " + refreshData())),
|
||||
klaus: () => createResource(() => gety("klaus " + refreshData())),
|
||||
marry: () => createResource(() => gety("marry " + refreshData())),
|
||||
})();
|
||||
};
|
||||
|
||||
export default () => {
|
||||
const [data, refetch, repos] = useRouteData() as any;
|
||||
|
||||
return (
|
||||
<div class="mx-auto w-96 my-11">
|
||||
<button
|
||||
type="button"
|
||||
class="btn mb-6"
|
||||
onClick={() => {
|
||||
repos.john[1].refetch();
|
||||
repos.klaus[1].refetch();
|
||||
repos.marry[1].refetch();
|
||||
}}
|
||||
>
|
||||
Refetch
|
||||
</button>
|
||||
<div>{data.john}</div>
|
||||
<div>{data.klaus}</div>
|
||||
<div>{data.marry}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -0,0 +1,43 @@
|
||||
import { login } from "~/auth/server";
|
||||
import server from "solid-start/server";
|
||||
import { composeRouteData, useServer } from "~/util";
|
||||
import { authData } from "~/auth/client";
|
||||
import { useNavigate } from "solid-app-router";
|
||||
|
||||
export const routeData = composeRouteData({
|
||||
auth: authData({ redirectWithAuth: "/" }),
|
||||
});
|
||||
|
||||
export default function Login() {
|
||||
const navigate = useNavigate();
|
||||
// TODO: Use createAction when it works with redirects
|
||||
const onClick = useServer(
|
||||
server(() => login("admin@example.com", "d1r3ctu5")),
|
||||
() => navigate("/")
|
||||
);
|
||||
|
||||
return (
|
||||
<main class="text-center mx-auto text-gray-700 p-4">
|
||||
<h1 class="max-6-xs text-6xl text-sky-700 font-thin uppercase my-16">
|
||||
Login Form
|
||||
</h1>
|
||||
|
||||
<div>Here shall be a login form</div>
|
||||
|
||||
<button class="btn" onClick={onClick}>
|
||||
Login and navigate
|
||||
</button>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
/*
|
||||
const [item] = createUrqlResource(function() {
|
||||
console.log('requested')
|
||||
|
||||
return gql`
|
||||
query HelloWorld {
|
||||
hello
|
||||
}`
|
||||
})
|
||||
*/
|
@ -0,0 +1,25 @@
|
||||
import { useRouteData } from "solid-app-router";
|
||||
import { createResource } from "solid-js";
|
||||
import server from "solid-start/server";
|
||||
import { login } from "~/auth/server";
|
||||
|
||||
// This doesn't work yet!
|
||||
// Open up: http://localhost:3000/loginWithGet?username=admin@example.com&password=d1r3ctu5
|
||||
|
||||
export function routeData() {
|
||||
return createResource(
|
||||
server(() => {
|
||||
const url = new URL(server.request.url);
|
||||
const username = url.searchParams.get("username");
|
||||
const password = url.searchParams.get("password");
|
||||
|
||||
return login(username, password, "/");
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
export default () => {
|
||||
const [data] = useRouteData();
|
||||
data();
|
||||
return <div></div>;
|
||||
};
|
@ -0,0 +1,16 @@
|
||||
import { useRouteData } from "solid-app-router";
|
||||
import { createResource } from "solid-js";
|
||||
import server from "solid-start/server";
|
||||
import { logout } from "~/auth/server";
|
||||
|
||||
export function routeData() {
|
||||
return createResource(server(() => logout(server.request)));
|
||||
}
|
||||
|
||||
// http://localhost:3000/login?username=admin@example.com&password=d1r3ctu5
|
||||
|
||||
export default () => {
|
||||
const [data] = useRouteData();
|
||||
data();
|
||||
return <div></div>;
|
||||
};
|
@ -0,0 +1,79 @@
|
||||
import { Session } from "solid-start/session/sessions";
|
||||
import {
|
||||
createClient,
|
||||
fetchExchange,
|
||||
errorExchange,
|
||||
makeOperation,
|
||||
} from "urql";
|
||||
import { authExchange } from "@urql/exchange-auth";
|
||||
import { GraphQLError } from "graphql";
|
||||
|
||||
export const createDirectusSystemClient = () => {
|
||||
const client = createClient({
|
||||
url: "http://localhost:8055/graphql/system",
|
||||
});
|
||||
|
||||
return client;
|
||||
};
|
||||
|
||||
export const createDirectusClient = (session: Session, onAuthError?) => {
|
||||
const client = createClient({
|
||||
url: "http://localhost:8055/graphql",
|
||||
exchanges: [
|
||||
authExchange({
|
||||
getAuth: async ({ authState }) => {
|
||||
if (!authState) {
|
||||
const token = session.get("accessToken");
|
||||
const refreshToken = session.get("refreshToken");
|
||||
if (token && refreshToken) {
|
||||
return { token, refreshToken };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
addAuthToOperation: ({ authState, operation }: any) => {
|
||||
if (!authState || !authState.token) {
|
||||
return operation;
|
||||
}
|
||||
|
||||
const fetchOptions =
|
||||
typeof operation.context.fetchOptions === "function"
|
||||
? operation.context.fetchOptions()
|
||||
: operation.context.fetchOptions || {};
|
||||
|
||||
return makeOperation(operation.kind, operation, {
|
||||
...operation.context,
|
||||
fetchOptions: {
|
||||
...fetchOptions,
|
||||
headers: {
|
||||
...fetchOptions.headers,
|
||||
Authorization: `Bearer ${authState.token}`,
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
}),
|
||||
errorExchange({
|
||||
onError: async (error, o) => {
|
||||
console.log("code", error.graphQLErrors[0].extensions.code);
|
||||
console.log(error.graphQLErrors[0].extensions.graphqlErrors);
|
||||
const isAuthError = error.graphQLErrors.some(
|
||||
(e: GraphQLError) =>
|
||||
["TOKEN_EXPIRED", "FORBIDDEN"].indexOf(
|
||||
e.extensions?.code as string
|
||||
) >= 0
|
||||
);
|
||||
console.log("isAuthError", isAuthError);
|
||||
if (onAuthError && isAuthError) {
|
||||
onAuthError();
|
||||
}
|
||||
},
|
||||
}),
|
||||
fetchExchange,
|
||||
],
|
||||
});
|
||||
|
||||
return client;
|
||||
};
|
@ -0,0 +1,9 @@
|
||||
import server from "solid-start/server";
|
||||
|
||||
export const getJwtData = async function (name: string) {
|
||||
return `hello ${name}, random: ${Math.round(Math.random() * 100)}`;
|
||||
};
|
||||
|
||||
export const serveJwtData = server((name) => {
|
||||
return getJwtData(name);
|
||||
});
|
@ -0,0 +1,54 @@
|
||||
import { gql, OperationResult } from "urql";
|
||||
import { getSession, isLoggedIn, logout } from "../auth/server";
|
||||
import server from "solid-start/server";
|
||||
import { awaitJson, createRC, RC } from "../util";
|
||||
import { createDirectusClient } from "./directus";
|
||||
|
||||
export const getTodos = async function (rc: RC) {
|
||||
const session = await getSession(rc);
|
||||
|
||||
if (!isLoggedIn(session)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
let result: OperationResult;
|
||||
let authError = false;
|
||||
|
||||
try {
|
||||
const client = createDirectusClient(session, () => {
|
||||
authError = true;
|
||||
});
|
||||
result = await client
|
||||
.query(
|
||||
gql`
|
||||
query {
|
||||
Todos {
|
||||
title
|
||||
}
|
||||
}
|
||||
`
|
||||
)
|
||||
.toPromise();
|
||||
|
||||
if (result.error) {
|
||||
console.dir(result.error);
|
||||
throw new Error(result.error.response);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
return [];
|
||||
}
|
||||
|
||||
if (authError) {
|
||||
console.log("logout");
|
||||
return await logout(rc, "/login");
|
||||
}
|
||||
|
||||
if (!result.data) return [];
|
||||
|
||||
return result.data.Todos;
|
||||
};
|
||||
|
||||
export const serveTodos = server(async () =>
|
||||
getTodos(createRC(server.request))
|
||||
);
|
@ -0,0 +1,111 @@
|
||||
import { useNavigate } from "solid-app-router";
|
||||
import {
|
||||
createEffect,
|
||||
resetErrorBoundaries,
|
||||
ResourceReturn,
|
||||
startTransition,
|
||||
} from "solid-js";
|
||||
import { createStore, produce } from "solid-js/store";
|
||||
import { isRedirectResponse, LocationHeader } from "solid-start/server";
|
||||
|
||||
// Should handle redirects, but it broke "quickly"
|
||||
export const useServer = function (serverFunction, callback?) {
|
||||
const navigate = useNavigate();
|
||||
|
||||
return async () => {
|
||||
let res;
|
||||
|
||||
try {
|
||||
res = await serverFunction();
|
||||
} catch (err) {
|
||||
if (err instanceof Response) {
|
||||
res = err;
|
||||
}
|
||||
}
|
||||
|
||||
if (isRedirectResponse(res)) {
|
||||
startTransition(() => {
|
||||
navigate(res.headers.get(LocationHeader));
|
||||
resetErrorBoundaries();
|
||||
});
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return callback ? callback(res) : res;
|
||||
};
|
||||
};
|
||||
|
||||
export const awaitJson = <T extends (...args) => any>(fn: T) => {
|
||||
return async (...args: Parameters<T>) => {
|
||||
const result = await fn(...args);
|
||||
|
||||
if (!result) {
|
||||
return result;
|
||||
}
|
||||
|
||||
if (!(result instanceof Response)) {
|
||||
return result;
|
||||
}
|
||||
|
||||
const contentType = result.headers.get("content-type");
|
||||
if (!contentType) {
|
||||
return result;
|
||||
}
|
||||
|
||||
if (contentType.indexOf("application/json") === -1) {
|
||||
return result;
|
||||
}
|
||||
|
||||
const jsonContent = await result.json();
|
||||
|
||||
return jsonContent;
|
||||
};
|
||||
};
|
||||
|
||||
export const composeRouteData =
|
||||
(functionMap: Record<any, (state) => ResourceReturn<any>>) => () => {
|
||||
const [state, setState] = createStore({} as any);
|
||||
|
||||
const repos = {};
|
||||
|
||||
for (const dataKey of Object.keys(functionMap)) {
|
||||
repos[dataKey] = functionMap[dataKey](state);
|
||||
|
||||
setState(dataKey, repos[dataKey][0]());
|
||||
|
||||
createEffect(function () {
|
||||
const newValue = repos[dataKey][0]();
|
||||
setState(
|
||||
produce(function (s) {
|
||||
s[dataKey] = newValue;
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
const refetch = function () {
|
||||
for (const dataKey of Object.keys(functionMap)) {
|
||||
repos[dataKey][1].refetch();
|
||||
}
|
||||
};
|
||||
|
||||
return [state, refetch, repos];
|
||||
};
|
||||
|
||||
export type RC = {
|
||||
request: Request;
|
||||
context: Record<any, any>;
|
||||
responseHeaders?: Headers;
|
||||
url: URL;
|
||||
};
|
||||
export const createRC = function (request: Request, responseHeaders?: Headers) {
|
||||
const rc: RC = {
|
||||
request,
|
||||
responseHeaders,
|
||||
url: new URL(request.url),
|
||||
context: {},
|
||||
};
|
||||
|
||||
return rc;
|
||||
};
|
@ -0,0 +1,104 @@
|
||||
import {
|
||||
createContext,
|
||||
createSignal,
|
||||
FlowComponent,
|
||||
useContext,
|
||||
} from "solid-js";
|
||||
import { isServer } from "solid-js/web";
|
||||
|
||||
const refresh_ = async function (
|
||||
ctx: ReturnType<typeof createRefreshyContext>
|
||||
) {
|
||||
const time = new Date().getTime();
|
||||
|
||||
if (ctx.lastRefresh + ctx.ttl * 1000 > time) {
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.lastRefresh = time;
|
||||
|
||||
ctx.refreshPromise = (async () => {
|
||||
const result = await ctx.refresh(ctx);
|
||||
ctx.refreshResult[1](result);
|
||||
ctx.refreshPromise = null;
|
||||
})();
|
||||
|
||||
await ctx.refreshPromise;
|
||||
};
|
||||
|
||||
export type RefreshyContext = ReturnType<typeof createRefreshyContext>;
|
||||
|
||||
const createRefreshyContext = function (
|
||||
refresh: Function,
|
||||
ttl = 60,
|
||||
refreshSoon = false
|
||||
) {
|
||||
const refreshResult = createSignal();
|
||||
const refreshy = <T extends (...args) => Promise<any>>(fetcher: T) => {
|
||||
if (isServer) {
|
||||
return fetcher;
|
||||
}
|
||||
|
||||
return async (...args: Parameters<T>) => {
|
||||
if (ctx.refreshPromise) {
|
||||
await ctx.refreshPromise;
|
||||
} else {
|
||||
await refresh_(ctx);
|
||||
}
|
||||
|
||||
return await fetcher(...args);
|
||||
};
|
||||
};
|
||||
|
||||
const ctx = {
|
||||
ttl,
|
||||
refresh,
|
||||
refreshResult,
|
||||
refreshy,
|
||||
lastRefresh: isServer || refreshSoon ? 0 : new Date().getTime(),
|
||||
refreshPromise: null as Promise<any> | null,
|
||||
};
|
||||
|
||||
return ctx;
|
||||
};
|
||||
const FetcherContext = createContext(
|
||||
{} as ReturnType<typeof createRefreshyContext>
|
||||
);
|
||||
export type RefreshFunction = (ctx: RefreshyContext) => any;
|
||||
export const RefreshyProvider: FlowComponent<{
|
||||
ttl?: number;
|
||||
refresh: RefreshFunction;
|
||||
refreshSoon?: boolean;
|
||||
}> = (props) => {
|
||||
const ctx = createRefreshyContext(
|
||||
props.refresh,
|
||||
props.ttl,
|
||||
props.refreshSoon
|
||||
);
|
||||
|
||||
return (
|
||||
<FetcherContext.Provider value={ctx}>
|
||||
{props.children}
|
||||
</FetcherContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const createRefreshMiddleware = (
|
||||
refresh: (request: Request, responseHeaders: Headers) => Promise<any>
|
||||
) => {
|
||||
return function ({ forward }) {
|
||||
return async (ctx) => {
|
||||
await refresh(ctx.request, ctx.responseHeaders);
|
||||
|
||||
return await forward(ctx);
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
export const useRefreshy = function () {
|
||||
const ctx = useContext(FetcherContext);
|
||||
const refreshy = ctx.refreshy;
|
||||
const signal = ctx.refreshResult[0];
|
||||
|
||||
return [refreshy, signal] as [typeof refreshy, typeof signal];
|
||||
};
|
@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
content: ["./src/**/*.{html,js,jsx,ts,tsx}"],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [require("daisyui")],
|
||||
};
|
@ -0,0 +1,16 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"esModuleInterop": true,
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "node",
|
||||
"jsxImportSource": "solid-js",
|
||||
"jsx": "preserve",
|
||||
"types": ["vite/client"],
|
||||
"baseUrl": "./",
|
||||
"paths": {
|
||||
"~/*": ["./src/*"]
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
import { defineConfig } from "vite";
|
||||
import solid from "solid-start";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [solid()],
|
||||
});
|
Loading…
Reference in New Issue