├── .env.template
├── .github
├── dependabot.yml
├── linters
│ └── .markdown-lint.yml
└── workflows
│ ├── combine.yml
│ └── lint-test-fmt.yml
├── .gitignore
├── .markdownlintignore
├── CHANGELOG.md
├── README.md
├── client
├── .eslintignore
├── .eslintrc.cjs
├── .prettierignore
├── .prettierrc
├── globals.d.ts
├── index.html
├── package.json
├── postcss.config.js
├── prettier.config.js
├── public
│ └── robots.txt
├── src
│ ├── App.svelte
│ ├── components
│ │ ├── Footer.svelte
│ │ ├── LazyRoute.svelte
│ │ ├── LazyRouteGuard.svelte
│ │ └── Nav.svelte
│ ├── hmr.ts
│ ├── index.css
│ ├── index.ts
│ ├── routes
│ │ ├── About.svelte
│ │ ├── Home.svelte
│ │ └── NotFound.svelte
│ └── tests
│ │ └── example.ts
├── svelte.config.js
├── tailwind.config.ts
├── tsconfig.json
└── vite.config.ts
├── server
├── .cargo
│ └── config.toml
├── Cargo.toml
├── Rocket.toml
├── rustfmt.toml
└── src
│ ├── cache.rs
│ ├── cors.rs
│ ├── csp.rs
│ ├── main.rs
│ └── postgres.rs
└── update.sh
/.env.template:
--------------------------------------------------------------------------------
1 | # Client
2 | VITE_API_URL="http://127.0.0.1:3000/api"
3 | VITE_LOG_LEVEL=debug
4 | BUILD_PATH=../server/dist
5 |
6 | # Server
7 | RUST_LOG=debug
8 | CORS_ORIGIN=127.0.0.1:3000
9 | DATABASE_URL=postgres://postgres:postgres@localhost:5432/postgres
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: cargo
4 | directory: /server
5 | schedule:
6 | interval: weekly
7 | commit-message:
8 | prefix: "build"
9 | prefix-development: "build"
10 |
11 | - package-ecosystem: npm
12 | directory: /client
13 | schedule:
14 | interval: weekly
15 | commit-message:
16 | prefix: "build"
17 | prefix-development: "build"
18 |
19 | - package-ecosystem: github-actions
20 | directory: /
21 | schedule:
22 | interval: weekly
23 | commit-message:
24 | prefix: "build"
25 | prefix-development: "build"
26 |
--------------------------------------------------------------------------------
/.github/linters/.markdown-lint.yml:
--------------------------------------------------------------------------------
1 | {
2 | "MD013": false,
3 | }
--------------------------------------------------------------------------------
/.github/workflows/combine.yml:
--------------------------------------------------------------------------------
1 | name: "Combine Dependabot PRs"
2 | on:
3 | workflow_dispatch:
4 |
5 | jobs:
6 | combine-prs:
7 | runs-on: ubuntu-latest
8 | steps:
9 | - uses: actions/checkout@v4.1.1
10 | - uses: maadhattah/combine-dependabot-prs@main
11 | with:
12 | branchPrefix: "dependabot"
13 | mustBeGreen: true
14 | combineBranchName: "combined-prs"
15 | ignoreLabel: "nocombine"
16 | baseBranch: "main"
17 | openPR: true
18 |
--------------------------------------------------------------------------------
/.github/workflows/lint-test-fmt.yml:
--------------------------------------------------------------------------------
1 | name: 'TS Lint, Test, and Format'
2 | on:
3 | pull_request:
4 | workflow_dispatch:
5 |
6 | jobs:
7 | lint-test-format:
8 | runs-on: ubuntu-latest
9 | defaults:
10 | run:
11 | working-directory: ./client
12 | steps:
13 | - uses: actions/checkout@v4.1.1
14 | - uses: actions/setup-node@v4
15 | with:
16 | node-version: 'latest'
17 | cache: 'npm'
18 | cache-dependency-path: '**/package-lock.json'
19 | - run: npm ci
20 | - run: npm run lint:fix
21 | - run: npm run tsc
22 | - run: npm run test
23 | - run: npm run fmt
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Directories
2 | .cargo
3 | .turbo
4 | assets
5 | build
6 | dist
7 | node_modules
8 | public
9 | target
10 |
11 | # Files
12 | .env
13 | .log
14 | Cargo.lock
15 | pnpm-lock.yaml
16 |
17 | # User Settings
18 | .idea
19 | .vscode
--------------------------------------------------------------------------------
/.markdownlintignore:
--------------------------------------------------------------------------------
1 | LICENSE
2 | LICENSE-MIT
3 | LICENSE-APACHE
4 | node_modules
5 | target
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Change Log
2 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Template: Rocket + Svelte SPA
2 |
3 | ## Backend
4 |
5 | - __[Rocket](https://rocket.rs)__
6 | - __[PostgreSQL](https://www.postgresql.org)__
7 |
8 | ## Frontend
9 |
10 | - __[Svelte](https://svelte.dev)__
11 | - __[svelte-navigator](https://github.com/mefechoel/svelte-navigator)__
12 | - __[TypeScript](https://www.typescriptlang.org)__
13 | - __[Tailwind CSS](https://tailwindcss.com)__
14 | - __[Vite](https://vitejs.dev/)__ + __[Vitest](https://vitest.dev/)__
15 |
16 | ## Getting Started
17 |
18 | - Clone the repository: `git clone
19 | https://github.com/robertwayne/template-rocket-svelte-spa`
20 | - Change `.env.TEMPLATE` to `.env` and set your Postgres credentials _(if not
21 | using defaults)_.
22 | - Build the client with `pnpm run build` from inside the `/client` directory.
23 | _Alternatively, you can use `pnpm run dev` to run the client with vite dev
24 | server._
25 | - Run the server with `cargo run` from inside the `/server` directory.
26 | - If you're serving from Rocket, visit `http://127.0.0.1:3000`.
27 | - If you're serving from vite, visit `http://127.0.0.1:8000`.
28 |
29 | ## Client Notes
30 |
31 | - Async, lazy loading route wrappers.
32 | - Responsive navigation menu built-in.
33 |
34 | ## Server Notes
35 |
36 | - Sets Cache Control headers for HTML, CSS, JS, WEBP, SVG, and WOFF2.
37 | - Sets CORS and CSP headers.
38 | - Includes a PostgreSQL fairing, which is disabled by default. Add this fairing
39 | in `main.rs` if you wish to use it.
40 |
41 | ## GitHub Action Notes
42 |
43 | - Runs _(client)_ tests, eslint, tsc, and prettier on PRs.
44 | - Runs dependabot weekly. You can manually run `combine` to squish all
45 | dependabot PRs into one PR.
46 | - Server tests/formatting are not run on PR _(yet)_.
47 |
48 | ## Misc Scripts
49 |
50 | | Command | Action |
51 | |---------|--------|
52 | | ./update.sh | Updates the dependencies of both the client and server projects. |
53 |
54 | ## Other Templates
55 |
56 | - __[Axum + SolidJS](https://github.com/robertwayne/template-axum-solidjs-spa)__
57 |
--------------------------------------------------------------------------------
/client/.eslintignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | dist/
--------------------------------------------------------------------------------
/client/.eslintrc.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | env: {
3 | browser: true,
4 | node: true,
5 | },
6 | parser: "@typescript-eslint/parser",
7 | plugins: ["svelte3", "@typescript-eslint"],
8 | overrides: [
9 | {
10 | files: ["*.ts"],
11 | extends: [
12 | "eslint:recommended",
13 | "plugin:@typescript-eslint/recommended",
14 | ],
15 | rules: {
16 | "@typescript-eslint/array-type": [
17 | "error",
18 | {
19 | default: "generic",
20 | },
21 | ],
22 | "no-undef": "off",
23 | "no-unused-vars": "off",
24 | "@typescript-eslint/no-unused-vars": [
25 | "error",
26 | {
27 | argsIgnorePattern: "^_",
28 | varsIgnorePattern: "^_",
29 | },
30 | ],
31 | "@typescript-eslint/no-empty-function": "warn",
32 | },
33 | },
34 | {
35 | files: ["*.svelte"],
36 | processor: "svelte3/svelte3",
37 | extends: [
38 | "eslint:recommended",
39 | "plugin:@typescript-eslint/recommended",
40 | ],
41 | rules: {
42 | "@typescript-eslint/array-type": [
43 | "error",
44 | {
45 | default: "generic",
46 | },
47 | ],
48 | "no-undef": "off",
49 | "no-unused-vars": "off",
50 | },
51 | },
52 | ],
53 | rules: {},
54 | settings: {
55 | "svelte3/typescript": require("typescript"),
56 | },
57 | }
58 |
--------------------------------------------------------------------------------
/client/.prettierignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | *.min.css
3 | *.min.js
4 | *.webmanifest
--------------------------------------------------------------------------------
/client/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "tabWidth": 4,
3 | "semi": false,
4 | "svelteSortOrder" : "options-scripts-markup-styles",
5 | "svelteStrictMode": false,
6 | "svelteIndentScriptAndStyle": true,
7 | "svelteAllowShorthand": true
8 | }
--------------------------------------------------------------------------------
/client/globals.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
--------------------------------------------------------------------------------
/client/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 | Template: Rocket + Svelte
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/client/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "client",
3 | "version": "0.1.0",
4 | "repository": "https://github.com/robertwayne/template-rocket-svelte-spa",
5 | "author": "Rob Wagner ",
6 | "license": "",
7 | "type": "module",
8 | "private": true,
9 | "scripts": {
10 | "dev": "vite",
11 | "build": "vite build",
12 | "serve": "vite preview",
13 | "fmt": "prettier --write --plugin-search-dir=. \"**/*.{ts,html,css,cjs,json,tsx}\"",
14 | "lint": "eslint \"src/**/*.{ts,svelte}\"",
15 | "lint:fix": "eslint --fix --fix-type problem,suggestion \"**/*.{ts,svelte}\"",
16 | "tsc": "tsc --noEmit",
17 | "tsc:watch": "tsc --noEmit --watch",
18 | "test": "vitest run --no-watch",
19 | "test:watch": "vitest"
20 | },
21 | "devDependencies": {
22 | "@sveltejs/vite-plugin-svelte": "^3.1.0",
23 | "@tailwindcss/forms": "^0.5.7",
24 | "@tailwindcss/typography": "^0.5.13",
25 | "@typescript-eslint/eslint-plugin": "^7.8.0",
26 | "@typescript-eslint/parser": "^7.8.0",
27 | "autoprefixer": "^10.4.19",
28 | "dotenv": "^16.4.5",
29 | "eslint": "^9.2.0",
30 | "eslint-plugin-svelte3": "^4.0.0",
31 | "happy-dom": "^14.10.1",
32 | "postcss": "^8.4.38",
33 | "postcss-load-config": "^5.1.0",
34 | "prettier": "^3.2.5",
35 | "prettier-plugin-tailwindcss": "^0.5.14",
36 | "svelte": "^4.2.15",
37 | "svelte-language-server": "^0.16.9",
38 | "svelte-navigator": "^3.2.2",
39 | "svelte-preprocess": "^5.1.4",
40 | "tailwindcss": "^3.4.3",
41 | "typescript": "^5.4.5",
42 | "vite": "^5.2.11",
43 | "vitest": "^1.6.0"
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/client/postcss.config.js:
--------------------------------------------------------------------------------
1 | import autoprefixer from "autoprefixer"
2 | import tailwindcss from "tailwindcss"
3 |
4 | export default {
5 | plugins: [tailwindcss(), autoprefixer],
6 | }
7 |
--------------------------------------------------------------------------------
/client/prettier.config.js:
--------------------------------------------------------------------------------
1 | import prettier from "prettier-plugin-tailwindcss"
2 |
3 | export default {
4 | plugins: [prettier],
5 | }
6 |
--------------------------------------------------------------------------------
/client/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
--------------------------------------------------------------------------------
/client/src/App.svelte:
--------------------------------------------------------------------------------
1 |
22 |
23 |
27 |
28 |
29 |
30 |
31 |
32 |
33 | {#each Object.entries(routes) as [path, component]}
34 | Loading...
35 | {/each}
36 | import("./routes/NotFound.svelte")}
39 | />
40 |
41 |
42 |
43 |
44 |
45 |
--------------------------------------------------------------------------------
/client/src/components/Footer.svelte:
--------------------------------------------------------------------------------
1 |
14 |
--------------------------------------------------------------------------------
/client/src/components/LazyRoute.svelte:
--------------------------------------------------------------------------------
1 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/client/src/components/LazyRouteGuard.svelte:
--------------------------------------------------------------------------------
1 |
45 |
46 | {#if loadedComponent}
47 |
48 | {:else if showFallback}
49 |
50 | {/if}
51 |
--------------------------------------------------------------------------------
/client/src/components/Nav.svelte:
--------------------------------------------------------------------------------
1 |
19 |
20 |
78 |
--------------------------------------------------------------------------------
/client/src/hmr.ts:
--------------------------------------------------------------------------------
1 | import type { Writable } from "svelte/store"
2 |
3 | let stores: Record> = {}
4 |
5 | export function registerStore(id: string, store: Writable): void {
6 | stores[id] = store
7 | }
8 |
9 | // preserve the store across HMR updates
10 | if (import.meta.hot) {
11 | if (import.meta.hot.data.stores) {
12 | stores = import.meta.hot.data.stores
13 | }
14 | import.meta.hot.accept()
15 | import.meta.hot.dispose(() => {
16 | if (import.meta.hot) {
17 | import.meta.hot.data.stores = stores
18 | }
19 | })
20 | }
21 |
--------------------------------------------------------------------------------
/client/src/index.css:
--------------------------------------------------------------------------------
1 | *,
2 | *::before,
3 | *::after {
4 | box-sizing: border-box;
5 | }
6 |
7 | * {
8 | margin: 0;
9 | padding: 0;
10 | font: inherit;
11 | }
12 |
13 | html,
14 | body,
15 | body > #app > div {
16 | font-family: "Roboto", sans-serif;
17 | font-display: swap;
18 | color-scheme: dark light;
19 | overflow-x: hidden;
20 | min-height: 100svh;
21 | }
22 |
23 | a {
24 | color: inherit;
25 | text-decoration: none;
26 | }
27 |
28 | img,
29 | picture,
30 | svg,
31 | video {
32 | display: block;
33 | max-width: 100%;
34 | }
35 |
36 | @tailwind base;
37 | @tailwind components;
38 | @tailwind utilities;
39 |
--------------------------------------------------------------------------------
/client/src/index.ts:
--------------------------------------------------------------------------------
1 | import "./index.css"
2 | import "./hmr"
3 |
4 | import App from "./App.svelte"
5 |
6 | const app = new App({
7 | target: document.body,
8 | })
9 |
10 | export default app
11 |
--------------------------------------------------------------------------------
/client/src/routes/About.svelte:
--------------------------------------------------------------------------------
1 |
2 |
About Page!
3 |
Welcome to the about page!
4 |
5 |
--------------------------------------------------------------------------------
/client/src/routes/Home.svelte:
--------------------------------------------------------------------------------
1 |
2 |
Home Page!
3 |
Welcome to the home page!
4 |
5 |
--------------------------------------------------------------------------------
/client/src/routes/NotFound.svelte:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
404 Not Found
7 |
The page you're looking for doesn't exist!
8 |
9 |
15 |
16 |
--------------------------------------------------------------------------------
/client/src/tests/example.ts:
--------------------------------------------------------------------------------
1 | // This is just an example test. You should remove it.
2 | if (import.meta.vitest) {
3 | const { it, expect } = import.meta.vitest
4 |
5 | it("should work", () => {
6 | expect(true).toBe(true)
7 | })
8 | }
9 |
--------------------------------------------------------------------------------
/client/svelte.config.js:
--------------------------------------------------------------------------------
1 | import preprocess from "svelte-preprocess"
2 |
3 | export default {
4 | preprocess: preprocess({ postcss: true }),
5 | }
6 |
--------------------------------------------------------------------------------
/client/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | content: ["./src/**/*.{html,js,svelte,ts}"],
3 | theme: {
4 | extend: {},
5 | },
6 | darkMode: "class",
7 | plugins: [
8 | require("@tailwindcss/typography"),
9 | require("@tailwindcss/forms"),
10 | ],
11 | }
12 |
--------------------------------------------------------------------------------
/client/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "esnext",
4 | "module": "esnext",
5 | "moduleResolution": "node",
6 | "lib": ["esnext", "DOM"],
7 | "rootDir": "../",
8 | "outDir": "dist",
9 | "strict": true,
10 | "alwaysStrict": true,
11 | "strictFunctionTypes": true,
12 | "strictNullChecks": true,
13 | "strictPropertyInitialization": true,
14 | "esModuleInterop": true,
15 | "forceConsistentCasingInFileNames": true,
16 | "noImplicitAny": true,
17 | "noImplicitReturns": true,
18 | "noImplicitThis": true,
19 | "noFallthroughCasesInSwitch": true,
20 | "noUnusedLocals": true,
21 | "noUnusedParameters": true,
22 | "emitDecoratorMetadata": true,
23 | "experimentalDecorators": true,
24 | "downlevelIteration": true,
25 | "declaration": true,
26 | "skipLibCheck": true,
27 | "pretty": true,
28 | "types": ["vitest/importMeta", "vite/client"]
29 | },
30 | "include": ["src/**/*", "*.d.ts"],
31 | "exclude": ["node_modules"]
32 | }
33 |
--------------------------------------------------------------------------------
/client/vite.config.ts:
--------------------------------------------------------------------------------
1 | import * as dotenv from "dotenv"
2 |
3 | import { defineConfig } from "vitest/config"
4 | import { svelte } from "@sveltejs/vite-plugin-svelte"
5 |
6 | dotenv.config({ path: "../.env" })
7 |
8 | export default defineConfig({
9 | base: "/",
10 | plugins: [svelte()],
11 | build: {
12 | outDir: process.env.BUILD_PATH || "dist",
13 | emptyOutDir: true,
14 | },
15 | optimizeDeps: {
16 | exclude: ["svelte-navigator"],
17 | },
18 | server: {
19 | host: "127.0.0.1",
20 | port: 8000,
21 | },
22 | define: {
23 | "import.meta.vitest": false,
24 | },
25 | test: {
26 | includeSource: ["src/**/*.ts"],
27 | globals: true,
28 | environment: "happy-dom",
29 | },
30 | })
31 |
--------------------------------------------------------------------------------
/server/.cargo/config.toml:
--------------------------------------------------------------------------------
1 | [build]
2 | rustflags = ["-C", "target-cpu=native"]
3 |
4 | # Use the mold linker
5 | # [target.x86_64-unknown-linux-gnu]
6 | # linker = "clang"
7 | # rustflags = [
8 | # "-C",
9 | # "link-arg=-fuse-ld=/usr/bin/mold",
10 | # "-C",
11 | # "target-cpu=native",
12 | # "-Z",
13 | # "share-generics=y",
14 | # ]
15 |
16 | [registries.crates-io]
17 | protocol = "sparse"
--------------------------------------------------------------------------------
/server/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "server"
3 | authors = ["Rob Wagner "]
4 | license = ""
5 | repository = "https://github.com/robertwayne/template-rocket-svelte-spa"
6 | version = "0.1.0"
7 | edition = "2021"
8 | publish = false
9 |
10 | [dependencies]
11 | dotenvy = "0.15"
12 | rocket = { version = "0.5", features = ["json", "secrets"] }
13 | sqlx = { version = "0.7", features = ["runtime-tokio-rustls", "postgres"] }
14 | tracing = { version = "0.1", default-features = false, features = ["std"] }
15 | tracing-subscriber = { version = "0.3", features = ["env-filter"] }
16 |
17 | [profile.release]
18 | opt-level = 3
19 | codegen-units = 1
20 | lto = true
21 | strip = true
22 |
--------------------------------------------------------------------------------
/server/Rocket.toml:
--------------------------------------------------------------------------------
1 | [default]
2 | port = 3000
3 |
4 | [release]
5 | secret_key = ""
6 |
--------------------------------------------------------------------------------
/server/rustfmt.toml:
--------------------------------------------------------------------------------
1 | group_imports = "StdExternalCrate"
2 | imports_granularity = "Crate"
3 | reorder_imports = true
4 |
--------------------------------------------------------------------------------
/server/src/cache.rs:
--------------------------------------------------------------------------------
1 | use rocket::{
2 | fairing::{self, Fairing},
3 | http::{ContentType, Header},
4 | Request, Response,
5 | };
6 |
7 | /// Attaches a Cache Control header to all responses. The default implementation
8 | /// caches CSS, JS, WOFF2, and WEBP files only, with a max age of 1 year.
9 | #[derive(Debug)]
10 | pub struct CacheControl {
11 | max_age: u32,
12 | cache_types: Vec,
13 | }
14 |
15 | impl Default for CacheControl {
16 | fn default() -> Self {
17 | CacheControl {
18 | max_age: 60 * 60 * 24 * 365,
19 | cache_types: vec![
20 | ContentType::CSS,
21 | ContentType::JavaScript,
22 | ContentType::WOFF2,
23 | ContentType::WEBP,
24 | ],
25 | }
26 | }
27 | }
28 |
29 | #[rocket::async_trait]
30 | impl Fairing for CacheControl {
31 | fn info(&self) -> fairing::Info {
32 | fairing::Info {
33 | name: "Cache Control",
34 | kind: fairing::Kind::Response,
35 | }
36 | }
37 |
38 | async fn on_response<'r>(&self, _request: &'r Request<'_>, response: &mut Response<'r>) {
39 | if let Some(content_type) = response.content_type() {
40 | if self.cache_types.contains(&content_type) {
41 | response.set_header(Header::new(
42 | "Cache-Control",
43 | format!("public, max-age={}", self.max_age),
44 | ));
45 | }
46 | }
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/server/src/cors.rs:
--------------------------------------------------------------------------------
1 | use std::env;
2 |
3 | use rocket::{
4 | fairing::{self, Fairing},
5 | http::{Header, Method, Status},
6 | Request, Response,
7 | };
8 |
9 | /// Attaches a CORS policy header to defined responses.
10 | #[derive(Debug, Default)]
11 | pub struct CrossOriginResourceSharing;
12 |
13 | #[rocket::async_trait]
14 | impl Fairing for CrossOriginResourceSharing {
15 | fn info(&self) -> fairing::Info {
16 | fairing::Info {
17 | name: "Cross-Origin Resource Sharing",
18 | kind: fairing::Kind::Response,
19 | }
20 | }
21 |
22 | async fn on_response<'r>(&self, request: &'r Request<'_>, response: &mut Response<'r>) {
23 | response.set_header(Header::new(
24 | "Access-Control-Allow-Origin",
25 | env::var("CORS_ORIGIN").unwrap_or_else(|_| "http://localhost:5173".to_string()),
26 | ));
27 | response.set_header(Header::new(
28 | "Access-Control-Allow-Headers",
29 | "Accept, Content-Type",
30 | ));
31 | response.set_header(Header::new(
32 | "Access-Control-Allow-Methods",
33 | "DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT",
34 | ));
35 | response.set_header(Header::new("Vary", "Origin"));
36 | response.set_header(Header::new("Access-Control-Allow-Credentials", "true"));
37 | response.set_header(Header::new("Access-Control-Max-Age", "86400"));
38 |
39 | if request.method() == Method::Options {
40 | response.set_status(Status::NoContent);
41 | }
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/server/src/csp.rs:
--------------------------------------------------------------------------------
1 | use rocket::{
2 | fairing::{self, Fairing},
3 | http::Header,
4 | Request, Response,
5 | };
6 |
7 | /// Attaches a Content Security Policy header to all responses.
8 | #[derive(Debug, Default)]
9 | pub struct ContentSecurityPolicy;
10 |
11 | #[rocket::async_trait]
12 | impl Fairing for ContentSecurityPolicy {
13 | fn info(&self) -> fairing::Info {
14 | fairing::Info {
15 | name: "Content Security Policy",
16 | kind: fairing::Kind::Response,
17 | }
18 | }
19 |
20 | async fn on_response<'r>(&self, _request: &'r Request<'_>, response: &mut Response<'r>) {
21 | response.set_header(
22 | Header::new("Content-Security-Policy",
23 | "default-src 'self'; script-src 'self'; script-src-elem 'self'; script-src-attr 'self'; style-src 'self'; style-src-elem 'self' 'unsafe-inline'; style-src-attr 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self' data:; connect-src 'self'; media-src 'self'; object-src 'none'; prefetch-src 'self'; child-src 'none'; frame-src 'none'; worker-src 'self'; frame-ancestors 'none'; form-action 'self'; upgrade-insecure-requests; base-uri 'self'; manifest-src 'self' data:"
24 | ));
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/server/src/main.rs:
--------------------------------------------------------------------------------
1 | #![forbid(unsafe_code)]
2 |
3 | #[macro_use]
4 | extern crate rocket;
5 |
6 | mod cache;
7 | mod cors;
8 | mod csp;
9 | mod postgres;
10 |
11 | use std::{
12 | env,
13 | path::{Path, PathBuf},
14 | };
15 |
16 | use dotenvy::dotenv;
17 | use rocket::{
18 | fs::{relative, NamedFile},
19 | shield::Shield,
20 | };
21 | use tracing_subscriber::EnvFilter;
22 |
23 | use crate::{cache::CacheControl, cors::CrossOriginResourceSharing, csp::ContentSecurityPolicy};
24 |
25 | const DIST: &str = relative!("dist");
26 |
27 | /// Matches against the robots.txt within the /dist root directory.
28 | #[get("/<_..>", rank = 0)]
29 | async fn robots() -> Option {
30 | NamedFile::open(Path::new(DIST).join("robots.txt"))
31 | .await
32 | .ok()
33 | }
34 |
35 | /// Matches against any file within the /dist/assets directory.
36 | #[get("/", rank = 1)]
37 | async fn static_files(file: PathBuf) -> Option {
38 | NamedFile::open(Path::new(DIST).join("assets/").join(file))
39 | .await
40 | .ok()
41 | }
42 |
43 | /// Matches against the index.html file within the /dist directory. This is the
44 | /// entry point to your SPA, dynamically populated by Svelte and Vite at build
45 | /// time.
46 | #[get("/<_..>", rank = 2)]
47 | async fn index() -> Option {
48 | NamedFile::open(Path::new(DIST).join("index.html"))
49 | .await
50 | .ok()
51 | }
52 |
53 | #[launch]
54 | fn rocket() -> _ {
55 | dotenv().ok();
56 | tracing_subscriber::fmt()
57 | .with_env_filter(EnvFilter::from_default_env())
58 | .init();
59 |
60 | rocket::build()
61 | .attach(CacheControl::default())
62 | .attach(ContentSecurityPolicy::default())
63 | .attach(CrossOriginResourceSharing::default())
64 | .attach(Shield::default())
65 | .mount("/robots.txt", routes![robots])
66 | .mount("/assets", routes![static_files])
67 | .mount("/", routes![index])
68 | }
69 |
--------------------------------------------------------------------------------
/server/src/postgres.rs:
--------------------------------------------------------------------------------
1 | use std::env;
2 |
3 | use rocket::{
4 | fairing::{self, Fairing},
5 | Build, Rocket,
6 | };
7 | use sqlx::PgPool;
8 |
9 | /// Creates and attaches a Postgres connection pool to the global &State in
10 | /// Rocket.
11 | ///
12 | /// FIXME: Replace with built-in Rocket DB pools.
13 | #[derive(Debug, Default)]
14 | pub struct Postgres;
15 |
16 | #[rocket::async_trait]
17 | impl Fairing for Postgres {
18 | fn info(&self) -> fairing::Info {
19 | fairing::Info {
20 | name: "Postgres",
21 | kind: fairing::Kind::Ignite,
22 | }
23 | }
24 |
25 | async fn on_ignite(&self, rocket: Rocket) -> fairing::Result {
26 | let url = env::var("DATABASE_URL").expect("DATABASE_URL must be set");
27 | let pool = PgPool::connect(url.as_str())
28 | .await
29 | .expect("Failed to connect to PostgreSQL database.");
30 |
31 | Ok(rocket.manage(pool))
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/update.sh:
--------------------------------------------------------------------------------
1 | echo "Updating client dependencies..."
2 | cd ./client
3 | pnpm upgrade &> /dev/null
4 |
5 | echo "Updating server dependencies..."
6 | cd ../server
7 | cargo update --quiet
8 |
9 | echo "All up to date! ✨"
--------------------------------------------------------------------------------