mirror of
https://github.com/kou029w/quot.git
synced 2025-01-18 08:05:08 +00:00
ログインできるようになった
This commit is contained in:
parent
661b13e5ff
commit
2e05578e9a
17 changed files with 1401 additions and 77 deletions
49
app/controllers/config.ts
Normal file
49
app/controllers/config.ts
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
import type { HttpOptions } from "openid-client";
|
||||||
|
import crypto, { type KeyObject } from "node:crypto";
|
||||||
|
|
||||||
|
interface Config {
|
||||||
|
port: number;
|
||||||
|
apiUrl: URL;
|
||||||
|
apiEndpoint: string;
|
||||||
|
viewsDir: string;
|
||||||
|
rootUrl: URL;
|
||||||
|
openid: {
|
||||||
|
issuer: string;
|
||||||
|
client: {
|
||||||
|
client_id: string;
|
||||||
|
client_secret: string;
|
||||||
|
};
|
||||||
|
request: HttpOptions;
|
||||||
|
};
|
||||||
|
key: KeyObject;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type { Config };
|
||||||
|
|
||||||
|
function defaultConfig(): Config {
|
||||||
|
const port = Number(process.env.PORT ?? "8080");
|
||||||
|
const rootUrl = new URL(
|
||||||
|
process.env.QUOT_ROOT_URL ?? `http://localhost:${port}/`
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
port,
|
||||||
|
rootUrl,
|
||||||
|
apiUrl: new URL(process.env.QUOT_API_URL ?? "http://127.0.0.1:3000"),
|
||||||
|
apiEndpoint: process.env.QUOT_API_ENDPOINT ?? "/api",
|
||||||
|
viewsDir: "views",
|
||||||
|
openid: {
|
||||||
|
issuer: process.env.QUOT_OPENID_ISSUER ?? "",
|
||||||
|
client: {
|
||||||
|
client_id: process.env.QUOT_OPENID_CLIENT_ID ?? "",
|
||||||
|
client_secret: process.env.QUOT_OPENID_CLIENT_SECRET ?? "",
|
||||||
|
},
|
||||||
|
request: { timeout: 5_000 },
|
||||||
|
},
|
||||||
|
key: crypto.createPrivateKey({
|
||||||
|
key: JSON.parse(process.env.QUOT_JWK ?? "{}"),
|
||||||
|
format: "jwk",
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default defaultConfig;
|
21
app/controllers/index.ts
Normal file
21
app/controllers/index.ts
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
import fastify, { type FastifyInstance } from "fastify";
|
||||||
|
import defaultConfig, { type Config } from "./config";
|
||||||
|
import routes from "./routes/index";
|
||||||
|
|
||||||
|
declare module "fastify" {
|
||||||
|
interface FastifyInstance {
|
||||||
|
config: Config;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function App(config: Config = defaultConfig()): FastifyInstance {
|
||||||
|
const app = fastify({ logger: true });
|
||||||
|
app.decorate("config", config).register(routes);
|
||||||
|
return app;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function start(app: FastifyInstance): Promise<string> {
|
||||||
|
await app.ready();
|
||||||
|
const address = await app.listen({ host: "::", port: app.config.port });
|
||||||
|
return address;
|
||||||
|
}
|
11
app/controllers/routes/api.ts
Normal file
11
app/controllers/routes/api.ts
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
import type { FastifyInstance } from "fastify";
|
||||||
|
import httpProxy from "@fastify/http-proxy";
|
||||||
|
|
||||||
|
async function api(fastify: FastifyInstance) {
|
||||||
|
await fastify.register(httpProxy, {
|
||||||
|
prefix: "/api",
|
||||||
|
upstream: fastify.config.apiUrl.href,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export default api;
|
14
app/controllers/routes/index.ts
Normal file
14
app/controllers/routes/index.ts
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
import type { FastifyInstance } from "fastify";
|
||||||
|
import api from "./api";
|
||||||
|
import login from "./login";
|
||||||
|
import views from "./views";
|
||||||
|
|
||||||
|
async function index(fastify: FastifyInstance) {
|
||||||
|
await Promise.all([
|
||||||
|
fastify.register(api),
|
||||||
|
fastify.register(login),
|
||||||
|
fastify.register(views),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default index;
|
32
app/controllers/routes/login.ts
Normal file
32
app/controllers/routes/login.ts
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
import { custom, Issuer } from "openid-client";
|
||||||
|
import { SignJWT } from "jose";
|
||||||
|
import type { FastifyInstance } from "fastify";
|
||||||
|
|
||||||
|
async function login(fastify: FastifyInstance) {
|
||||||
|
custom.setHttpOptionsDefaults(fastify.config.openid.request);
|
||||||
|
const issuer = await Issuer.discover(fastify.config.openid.issuer);
|
||||||
|
const client = new issuer.Client(fastify.config.openid.client);
|
||||||
|
|
||||||
|
fastify.get("/login", async (request, reply) => {
|
||||||
|
const params = client.callbackParams(request.raw);
|
||||||
|
const redirect_uri = new URL("login", fastify.config.rootUrl).href;
|
||||||
|
if (!("code" in params)) {
|
||||||
|
const authorizationUrl = client.authorizationUrl({ redirect_uri });
|
||||||
|
return reply.redirect(authorizationUrl);
|
||||||
|
}
|
||||||
|
const tokens = await client.callback(redirect_uri, params);
|
||||||
|
const userUrl = new URL(issuer.metadata.issuer);
|
||||||
|
const userinfo = await client.userinfo(tokens);
|
||||||
|
userUrl.username = userinfo.sub;
|
||||||
|
const jwt = await new SignJWT({ role: "writer" })
|
||||||
|
.setProtectedHeader({ typ: "JWT", alg: "RS256" })
|
||||||
|
.setExpirationTime("30days")
|
||||||
|
.setSubject(userUrl.href)
|
||||||
|
.sign(fastify.config.key);
|
||||||
|
const url = new URL(fastify.config.rootUrl);
|
||||||
|
url.hash = new URLSearchParams({ jwt }).toString();
|
||||||
|
return reply.redirect(url.href);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export default login;
|
33
app/controllers/routes/views.ts
Normal file
33
app/controllers/routes/views.ts
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
import type { FastifyInstance } from "fastify";
|
||||||
|
import httpProxy from "@fastify/http-proxy";
|
||||||
|
import esbuild from "esbuild";
|
||||||
|
|
||||||
|
async function views(fastify: FastifyInstance) {
|
||||||
|
const viewsDir = fastify.config.viewsDir;
|
||||||
|
const entryPoint = `${viewsDir}/index.ts`;
|
||||||
|
const { host, port } = await esbuild.serve(
|
||||||
|
{
|
||||||
|
host: "127.0.0.1",
|
||||||
|
servedir: viewsDir,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
bundle: true,
|
||||||
|
minify: true,
|
||||||
|
entryPoints: [entryPoint],
|
||||||
|
define: {
|
||||||
|
"import.meta.env.QUOT_API_ENDPOINT": JSON.stringify(
|
||||||
|
fastify.config.apiEndpoint
|
||||||
|
),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
await fastify.register(httpProxy, {
|
||||||
|
upstream: `http://${host}:${port}`,
|
||||||
|
async preHandler(req) {
|
||||||
|
if (!/\.(?:html|css|js)$/.test(req.url)) req.raw.url = "/";
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export default views;
|
60
app/main.ts
60
app/main.ts
|
@ -1,59 +1,3 @@
|
||||||
import type { AddressInfo } from "node:net";
|
import { App, start } from "./controllers/index";
|
||||||
import http from "node:http";
|
|
||||||
import fs from "node:fs";
|
|
||||||
import esbuild from "esbuild";
|
|
||||||
|
|
||||||
async function main() {
|
start(App());
|
||||||
const port = Number(process.env.PORT ?? "8080");
|
|
||||||
const apiUrl = process.env.QUOT_API_URL || "http://127.0.0.1:3000";
|
|
||||||
const viewsDir = `${__dirname}/views`;
|
|
||||||
const htmlPath = `${viewsDir}/index.html`;
|
|
||||||
const scriptPath = `${viewsDir}/index.ts`;
|
|
||||||
|
|
||||||
const result = await esbuild.serve(
|
|
||||||
{
|
|
||||||
host: "127.0.0.1",
|
|
||||||
servedir: viewsDir,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
bundle: true,
|
|
||||||
minify: true,
|
|
||||||
entryPoints: [scriptPath],
|
|
||||||
define: {
|
|
||||||
"import.meta.env.QUOT_API_URL": JSON.stringify(
|
|
||||||
process.env.QUOT_API_URL ?? ""
|
|
||||||
),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const handler: http.RequestListener = (req, res) => {
|
|
||||||
const api = req.url?.startsWith("/api/") && new URL(apiUrl);
|
|
||||||
const options = {
|
|
||||||
hostname: /**/ api ? api.hostname /* */ : result.host,
|
|
||||||
port: /* */ api ? api.port /* */ : result.port,
|
|
||||||
path: /* */ api ? req.url?.slice("/api".length) /**/ : req.url,
|
|
||||||
method: req.method,
|
|
||||||
headers: req.headers,
|
|
||||||
};
|
|
||||||
|
|
||||||
const proxyReq = http.request(options, (proxyRes) => {
|
|
||||||
if (api || proxyRes.statusCode === 200) {
|
|
||||||
res.writeHead(proxyRes.statusCode ?? 500, proxyRes.headers);
|
|
||||||
proxyRes.pipe(res);
|
|
||||||
} else {
|
|
||||||
res.writeHead(200, { "Content-Type": "text/html" });
|
|
||||||
fs.createReadStream(htmlPath).pipe(res);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
req.pipe(proxyReq);
|
|
||||||
};
|
|
||||||
|
|
||||||
const server = http.createServer(handler).listen(port);
|
|
||||||
const address = server.address() as AddressInfo;
|
|
||||||
|
|
||||||
console.log(`http://0.0.0.0:${address.port}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
main();
|
|
||||||
|
|
1045
app/package-lock.json
generated
1045
app/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -4,7 +4,7 @@
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "tsc --noEmit",
|
"test": "tsc --noEmit",
|
||||||
"start": "node -r esbuild-register main.ts"
|
"start": "TS_NODE_DEV=true node -r esbuild-register main.ts"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^18.7.0"
|
"node": "^18.7.0"
|
||||||
|
@ -12,10 +12,14 @@
|
||||||
"packageManager": "npm@8.19.0",
|
"packageManager": "npm@8.19.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@exampledev/new.css": "^1.1.3",
|
"@exampledev/new.css": "^1.1.3",
|
||||||
|
"@fastify/http-proxy": "^8.2.2",
|
||||||
"@lexical/plain-text": "^0.3.11",
|
"@lexical/plain-text": "^0.3.11",
|
||||||
"esbuild": "^0.15.5",
|
"esbuild": "^0.15.5",
|
||||||
"esbuild-register": "^3.3.3",
|
"esbuild-register": "^3.3.3",
|
||||||
|
"fastify": "^4.5.3",
|
||||||
|
"jose": "^4.9.2",
|
||||||
"lexical": "^0.3.11",
|
"lexical": "^0.3.11",
|
||||||
|
"openid-client": "^5.1.9",
|
||||||
"solid-js": "^1.4.8"
|
"solid-js": "^1.4.8"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
|
@ -3,11 +3,32 @@ import { createSignal } from "solid-js";
|
||||||
import Index from "./pages/index";
|
import Index from "./pages/index";
|
||||||
import Page from "./pages/page";
|
import Page from "./pages/page";
|
||||||
import random from "./helpers/random";
|
import random from "./helpers/random";
|
||||||
|
import { decodeJwt } from "jose";
|
||||||
|
|
||||||
const routes = {
|
const routes = {
|
||||||
"/": Index,
|
"/": Index,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
async function updateUser(jwt: string): Promise<boolean> {
|
||||||
|
const decoded = decodeJwt(jwt);
|
||||||
|
const id = decoded.sub ?? "";
|
||||||
|
if (!id) return false;
|
||||||
|
const res = await fetch(
|
||||||
|
`${import.meta.env.QUOT_API_ENDPOINT}/users?id=eq.${encodeURIComponent(
|
||||||
|
id
|
||||||
|
)}`,
|
||||||
|
{
|
||||||
|
method: "PUT",
|
||||||
|
headers: {
|
||||||
|
authorization: `Bearer ${jwt}`,
|
||||||
|
"content-type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ id }),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return res.ok;
|
||||||
|
}
|
||||||
|
|
||||||
export default () => {
|
export default () => {
|
||||||
const [pathname, setPathname] = createSignal(
|
const [pathname, setPathname] = createSignal(
|
||||||
document.location.pathname as keyof typeof routes
|
document.location.pathname as keyof typeof routes
|
||||||
|
@ -29,6 +50,18 @@ export default () => {
|
||||||
setPathname(document.location.pathname as keyof typeof routes);
|
setPathname(document.location.pathname as keyof typeof routes);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (window.location.hash) {
|
||||||
|
const params = new URLSearchParams(window.location.hash.slice(1));
|
||||||
|
const jwt = params.get("jwt");
|
||||||
|
if (jwt) {
|
||||||
|
window.history.replaceState({}, "", ".");
|
||||||
|
window.localStorage.setItem("jwt", jwt);
|
||||||
|
updateUser(jwt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const authenticated = Boolean(window.localStorage.getItem("jwt"));
|
||||||
|
|
||||||
return () => (
|
return () => (
|
||||||
<>
|
<>
|
||||||
<header>
|
<header>
|
||||||
|
@ -36,7 +69,7 @@ export default () => {
|
||||||
<a href="/">Quot</a>
|
<a href="/">Quot</a>
|
||||||
</h1>
|
</h1>
|
||||||
<nav>
|
<nav>
|
||||||
<a href="/new">📄</a>
|
<a href={authenticated ? "/new" : "/login"}>📄</a>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
{routes[pathname()] ?? (
|
{routes[pathname()] ?? (
|
||||||
|
|
2
app/views/env.d.ts
vendored
2
app/views/env.d.ts
vendored
|
@ -1,5 +1,5 @@
|
||||||
interface ImportMeta {
|
interface ImportMeta {
|
||||||
env: {
|
env: {
|
||||||
QUOT_API_URL: string;
|
QUOT_API_ENDPOINT: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,8 +4,10 @@ import Cards from "../components/cards";
|
||||||
import Card from "../components/card";
|
import Card from "../components/card";
|
||||||
|
|
||||||
async function fetchPages(): Promise<Pages.Response> {
|
async function fetchPages(): Promise<Pages.Response> {
|
||||||
|
const jwt = window.localStorage.getItem("jwt");
|
||||||
const res = await fetch(
|
const res = await fetch(
|
||||||
`${import.meta.env.QUOT_API_URL}/api/pages?order=updated.desc`
|
`${import.meta.env.QUOT_API_ENDPOINT}/pages?order=updated.desc`,
|
||||||
|
{ headers: jwt ? { authorization: `Bearer ${jwt}` } : {} }
|
||||||
);
|
);
|
||||||
const data = (await res.json()) as Pages.Response;
|
const data = (await res.json()) as Pages.Response;
|
||||||
return data;
|
return data;
|
||||||
|
|
|
@ -10,11 +10,16 @@ async function updatePage(
|
||||||
id: number,
|
id: number,
|
||||||
content: Pages.RequestContentPage
|
content: Pages.RequestContentPage
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
|
const jwt = window.localStorage.getItem("jwt");
|
||||||
|
if (!jwt) return false;
|
||||||
const res = await fetch(
|
const res = await fetch(
|
||||||
`${import.meta.env.QUOT_API_URL}/api/pages?id=eq.${id}`,
|
`${import.meta.env.QUOT_API_ENDPOINT}/pages?id=eq.${id}`,
|
||||||
{
|
{
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
headers: { "content-type": "application/json" },
|
headers: {
|
||||||
|
authorization: `Bearer ${jwt}`,
|
||||||
|
"content-type": "application/json",
|
||||||
|
},
|
||||||
body: JSON.stringify(content),
|
body: JSON.stringify(content),
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
@ -22,16 +27,23 @@ async function updatePage(
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deletePage(id: number): Promise<boolean> {
|
async function deletePage(id: number): Promise<boolean> {
|
||||||
|
const jwt = window.localStorage.getItem("jwt");
|
||||||
|
if (!jwt) return false;
|
||||||
const res = await fetch(
|
const res = await fetch(
|
||||||
`${import.meta.env.QUOT_API_URL}/api/pages?id=eq.${id}`,
|
`${import.meta.env.QUOT_API_ENDPOINT}/pages?id=eq.${id}`,
|
||||||
{ method: "DELETE" }
|
{
|
||||||
|
method: "DELETE",
|
||||||
|
headers: { authorization: `Bearer ${jwt}` },
|
||||||
|
}
|
||||||
);
|
);
|
||||||
return res.ok;
|
return res.ok;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchPage(id: number): Promise<Pages.ResponsePage> {
|
async function fetchPage(id: number): Promise<Pages.ResponsePage> {
|
||||||
|
const jwt = window.localStorage.getItem("jwt");
|
||||||
const res = await fetch(
|
const res = await fetch(
|
||||||
`${import.meta.env.QUOT_API_URL}/api/pages?id=eq.${id}`
|
`${import.meta.env.QUOT_API_ENDPOINT}/pages?id=eq.${id}`,
|
||||||
|
{ headers: jwt ? { authorization: `Bearer ${jwt}` } : {} }
|
||||||
);
|
);
|
||||||
const data = (await res.json()) as Pages.Response;
|
const data = (await res.json()) as Pages.Response;
|
||||||
return data[0]!;
|
return data[0]!;
|
||||||
|
|
20
compose.yml
20
compose.yml
|
@ -5,10 +5,30 @@ services:
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ports: ["8080:8080"]
|
ports: ["8080:8080"]
|
||||||
environment:
|
environment:
|
||||||
|
# https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata
|
||||||
|
QUOT_OPENID_ISSUER: ${QUOT_OPENID_ISSUER:?}
|
||||||
|
# https://www.rfc-editor.org/rfc/rfc6749#section-2.3.1
|
||||||
|
QUOT_OPENID_CLIENT_ID: ${QUOT_OPENID_CLIENT_ID:?}
|
||||||
|
QUOT_OPENID_CLIENT_SECRET: ${QUOT_OPENID_CLIENT_SECRET:?}
|
||||||
|
QUOT_JWK: ${QUOT_JWK:?} # https://mkjwk.org
|
||||||
DATABASE_URL: postgresql://postgres:${POSTGRES_PASSWORD}@/postgres?host=/var/run/postgresql
|
DATABASE_URL: postgresql://postgres:${POSTGRES_PASSWORD}@/postgres?host=/var/run/postgresql
|
||||||
volumes:
|
volumes:
|
||||||
- postgres_socket:/var/run/postgresql
|
- postgres_socket:/var/run/postgresql
|
||||||
depends_on: [db]
|
depends_on: [db]
|
||||||
|
api:
|
||||||
|
profiles: [dev]
|
||||||
|
image: kou029w/quot
|
||||||
|
build: "."
|
||||||
|
restart: unless-stopped
|
||||||
|
ports: ["3000:3000"]
|
||||||
|
environment:
|
||||||
|
QUOT_JWK: ${QUOT_JWK:?} # https://mkjwk.org
|
||||||
|
DATABASE_URL: postgresql://postgres:${POSTGRES_PASSWORD}@/postgres?host=/var/run/postgresql
|
||||||
|
volumes:
|
||||||
|
- postgres_socket:/var/run/postgresql
|
||||||
|
depends_on:
|
||||||
|
db:
|
||||||
|
condition: service_healthy
|
||||||
dbmate:
|
dbmate:
|
||||||
profiles: [dev]
|
profiles: [dev]
|
||||||
image: amacneil/dbmate:1.15@sha256:8fb25de3fce073e39eb3f9411af0410d0e26cc6d120544a7510b964e218abc27
|
image: amacneil/dbmate:1.15@sha256:8fb25de3fce073e39eb3f9411af0410d0e26cc6d120544a7510b964e218abc27
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
-- migrate:up
|
-- migrate:up
|
||||||
CREATE ROLE anon;
|
CREATE ROLE guest;
|
||||||
ALTER ROLE anon SET statement_timeout = '1s';
|
ALTER ROLE guest SET statement_timeout = '1s';
|
||||||
|
CREATE ROLE writer;
|
||||||
|
ALTER ROLE writer SET statement_timeout = '1s';
|
||||||
|
|
||||||
CREATE FUNCTION update_timestamp() RETURNS trigger LANGUAGE plpgsql AS $$
|
CREATE FUNCTION update_timestamp() RETURNS trigger LANGUAGE plpgsql AS $$
|
||||||
BEGIN
|
BEGIN
|
||||||
|
@ -9,20 +11,52 @@ CREATE FUNCTION update_timestamp() RETURNS trigger LANGUAGE plpgsql AS $$
|
||||||
END
|
END
|
||||||
$$;
|
$$;
|
||||||
|
|
||||||
CREATE TABLE pages (
|
CREATE TABLE users (
|
||||||
id SERIAL PRIMARY KEY,
|
id TEXT PRIMARY KEY DEFAULT current_setting('request.jwt.claims', true)::json ->> 'sub'::text,
|
||||||
title TEXT NOT NULL UNIQUE,
|
|
||||||
text TEXT NOT NULL,
|
|
||||||
created TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
created TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
updated TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
|
updated TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
);
|
);
|
||||||
|
|
||||||
GRANT ALL ON pages TO anon;
|
CREATE TRIGGER users_updated BEFORE UPDATE ON users FOR EACH ROW
|
||||||
|
EXECUTE PROCEDURE update_timestamp();
|
||||||
|
|
||||||
|
GRANT ALL (id) ON users TO writer;
|
||||||
|
GRANT SELECT, DELETE ON users TO writer;
|
||||||
|
ALTER TABLE users ENABLE ROW LEVEL SECURITY;
|
||||||
|
CREATE POLICY users_policy ON users USING (
|
||||||
|
id = current_setting('request.jwt.claims', true)::json ->> 'sub'::text
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE pages (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
user_id TEXT REFERENCES users ON DELETE CASCADE DEFAULT current_setting('request.jwt.claims', true)::json ->> 'sub'::text,
|
||||||
|
title TEXT NOT NULL UNIQUE,
|
||||||
|
text TEXT NOT NULL,
|
||||||
|
created TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
published TIMESTAMPTZ
|
||||||
|
);
|
||||||
|
|
||||||
CREATE TRIGGER pages_updated BEFORE UPDATE ON pages FOR EACH ROW
|
CREATE TRIGGER pages_updated BEFORE UPDATE ON pages FOR EACH ROW
|
||||||
EXECUTE PROCEDURE update_timestamp();
|
EXECUTE PROCEDURE update_timestamp();
|
||||||
|
|
||||||
|
ALTER TABLE pages ENABLE ROW LEVEL SECURITY;
|
||||||
|
|
||||||
|
GRANT SELECT ON pages TO guest;
|
||||||
|
CREATE POLICY pages_guest_read_policy ON pages TO guest USING (
|
||||||
|
published <= CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
GRANT SELECT, DELETE ON pages TO writer;
|
||||||
|
GRANT ALL (id, title, text, published) ON pages TO writer;
|
||||||
|
GRANT ALL ON SEQUENCE pages_id_seq TO writer;
|
||||||
|
CREATE POLICY pages_write_policy ON pages TO writer USING (
|
||||||
|
user_id = current_setting('request.jwt.claims', true)::json ->> 'sub'::text
|
||||||
|
);
|
||||||
|
|
||||||
-- migrate:down
|
-- migrate:down
|
||||||
|
DROP TABLE users;
|
||||||
DROP TABLE pages;
|
DROP TABLE pages;
|
||||||
DROP FUNCTION update_timestamp();
|
DROP FUNCTION update_timestamp();
|
||||||
DROP ROLE anon;
|
DROP ROLE guest;
|
||||||
|
DROP ROLE writer;
|
||||||
|
|
|
@ -33,10 +33,12 @@ SET default_table_access_method = heap;
|
||||||
|
|
||||||
CREATE TABLE public.pages (
|
CREATE TABLE public.pages (
|
||||||
id integer NOT NULL,
|
id integer NOT NULL,
|
||||||
|
user_id text DEFAULT ((current_setting('request.jwt.claims'::text, true))::json ->> 'sub'::text),
|
||||||
title text NOT NULL,
|
title text NOT NULL,
|
||||||
text text NOT NULL,
|
text text NOT NULL,
|
||||||
created timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
created timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||||
updated timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL
|
updated timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||||
|
published timestamp with time zone
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
|
@ -69,6 +71,17 @@ CREATE TABLE public.schema_migrations (
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: users; Type: TABLE; Schema: public; Owner: -
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE public.users (
|
||||||
|
id text DEFAULT ((current_setting('request.jwt.claims'::text, true))::json ->> 'sub'::text) NOT NULL,
|
||||||
|
created timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||||
|
updated timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
--
|
--
|
||||||
-- Name: pages id; Type: DEFAULT; Schema: public; Owner: -
|
-- Name: pages id; Type: DEFAULT; Schema: public; Owner: -
|
||||||
--
|
--
|
||||||
|
@ -100,6 +113,14 @@ ALTER TABLE ONLY public.schema_migrations
|
||||||
ADD CONSTRAINT schema_migrations_pkey PRIMARY KEY (version);
|
ADD CONSTRAINT schema_migrations_pkey PRIMARY KEY (version);
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: users users_pkey; Type: CONSTRAINT; Schema: public; Owner: -
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER TABLE ONLY public.users
|
||||||
|
ADD CONSTRAINT users_pkey PRIMARY KEY (id);
|
||||||
|
|
||||||
|
|
||||||
--
|
--
|
||||||
-- Name: pages pages_updated; Type: TRIGGER; Schema: public; Owner: -
|
-- Name: pages pages_updated; Type: TRIGGER; Schema: public; Owner: -
|
||||||
--
|
--
|
||||||
|
@ -107,6 +128,54 @@ ALTER TABLE ONLY public.schema_migrations
|
||||||
CREATE TRIGGER pages_updated BEFORE UPDATE ON public.pages FOR EACH ROW EXECUTE FUNCTION public.update_timestamp();
|
CREATE TRIGGER pages_updated BEFORE UPDATE ON public.pages FOR EACH ROW EXECUTE FUNCTION public.update_timestamp();
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: users users_updated; Type: TRIGGER; Schema: public; Owner: -
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TRIGGER users_updated BEFORE UPDATE ON public.users FOR EACH ROW EXECUTE FUNCTION public.update_timestamp();
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: pages pages_user_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: -
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER TABLE ONLY public.pages
|
||||||
|
ADD CONSTRAINT pages_user_id_fkey FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE CASCADE;
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: pages; Type: ROW SECURITY; Schema: public; Owner: -
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER TABLE public.pages ENABLE ROW LEVEL SECURITY;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: pages pages_guest_read_policy; Type: POLICY; Schema: public; Owner: -
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE POLICY pages_guest_read_policy ON public.pages TO guest USING ((published <= CURRENT_TIMESTAMP));
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: pages pages_write_policy; Type: POLICY; Schema: public; Owner: -
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE POLICY pages_write_policy ON public.pages TO writer USING ((user_id = ((current_setting('request.jwt.claims'::text, true))::json ->> 'sub'::text)));
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: users; Type: ROW SECURITY; Schema: public; Owner: -
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER TABLE public.users ENABLE ROW LEVEL SECURITY;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: users users_policy; Type: POLICY; Schema: public; Owner: -
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE POLICY users_policy ON public.users USING ((id = ((current_setting('request.jwt.claims'::text, true))::json ->> 'sub'::text)));
|
||||||
|
|
||||||
|
|
||||||
--
|
--
|
||||||
-- PostgreSQL database dump complete
|
-- PostgreSQL database dump complete
|
||||||
--
|
--
|
||||||
|
|
|
@ -3,5 +3,6 @@ set -e
|
||||||
dbmate --wait up
|
dbmate --wait up
|
||||||
export PGRST_DB_URI="${PGRST_DB_URI:-${DATABASE_URL}}"
|
export PGRST_DB_URI="${PGRST_DB_URI:-${DATABASE_URL}}"
|
||||||
export PGRST_DB_SCHEMA="${PGRST_DB_SCHEMA:-public}"
|
export PGRST_DB_SCHEMA="${PGRST_DB_SCHEMA:-public}"
|
||||||
export PGRST_DB_ANON_ROLE="${PGRST_DB_ANON_ROLE:-anon}"
|
export PGRST_DB_ANON_ROLE="${PGRST_DB_ANON_ROLE:-guest}"
|
||||||
|
export PGRST_JWT_SECRET="${QUOT_JWK}"
|
||||||
exec "$@"
|
exec "$@"
|
||||||
|
|
Loading…
Add table
Reference in a new issue