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 http from "node:http";
|
||||
import fs from "node:fs";
|
||||
import esbuild from "esbuild";
|
||||
import { App, start } from "./controllers/index";
|
||||
|
||||
async function main() {
|
||||
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();
|
||||
start(App());
|
||||
|
|
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,
|
||||
"scripts": {
|
||||
"test": "tsc --noEmit",
|
||||
"start": "node -r esbuild-register main.ts"
|
||||
"start": "TS_NODE_DEV=true node -r esbuild-register main.ts"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.7.0"
|
||||
|
@ -12,10 +12,14 @@
|
|||
"packageManager": "npm@8.19.0",
|
||||
"dependencies": {
|
||||
"@exampledev/new.css": "^1.1.3",
|
||||
"@fastify/http-proxy": "^8.2.2",
|
||||
"@lexical/plain-text": "^0.3.11",
|
||||
"esbuild": "^0.15.5",
|
||||
"esbuild-register": "^3.3.3",
|
||||
"fastify": "^4.5.3",
|
||||
"jose": "^4.9.2",
|
||||
"lexical": "^0.3.11",
|
||||
"openid-client": "^5.1.9",
|
||||
"solid-js": "^1.4.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
|
@ -3,11 +3,32 @@ import { createSignal } from "solid-js";
|
|||
import Index from "./pages/index";
|
||||
import Page from "./pages/page";
|
||||
import random from "./helpers/random";
|
||||
import { decodeJwt } from "jose";
|
||||
|
||||
const routes = {
|
||||
"/": 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 () => {
|
||||
const [pathname, setPathname] = createSignal(
|
||||
document.location.pathname as keyof typeof routes
|
||||
|
@ -29,6 +50,18 @@ export default () => {
|
|||
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 () => (
|
||||
<>
|
||||
<header>
|
||||
|
@ -36,7 +69,7 @@ export default () => {
|
|||
<a href="/">Quot</a>
|
||||
</h1>
|
||||
<nav>
|
||||
<a href="/new">📄</a>
|
||||
<a href={authenticated ? "/new" : "/login"}>📄</a>
|
||||
</nav>
|
||||
</header>
|
||||
{routes[pathname()] ?? (
|
||||
|
|
2
app/views/env.d.ts
vendored
2
app/views/env.d.ts
vendored
|
@ -1,5 +1,5 @@
|
|||
interface ImportMeta {
|
||||
env: {
|
||||
QUOT_API_URL: string;
|
||||
QUOT_API_ENDPOINT: string;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -4,8 +4,10 @@ import Cards from "../components/cards";
|
|||
import Card from "../components/card";
|
||||
|
||||
async function fetchPages(): Promise<Pages.Response> {
|
||||
const jwt = window.localStorage.getItem("jwt");
|
||||
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;
|
||||
return data;
|
||||
|
|
|
@ -10,11 +10,16 @@ async function updatePage(
|
|||
id: number,
|
||||
content: Pages.RequestContentPage
|
||||
): Promise<boolean> {
|
||||
const jwt = window.localStorage.getItem("jwt");
|
||||
if (!jwt) return false;
|
||||
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",
|
||||
headers: { "content-type": "application/json" },
|
||||
headers: {
|
||||
authorization: `Bearer ${jwt}`,
|
||||
"content-type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(content),
|
||||
}
|
||||
);
|
||||
|
@ -22,16 +27,23 @@ async function updatePage(
|
|||
}
|
||||
|
||||
async function deletePage(id: number): Promise<boolean> {
|
||||
const jwt = window.localStorage.getItem("jwt");
|
||||
if (!jwt) return false;
|
||||
const res = await fetch(
|
||||
`${import.meta.env.QUOT_API_URL}/api/pages?id=eq.${id}`,
|
||||
{ method: "DELETE" }
|
||||
`${import.meta.env.QUOT_API_ENDPOINT}/pages?id=eq.${id}`,
|
||||
{
|
||||
method: "DELETE",
|
||||
headers: { authorization: `Bearer ${jwt}` },
|
||||
}
|
||||
);
|
||||
return res.ok;
|
||||
}
|
||||
|
||||
async function fetchPage(id: number): Promise<Pages.ResponsePage> {
|
||||
const jwt = window.localStorage.getItem("jwt");
|
||||
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;
|
||||
return data[0]!;
|
||||
|
|
20
compose.yml
20
compose.yml
|
@ -5,10 +5,30 @@ services:
|
|||
restart: unless-stopped
|
||||
ports: ["8080:8080"]
|
||||
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
|
||||
volumes:
|
||||
- postgres_socket:/var/run/postgresql
|
||||
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:
|
||||
profiles: [dev]
|
||||
image: amacneil/dbmate:1.15@sha256:8fb25de3fce073e39eb3f9411af0410d0e26cc6d120544a7510b964e218abc27
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
-- migrate:up
|
||||
CREATE ROLE anon;
|
||||
ALTER ROLE anon SET statement_timeout = '1s';
|
||||
CREATE ROLE guest;
|
||||
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 $$
|
||||
BEGIN
|
||||
|
@ -9,20 +11,52 @@ CREATE FUNCTION update_timestamp() RETURNS trigger LANGUAGE plpgsql AS $$
|
|||
END
|
||||
$$;
|
||||
|
||||
CREATE TABLE pages (
|
||||
id SERIAL PRIMARY KEY,
|
||||
title TEXT NOT NULL UNIQUE,
|
||||
text TEXT NOT NULL,
|
||||
CREATE TABLE users (
|
||||
id TEXT PRIMARY KEY DEFAULT current_setting('request.jwt.claims', true)::json ->> 'sub'::text,
|
||||
created 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
|
||||
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
|
||||
DROP TABLE users;
|
||||
DROP TABLE pages;
|
||||
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 (
|
||||
id integer NOT NULL,
|
||||
user_id text DEFAULT ((current_setting('request.jwt.claims'::text, true))::json ->> 'sub'::text),
|
||||
title text NOT NULL,
|
||||
text text 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: -
|
||||
--
|
||||
|
@ -100,6 +113,14 @@ ALTER TABLE ONLY public.schema_migrations
|
|||
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: -
|
||||
--
|
||||
|
@ -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();
|
||||
|
||||
|
||||
--
|
||||
-- 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
|
||||
--
|
||||
|
|
|
@ -3,5 +3,6 @@ set -e
|
|||
dbmate --wait up
|
||||
export PGRST_DB_URI="${PGRST_DB_URI:-${DATABASE_URL}}"
|
||||
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 "$@"
|
||||
|
|
Loading…
Add table
Reference in a new issue