├── web
├── .prettierrc.json
├── env.d.ts
├── public
│ └── favicon.ico
├── e2e
│ ├── tsconfig.json
│ └── vue.spec.ts
├── src
│ ├── components
│ │ ├── nav
│ │ │ ├── index.ts
│ │ │ ├── NavBar.vue
│ │ │ └── NavLink.vue
│ │ ├── Avatar.vue
│ │ ├── UserProfileImage.vue
│ │ ├── CodePreview.vue
│ │ └── DropdownMenu.vue
│ ├── assets
│ │ ├── main.css
│ │ ├── logo-black.svg
│ │ └── logo-white.svg
│ ├── stores
│ │ └── counter.ts
│ ├── main.ts
│ ├── views
│ │ ├── HomeView.vue
│ │ ├── posts
│ │ │ ├── MyPostsView.vue
│ │ │ └── NewPostView.vue
│ │ └── ProfileView.vue
│ ├── router
│ │ └── index.ts
│ ├── App.vue
│ ├── utils
│ │ └── index.ts
│ └── api
│ │ └── index.ts
├── postcss.config.js
├── tsconfig.vitest.json
├── tsconfig.config.json
├── tsconfig.json
├── tsconfig.app.json
├── .eslintrc.cjs
├── vite.config.ts
├── .gitignore
├── index.html
├── package.json
├── README.md
├── playwright.config.ts
└── tailwind.config.js
├── api
├── profile
│ ├── .gitignore
│ ├── Cargo.toml
│ ├── src
│ │ ├── config.rs
│ │ ├── auth.rs
│ │ ├── model.rs
│ │ └── lib.rs
│ └── Cargo.lock
└── post
│ ├── go.mod
│ ├── config.go
│ ├── go.sum
│ ├── middleware.go
│ ├── utils.go
│ ├── main.go
│ ├── model.go
│ └── data.go
├── .gitignore
├── modules
└── spin_static_fs.wasm
├── scripts
├── create-profile.json
├── update-profile.json
├── db
│ ├── initdb.d
│ │ ├── 1_create_table_profiles.sql
│ │ └── 2_create_table_posts.sql
│ └── up.sh
└── validate.sh
├── .vscode
├── settings.json
└── extensions.json
├── README.md
├── spin.toml
└── LICENSE
/web/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {}
--------------------------------------------------------------------------------
/api/profile/.gitignore:
--------------------------------------------------------------------------------
1 | target/
2 |
--------------------------------------------------------------------------------
/web/env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | scripts/db/data/
2 | .spin/
3 | api/post/main.wasm
4 |
--------------------------------------------------------------------------------
/web/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fermyon/code-things/HEAD/web/public/favicon.ico
--------------------------------------------------------------------------------
/modules/spin_static_fs.wasm:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fermyon/code-things/HEAD/modules/spin_static_fs.wasm
--------------------------------------------------------------------------------
/web/e2e/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@vue/tsconfig/tsconfig.node.json",
3 | "include": ["./**/*"]
4 | }
5 |
--------------------------------------------------------------------------------
/web/src/components/nav/index.ts:
--------------------------------------------------------------------------------
1 | export { default as NavBar } from "./NavBar.vue";
2 | export { default as NavLink } from "./NavLink.vue";
3 |
--------------------------------------------------------------------------------
/scripts/create-profile.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "A4916AC2-AB0D-45A9-ADEA-959F8DEB2A14",
3 | "handle": "justin",
4 | "avatar": "https://avatars.githubusercontent.com/u/3060890?v=4"
5 | }
--------------------------------------------------------------------------------
/scripts/update-profile.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "A4916AC2-AB0D-45A9-ADEA-959F8DEB2A14",
3 | "handle": "justin",
4 | "avatar": "https://avatars.githubusercontent.com/u/3060890"
5 | }
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "[rust]": {
3 | "editor.defaultFormatter": "rust-lang.rust-analyzer"
4 | },
5 | "rust-analyzer.linkedProjects": [
6 | "api/profile/Cargo.toml"
7 | ],
8 | }
--------------------------------------------------------------------------------
/web/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: [
3 | require("postcss-import"),
4 | require("tailwindcss/nesting"),
5 | require("tailwindcss"),
6 | require("autoprefixer"),
7 | ],
8 | };
9 |
--------------------------------------------------------------------------------
/scripts/db/initdb.d/1_create_table_profiles.sql:
--------------------------------------------------------------------------------
1 | CREATE TABLE profiles (
2 | id varchar(64) NOT NULL,
3 | handle varchar(32) NOT NULL,
4 | avatar varchar(256),
5 | UNIQUE(handle),
6 | PRIMARY KEY (id)
7 | );
--------------------------------------------------------------------------------
/web/tsconfig.vitest.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.app.json",
3 | "exclude": [],
4 | "compilerOptions": {
5 | "composite": true,
6 | "lib": [],
7 | "types": ["node", "jsdom"]
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/web/tsconfig.config.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@vue/tsconfig/tsconfig.node.json",
3 | "include": ["vite.config.*", "vitest.config.*", "cypress.config.*", "playwright.config.*"],
4 | "compilerOptions": {
5 | "composite": true,
6 | "types": ["node"]
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/web/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "files": [],
3 | "references": [
4 | {
5 | "path": "./tsconfig.config.json"
6 | },
7 | {
8 | "path": "./tsconfig.app.json"
9 | },
10 | {
11 | "path": "./tsconfig.vitest.json"
12 | }
13 | ]
14 | }
15 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": [
3 | "bradlc.vscode-tailwindcss",
4 | "esbenp.prettier-vscode",
5 | "rust-lang.rust-analyzer",
6 | "serayuzgur.crates",
7 | "vue.vscode-typescript-vue-plugin",
8 | "vue.volar"
9 | ]
10 | }
--------------------------------------------------------------------------------
/web/src/assets/main.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @layer base {
3 | html {
4 | font-family: Sen, Europa, Avenir, system, -apple-system, ".SFNSText-Regular", San Francisco, Segoe UI, Helvetica Neue, Lucida Grande, sans-serif;
5 | }
6 | }
7 |
8 | @tailwind components;
9 | @tailwind utilities;
10 |
--------------------------------------------------------------------------------
/web/e2e/vue.spec.ts:
--------------------------------------------------------------------------------
1 | import { test, expect } from "@playwright/test";
2 |
3 | // See here how to get started:
4 | // https://playwright.dev/docs/intro
5 | test("visits the app root url", async ({ page }) => {
6 | await page.goto("/");
7 | await expect(page.locator("div.greetings > h1")).toHaveText("You did it!");
8 | });
9 |
--------------------------------------------------------------------------------
/web/tsconfig.app.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@vue/tsconfig/tsconfig.web.json",
3 | "include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
4 | "exclude": ["src/**/__tests__/*"],
5 | "compilerOptions": {
6 | "composite": true,
7 | "baseUrl": ".",
8 | "paths": {
9 | "@/*": ["./src/*"]
10 | }
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/scripts/db/initdb.d/2_create_table_posts.sql:
--------------------------------------------------------------------------------
1 | CREATE TABLE posts (
2 | id serial4 NOT NULL,
3 | author_id varchar(64) NOT NULL,
4 | "content" text NOT NULL,
5 | "type" varchar(64) NOT NULL,
6 | "data" text NOT NULL,
7 | visibility varchar(32) NOT NULL DEFAULT 'public',
8 | PRIMARY KEY (id),
9 | FOREIGN KEY (author_id) REFERENCES profiles(id)
10 | );
--------------------------------------------------------------------------------
/web/src/stores/counter.ts:
--------------------------------------------------------------------------------
1 | import { ref, computed } from "vue";
2 | import { defineStore } from "pinia";
3 |
4 | export const useCounterStore = defineStore("counter", () => {
5 | const count = ref(0);
6 | const doubleCount = computed(() => count.value * 2);
7 | function increment() {
8 | count.value++;
9 | }
10 |
11 | return { count, doubleCount, increment };
12 | });
13 |
--------------------------------------------------------------------------------
/api/post/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/fermyon/code-things/api/post
2 |
3 | go 1.20
4 |
5 | replace github.com/fermyon/spin/sdk/go v1.0.0 => github.com/jpflueger/spin/sdk/go v0.6.1-0.20230405131322-423d9b11be46
6 |
7 | require (
8 | github.com/MicahParks/keyfunc v1.9.0
9 | github.com/fermyon/spin/sdk/go v1.0.0
10 | github.com/go-chi/chi/v5 v5.0.8
11 | github.com/golang-jwt/jwt/v4 v4.5.0
12 | )
13 |
--------------------------------------------------------------------------------
/web/.eslintrc.cjs:
--------------------------------------------------------------------------------
1 | /* eslint-env node */
2 | require("@rushstack/eslint-patch/modern-module-resolution");
3 |
4 | module.exports = {
5 | root: true,
6 | extends: [
7 | "plugin:vue/vue3-essential",
8 | "eslint:recommended",
9 | "@vue/eslint-config-typescript",
10 | "@vue/eslint-config-prettier",
11 | ],
12 | parserOptions: {
13 | ecmaVersion: "latest",
14 | },
15 | };
16 |
--------------------------------------------------------------------------------
/web/src/components/Avatar.vue:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
14 |
18 |
--------------------------------------------------------------------------------
/web/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { fileURLToPath, URL } from "node:url";
2 |
3 | import { defineConfig } from "vite";
4 | import vue from "@vitejs/plugin-vue";
5 | import vueJsx from "@vitejs/plugin-vue-jsx";
6 |
7 | // https://vitejs.dev/config/
8 | export default defineConfig({
9 | plugins: [vue(), vueJsx()],
10 | resolve: {
11 | alias: {
12 | "@": fileURLToPath(new URL("./src", import.meta.url)),
13 | },
14 | },
15 | });
16 |
--------------------------------------------------------------------------------
/web/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | .DS_Store
12 | dist
13 | dist-ssr
14 | coverage
15 | *.local
16 |
17 | /cypress/videos/
18 | /cypress/screenshots/
19 |
20 | # Editor directories and files
21 | .vscode/*
22 | !.vscode/extensions.json
23 | .idea
24 | *.suo
25 | *.ntvs*
26 | *.njsproj
27 | *.sln
28 | *.sw?
29 |
30 | test-results/
31 | playwright-report/
32 |
--------------------------------------------------------------------------------
/web/src/components/UserProfileImage.vue:
--------------------------------------------------------------------------------
1 |
8 |
9 |
10 |
16 |
20 |
--------------------------------------------------------------------------------
/web/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Code Things
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/scripts/db/up.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | name=code-things-pg
4 | image=postgres
5 | username=code-things
6 | password=password
7 | dbname=code_things
8 | port=5432
9 |
10 | [[ $(docker ps -f "name=$name" --format '{{.Names}}') == $name ]] || docker run -d \
11 | --name "$name" \
12 | -p $port:$port \
13 | -e POSTGRES_USER=$username \
14 | -e POSTGRES_PASSWORD=$password \
15 | -e POSTGRES_DB=$dbname \
16 | -v "$(pwd)/scripts/db/data:/var/lib/postgresql/data" \
17 | -v "$(pwd)/scripts/db/initdb.d:/docker-entrypoint-initdb.d" \
18 | $image
19 |
--------------------------------------------------------------------------------
/api/profile/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "profile-api"
3 | authors = ["Justin Pflueger "]
4 | description = "Profile REST API"
5 | version = "0.1.0"
6 | edition = "2021"
7 |
8 | [lib]
9 | crate-type = [ "cdylib" ]
10 |
11 | [dependencies]
12 | anyhow = "1"
13 | bytes = "1"
14 | http = "0.2"
15 | spin-sdk = { git = "https://github.com/fermyon/spin", tag = "v1.0.0" }
16 | wit-bindgen-rust = { git = "https://github.com/bytecodealliance/wit-bindgen", rev = "cb871cfa1ee460b51eb1d144b175b9aab9c50aba" }
17 | serde = { version = "1.0", features = ["derive"] }
18 | serde_json = "1.0"
19 | jwt-simple = "0.11.3"
20 | base64 = "0.21.0"
21 |
22 | [workspace]
23 |
--------------------------------------------------------------------------------
/web/src/components/nav/NavBar.vue:
--------------------------------------------------------------------------------
1 |
2 |
21 |
22 |
--------------------------------------------------------------------------------
/web/src/components/nav/NavLink.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
14 |
15 |
16 |
17 |
18 |
19 |
29 |
--------------------------------------------------------------------------------
/api/post/config.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/fermyon/spin/sdk/go/config"
7 | )
8 |
9 | // Config Helpers
10 |
11 | func configGetRequired(key string) string {
12 | if val, err := config.Get(key); err != nil {
13 | panic(fmt.Sprintf("Missing required config item 'jwks_uri': %v", err))
14 | } else {
15 | return val
16 | }
17 | }
18 |
19 | func getIssuer() string {
20 | domain := configGetRequired("auth_domain")
21 | return fmt.Sprintf("https://%v/", domain)
22 | }
23 |
24 | func getAudience() string {
25 | return configGetRequired("auth_audience")
26 | }
27 |
28 | func getJwksUri() string {
29 | domain := configGetRequired("auth_domain")
30 | return fmt.Sprintf("https://%v/.well-known/jwks.json", domain)
31 | }
32 |
33 | func getDbUrl() string {
34 | return configGetRequired("db_url")
35 | }
36 |
--------------------------------------------------------------------------------
/api/post/go.sum:
--------------------------------------------------------------------------------
1 | github.com/MicahParks/keyfunc v1.9.0 h1:lhKd5xrFHLNOWrDc4Tyb/Q1AJ4LCzQ48GVJyVIID3+o=
2 | github.com/MicahParks/keyfunc v1.9.0/go.mod h1:IdnCilugA0O/99dW+/MkvlyrsX8+L8+x95xuVNtM5jw=
3 | github.com/go-chi/chi/v5 v5.0.8 h1:lD+NLqFcAi1ovnVZpsnObHGW4xb4J8lNmoYVfECH1Y0=
4 | github.com/go-chi/chi/v5 v5.0.8/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
5 | github.com/golang-jwt/jwt/v4 v4.4.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
6 | github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
7 | github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
8 | github.com/jpflueger/spin/sdk/go v0.6.1-0.20230405131322-423d9b11be46 h1:pcspeUjy3L8kvh6s1obO7KYI9I3uYwSVar7EeHayydE=
9 | github.com/jpflueger/spin/sdk/go v0.6.1-0.20230405131322-423d9b11be46/go.mod h1:ARV2oVtnUCykLM+xCBZq8MQrCZddzb3JbeBettYv1S0=
10 |
--------------------------------------------------------------------------------
/web/src/main.ts:
--------------------------------------------------------------------------------
1 | import { createApp } from "vue";
2 | import { createPinia } from "pinia";
3 | import { createAuth0 } from "@auth0/auth0-vue";
4 | import piniaPluginPersistedState from 'pinia-plugin-persistedstate';
5 |
6 | import "@/assets/main.css";
7 |
8 | import App from "./App.vue";
9 | import router from "./router";
10 |
11 | const app = createApp(App);
12 |
13 | // setup application state
14 | const pinia = createPinia();
15 | pinia.use(piniaPluginPersistedState)
16 | app.use(pinia);
17 |
18 | // router must come after store
19 | app.use(router);
20 |
21 | const auth0 = createAuth0({
22 | domain: import.meta.env.VITE_AUTH0_DOMAIN,
23 | clientId: import.meta.env.VITE_AUTH0_CLIENT_ID,
24 | authorizationParams: {
25 | audience: import.meta.env.VITE_AUTH0_AUDIENCE,
26 | redirect_uri: window.location.origin,
27 | },
28 | cacheLocation: "localstorage",
29 | });
30 | app.use(auth0);
31 |
32 | app.mount("#app");
33 |
--------------------------------------------------------------------------------
/web/src/views/HomeView.vue:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | Create Post
17 |
18 |
19 |
20 |
21 |
26 |
27 |
--------------------------------------------------------------------------------
/scripts/validate.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | scheme="http"
4 | host="127.0.0.1:3000"
5 |
6 | echo "Creating the profile"
7 | curl -v -X POST $scheme://$host/api/profile \
8 | -H 'Content-Type: application/json' \
9 | -d @scripts/create-profile.json
10 | echo "------------------------------------------------------------"; echo
11 |
12 | echo "Fetching the profile"
13 | curl -v -X GET $scheme://$host/api/profile/justin
14 | echo "------------------------------------------------------------"; echo
15 |
16 | echo "Updating the avatar"
17 | curl -v -X PUT $scheme://$host/api/profile/justin \
18 | -H 'Content-Type: application/json' \
19 | -d @scripts/update-profile.json
20 | echo "------------------------------------------------------------"; echo
21 |
22 | echo "Deleting profile"
23 | curl -v -X DELETE $scheme://$host/api/profile/justin
24 | echo "------------------------------------------------------------"; echo
25 |
26 | echo "Fetching after delete should be 404"
27 | curl -v -X GET $scheme://$host/api/profile/justin
28 | echo "------------------------------------------------------------"; echo
29 |
--------------------------------------------------------------------------------
/web/src/router/index.ts:
--------------------------------------------------------------------------------
1 | import { createRouter, createWebHistory } from "vue-router";
2 | import HomeView from "../views/HomeView.vue";
3 |
4 | const router = createRouter({
5 | history: createWebHistory(import.meta.env.BASE_URL),
6 | routes: [
7 | {
8 | path: "/",
9 | name: "home",
10 | component: HomeView,
11 | },
12 | {
13 | path: "/profile",
14 | name: "profile",
15 | // route level code-splitting
16 | // this generates a separate chunk (About.[hash].js) for this route
17 | // which is lazy-loaded when the route is visited.
18 | component: () => import("../views/ProfileView.vue"),
19 | },
20 | {
21 | path: "/posts/new",
22 | name: "posts-new",
23 | // route level code-splitting
24 | // this generates a separate chunk (About.[hash].js) for this route
25 | // which is lazy-loaded when the route is visited.
26 | component: () => import("../views/posts/NewPostView.vue"),
27 | },
28 | {
29 | path: "/posts",
30 | name: "my-posts",
31 | // route level code-splitting
32 | // this generates a separate chunk (About.[hash].js) for this route
33 | // which is lazy-loaded when the route is visited.
34 | component: () => import("../views/posts/MyPostsView.vue"),
35 | },
36 | ],
37 | });
38 |
39 | export default router;
40 |
--------------------------------------------------------------------------------
/web/src/App.vue:
--------------------------------------------------------------------------------
1 |
34 |
35 |
36 |
37 |
38 |
39 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
--------------------------------------------------------------------------------
/web/src/utils/index.ts:
--------------------------------------------------------------------------------
1 | // regular expression to validate permalink with
2 | const permalinkRegex =
3 | /https:\/\/github\.com\/[a-zA-Z0-9-_\.]+\/[a-zA-Z0-9-_\.]+\/blob\/[a-z0-9]{40}(\/[a-zA-Z0-9-_\.]+)+#L[0-9]+-L[0-9]+/;
4 |
5 | // function to get the permalink
6 | export const getPermalinkPreview = async (
7 | permalink: string
8 | ): Promise => {
9 | try {
10 | // test the input returning null if not valid
11 | if (!permalinkRegex.test(permalink)) {
12 | return null;
13 | }
14 |
15 | // parse the permalink
16 | const permalinkUrl = new URL(permalink);
17 |
18 | // get the range start/end from the hash
19 | const [rangeStart, rangeEnd] = permalinkUrl.hash
20 | .slice(1) // remove the '#'
21 | .split("-") // separate start/end
22 | .map((v) => parseInt(v.slice(1))); // remove the 'L' from start/end & parse as int
23 | permalinkUrl.hash = "";
24 |
25 | // change the host from github.com to raw.githubusercontent.com
26 | permalinkUrl.host = "raw.githubusercontent.com";
27 |
28 | // remove the /blob segment from the url
29 | permalinkUrl.pathname = permalinkUrl.pathname
30 | .split("/")
31 | .filter((part) => part != "blob")
32 | .join("/");
33 |
34 | const response = await fetch(permalinkUrl);
35 | const contents = await response.text();
36 | const contentRange = contents
37 | .split(/\r\n|\n|\r/)
38 | .slice(rangeStart - 1, rangeEnd)
39 | .join("\n");
40 | return contentRange;
41 | } catch (e: any) {
42 | //TODO: better error handling
43 | console.error("Failed to fetch the code preview", e);
44 | return null;
45 | }
46 | };
47 |
--------------------------------------------------------------------------------
/api/profile/src/config.rs:
--------------------------------------------------------------------------------
1 | use std::collections::HashSet;
2 |
3 | use jwt_simple::prelude::Duration;
4 | use spin_sdk::config::get;
5 |
6 | const KEY_DB_URL: &str = "db_url";
7 | const KEY_AUTH_DOMAIN: &str = "auth_domain";
8 | const KEY_AUTH_AUDIENCE: &str = "auth_audience";
9 | const KEY_AUTH_MAX_VALIDITY_SECS: &str = "auth_max_validity_secs";
10 |
11 | #[derive(Debug)]
12 | pub(crate) struct Config {
13 | pub db_url: String,
14 | pub auth_audiences: HashSet,
15 | pub auth_issuers: HashSet,
16 | pub auth_max_validity: Option,
17 | pub jwks_url: String,
18 | }
19 |
20 | impl Default for Config {
21 | fn default() -> Self {
22 | let db_url = get(KEY_DB_URL).expect("Missing config item 'db_url'");
23 | let auth_domain = get(KEY_AUTH_DOMAIN).expect("Missing config item 'auth_domain'");
24 | let auth_max_validity: Option = get(KEY_AUTH_MAX_VALIDITY_SECS)
25 | .ok()
26 | .map(|s| {
27 | s.parse::()
28 | .expect("Value provided must parse into an integer")
29 | })
30 | .map(Duration::from_secs);
31 |
32 | let auth_audiences = HashSet::from([
33 | get(KEY_AUTH_AUDIENCE).expect("Missing configuration item 'auth_audience'")
34 | ]);
35 | let auth_issuers = HashSet::from([format!("https://{0}/", auth_domain)]);
36 | let jwks_url = format!("https://{0}/.well-known/jwks.json", auth_domain);
37 |
38 | Self {
39 | db_url,
40 | auth_audiences,
41 | auth_issuers,
42 | auth_max_validity,
43 | jwks_url,
44 | }
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/web/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "code-things-web",
3 | "version": "0.0.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "vite --open --host 127.0.0.1 --port 3000",
7 | "build": "run-p type-check build-only",
8 | "preview": "vite preview",
9 | "test:unit": "vitest --environment jsdom --root src/",
10 | "test:e2e": "playwright test",
11 | "build-only": "vite build",
12 | "type-check": "vue-tsc --noEmit -p tsconfig.vitest.json --composite false",
13 | "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore"
14 | },
15 | "dependencies": {
16 | "@auth0/auth0-vue": "^2.0.1",
17 | "@headlessui/vue": "^1.7.7",
18 | "@heroicons/vue": "^2.0.13",
19 | "pinia": "^2.0.28",
20 | "pinia-plugin-persistedstate": "^3.0.2",
21 | "vue": "^3.2.45",
22 | "vue-router": "^4.1.6"
23 | },
24 | "devDependencies": {
25 | "@playwright/test": "^1.28.1",
26 | "@rushstack/eslint-patch": "^1.1.4",
27 | "@tailwindcss/forms": "^0.5.3",
28 | "@types/jsdom": "^20.0.1",
29 | "@types/node": "^18.11.12",
30 | "@vitejs/plugin-vue": "^4.0.0",
31 | "@vitejs/plugin-vue-jsx": "^3.0.0",
32 | "@vue/eslint-config-prettier": "^7.0.0",
33 | "@vue/eslint-config-typescript": "^11.0.0",
34 | "@vue/test-utils": "^2.2.6",
35 | "@vue/tsconfig": "^0.1.3",
36 | "autoprefixer": "^10.4.13",
37 | "eslint": "^8.22.0",
38 | "eslint-plugin-vue": "^9.3.0",
39 | "jsdom": "^20.0.3",
40 | "npm-run-all": "^4.1.5",
41 | "postcss": "^8.4.21",
42 | "prettier": "^2.7.1",
43 | "prettier-plugin-tailwindcss": "^0.2.2",
44 | "tailwindcss": "^3.2.4",
45 | "typescript": "~4.7.4",
46 | "vite": "^4.0.0",
47 | "vitest": "^0.25.6",
48 | "vue-tsc": "^1.0.12"
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/web/src/components/CodePreview.vue:
--------------------------------------------------------------------------------
1 |
43 |
44 |
45 |
47 | {{ previewContentHead }}
48 |
56 | {{ previewContentTail }}
60 |
61 |
62 |
63 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Code Things
2 | This social media application built using the Spin SDK is intended to be an in-depth reference or guide for Spin developers. It focuses on more common real-world use cases like CRUD APIs, Token auth, etc.
3 |
4 | ## SDK Requirements
5 | The following SDKs are required to build this application.
6 | - [Spin SDK](https://developer.fermyon.com/spin/install)
7 | - [rustup](https://rustup.rs)
8 | - [Node.js](https://nodejs.org)
9 |
10 | ## External Resources
11 | The following resources are required to run this application. For local development, these resources can be started via docker.
12 | - [PostreSQL](https://www.postgresql.org)
13 |
14 | ## Auth0 Setup
15 |
16 | To complete this setup for Fermyon Cloud, you must have run `spin deploy` at least once to capture the app's URL. For example, the application's URL in the following example is `https://code-things-xxx.fermyon.app`:
17 | ```
18 | % spin deploy
19 | Uploading code-things version 0.1.0+rcf68d278...
20 | Deploying...
21 | Waiting for application to become ready.......... ready
22 | Available Routes:
23 | web: https://code-things-xxx.fermyon.app (wildcard)
24 | profile-api: https://code-things-xxx.fermyon.app/api/profile (wildcard)
25 | ```
26 |
27 | 1. Sign up for Auth0 account (free)
28 | 2. Create a "Single Page Web" application
29 | a. Configure callback URLs: `http://127.0.0.1:3000, https://code-things-xxx.fermyon.app`
30 | b. Configure logout URLs: `http://127.0.0.1:3000, https://code-things-xxx.fermyon.app`
31 | c. Allowed web origins: `http://127.0.0.1:3000, https://code-things-xxx.fermyon.app`
32 | d. Add GitHub Connection
33 | 3. Create API
34 | a. Name: 'Code Things API'
35 | b. Identifier: `https://code-things-xxx.fermyon.app/`
36 | c. Signing Algorithm: `RS256`
37 | 4. Add the Auth0 configuration to Vue.js:
38 | a. Create a file at `./web/.env.local` (this is gitignored)
39 | b. Add domain: `VITE_AUTH0_DOMAIN = "dev-xxx.us.auth0.com"`
40 | c. Add client id: `VITE_AUTH0_CLIENT_ID = "xxx"`
41 | c. Add audience: `VITE_AUTH0_AUDIENCE = "https://code-things.fermyon.app/api"`
42 |
--------------------------------------------------------------------------------
/spin.toml:
--------------------------------------------------------------------------------
1 | spin_version = "1"
2 | authors = ["Justin Pflueger "]
3 | description = "Social media app for code snippets"
4 | name = "code-things"
5 | trigger = { type = "http", base = "/" }
6 | version = "0.1.0"
7 |
8 | [variables]
9 | db_url = { default = "host=127.0.0.1 user=code-things password=password dbname=code_things" }
10 | auth_domain = { default = "dev-czhnnl8ikcojc040.us.auth0.com" }
11 | auth_audience = { default = "https://code-things.fermyon.app/api" }
12 | auth_max_validity_secs = { default = "86400" }
13 |
14 | [[component]]
15 | id = "web"
16 | source = "modules/spin_static_fs.wasm"
17 | environment = { FALLBACK_PATH = "index.html" }
18 | [[component.files]]
19 | source = "web/dist"
20 | destination = "/"
21 | [component.trigger]
22 | route = "/..."
23 | [component.build]
24 | command = "npm run build"
25 | workdir = "web"
26 |
27 | [[component]]
28 | id = "profile-api"
29 | source = "api/profile/target/wasm32-wasi/release/profile_api.wasm"
30 | allowed_http_hosts = [ "dev-czhnnl8ikcojc040.us.auth0.com", "code-things.us.auth0.com" ]
31 | key_value_stores = ["default"]
32 | [component.trigger]
33 | route = "/api/profile/..."
34 | [component.build]
35 | command = "cargo build --target wasm32-wasi --release"
36 | workdir = "api/profile"
37 | watch = ["api/profile/src/**/*.rs", "api/profile/Cargo.toml", "spin.toml"]
38 | [component.config]
39 | db_url = "{{ db_url }}"
40 | auth_domain = "{{ auth_domain }}"
41 | auth_audience = "{{ auth_audience }}"
42 | auth_max_validity_secs = "{{ auth_max_validity_secs }}"
43 |
44 | [[component]]
45 | id = "post"
46 | source = "api/post/main.wasm"
47 | allowed_http_hosts = [ "dev-czhnnl8ikcojc040.us.auth0.com", "code-things.us.auth0.com" ]
48 | key_value_stores = ["default"]
49 | [component.trigger]
50 | route = "/api/post/..."
51 | [component.build]
52 | command = "tinygo build -target=wasi -gc=leaking -no-debug -o main.wasm ."
53 | workdir = "./api/post"
54 | watch = ["api/post/*.go", "api/post/go.mod", "spin.toml"]
55 | [component.config]
56 | db_url = "{{ db_url }}"
57 | auth_domain = "{{ auth_domain }}"
58 | auth_audience = "{{ auth_audience }}"
59 | auth_max_validity_secs = "{{ auth_max_validity_secs }}"
--------------------------------------------------------------------------------
/web/src/components/DropdownMenu.vue:
--------------------------------------------------------------------------------
1 |
30 |
31 |
32 |
66 |
67 |
--------------------------------------------------------------------------------
/web/src/api/index.ts:
--------------------------------------------------------------------------------
1 | export interface Profile {
2 | id?: string;
3 | handle: string;
4 | avatar: string;
5 | }
6 |
7 | export const profile = {
8 | get: (token: string, id: string) =>
9 | fetch(`/api/profile/${id}`, {
10 | headers: {
11 | Authorization: `Bearer ${token}`,
12 | },
13 | }),
14 | create: (token: string, profile: Profile) =>
15 | fetch("/api/profile", {
16 | method: "POST",
17 | headers: {
18 | Authorization: `Bearer ${token}`,
19 | },
20 | body: JSON.stringify(profile),
21 | }),
22 | update: (token: string, profile: Profile) =>
23 | fetch(`/api/profile`, {
24 | method: "PUT",
25 | headers: {
26 | Authorization: `Bearer ${token}`,
27 | },
28 | body: JSON.stringify(profile),
29 | }),
30 | delete: (token: string, id: string) =>
31 | fetch(`/api/profile/${id}`, {
32 | method: "DELETE",
33 | headers: {
34 | Authorization: `Bearer ${token}`,
35 | },
36 | }),
37 | };
38 |
39 | export interface Post {
40 | id?: number;
41 | author_id: string;
42 | content: string;
43 | type: string;
44 | data: string;
45 | visibility: string;
46 | }
47 |
48 | export const posts = {
49 | get: (token: string, id: string) =>
50 | fetch(`/api/posts/${id}`, {
51 | headers: {
52 | Authorization: `Bearer ${token}`,
53 | },
54 | }),
55 | list: (token: string, limit: number, offset: number) =>
56 | fetch(`/api/posts?limit=${limit}&offset=${offset}`, {
57 | headers: {
58 | Authorization: `Bearer ${token}`,
59 | },
60 | }),
61 | create: (token: string, post: Post) =>
62 | fetch("/api/posts", {
63 | method: "POST",
64 | headers: {
65 | Authorization: `Bearer ${token}`,
66 | },
67 | body: JSON.stringify(post),
68 | }),
69 | update: (token: string, post: Post) =>
70 | fetch("/api/posts", {
71 | method: "PUT",
72 | headers: {
73 | Authorization: `Bearer ${token}`,
74 | },
75 | body: JSON.stringify(post),
76 | }),
77 | delete: (token: string, id: string) =>
78 | fetch(`/api/posts/${id}`, {
79 | method: "DELETE",
80 | headers: {
81 | Authorization: `Bearer ${token}`,
82 | },
83 | }),
84 | };
85 |
--------------------------------------------------------------------------------
/web/README.md:
--------------------------------------------------------------------------------
1 | # web
2 |
3 | This template should help get you started developing with Vue 3 in Vite.
4 |
5 | ## Recommended IDE Setup
6 |
7 | [VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin).
8 |
9 | ## Type Support for `.vue` Imports in TS
10 |
11 | TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin) to make the TypeScript language service aware of `.vue` types.
12 |
13 | If the standalone TypeScript plugin doesn't feel fast enough to you, Volar has also implemented a [Take Over Mode](https://github.com/johnsoncodehk/volar/discussions/471#discussioncomment-1361669) that is more performant. You can enable it by the following steps:
14 |
15 | 1. Disable the built-in TypeScript Extension
16 | 1) Run `Extensions: Show Built-in Extensions` from VSCode's command palette
17 | 2) Find `TypeScript and JavaScript Language Features`, right click and select `Disable (Workspace)`
18 | 2. Reload the VSCode window by running `Developer: Reload Window` from the command palette.
19 |
20 | ## Customize configuration
21 |
22 | See [Vite Configuration Reference](https://vitejs.dev/config/).
23 |
24 | ## Project Setup
25 |
26 | ```sh
27 | npm install
28 | ```
29 |
30 | ### Compile and Hot-Reload for Development
31 |
32 | ```sh
33 | npm run dev
34 | ```
35 |
36 | ### Type-Check, Compile and Minify for Production
37 |
38 | ```sh
39 | npm run build
40 | ```
41 |
42 | ### Run Unit Tests with [Vitest](https://vitest.dev/)
43 |
44 | ```sh
45 | npm run test:unit
46 | ```
47 |
48 | ### Run End-to-End Tests with [Playwright](https://playwright.dev)
49 |
50 | ```sh
51 | # Install browsers for the first run
52 | npx playwright install
53 |
54 | # When testing on CI, must build the project first
55 | npm run build
56 |
57 | # Runs the end-to-end tests
58 | npm run test:e2e
59 | # Runs the tests only on Chromium
60 | npm run test:e2e -- --project=chromium
61 | # Runs the tests of a specific file
62 | npm run test:e2e -- tests/example.spec.ts
63 | # Runs the tests in debug mode
64 | npm run test:e2e -- --debug
65 | ```
66 |
67 | ### Lint with [ESLint](https://eslint.org/)
68 |
69 | ```sh
70 | npm run lint
71 | ```
72 |
--------------------------------------------------------------------------------
/api/profile/src/auth.rs:
--------------------------------------------------------------------------------
1 | use anyhow::{bail, Result};
2 | use base64::{alphabet, engine, Engine as _};
3 | use jwt_simple::prelude::*;
4 | use serde::{Deserialize, Serialize};
5 | use spin_sdk::outbound_http;
6 |
7 | // base64 decoding should support URL safe with no padding and allow trailing bits for JWT tokens
8 | const BASE64_CONFIG: engine::GeneralPurposeConfig = engine::GeneralPurposeConfig::new()
9 | .with_decode_allow_trailing_bits(true)
10 | .with_decode_padding_mode(engine::DecodePaddingMode::RequireNone);
11 | const BASE64_ENGINE: engine::GeneralPurpose =
12 | engine::GeneralPurpose::new(&alphabet::URL_SAFE, BASE64_CONFIG);
13 |
14 | #[derive(Serialize, Deserialize, Debug)]
15 | pub(crate) struct JsonWebKey {
16 | #[serde(rename = "alg")]
17 | algorithm: String,
18 | #[serde(rename = "kty")]
19 | key_type: String,
20 | #[serde(rename = "use")]
21 | public_key_use: String,
22 | #[serde(rename = "n")]
23 | modulus: String,
24 | #[serde(rename = "e")]
25 | exponent: String,
26 | #[serde(rename = "kid")]
27 | identifier: String,
28 | #[serde(rename = "x5t")]
29 | thumbprint: String,
30 | #[serde(rename = "x5c")]
31 | chain: Vec,
32 | }
33 |
34 | impl JsonWebKey {
35 | //TODO: cache the public key after it's been computed
36 | pub fn to_rsa256_public_key(self) -> Result {
37 | let n = BASE64_ENGINE.decode(self.modulus)?;
38 | let e = BASE64_ENGINE.decode(self.exponent)?;
39 | Ok(RS256PublicKey::from_components(&n, &e)?.with_key_id(self.identifier.as_str()))
40 | }
41 | }
42 |
43 | #[derive(Serialize, Deserialize, Debug)]
44 | pub(crate) struct JsonWebKeySet {
45 | keys: Vec,
46 | }
47 |
48 | impl JsonWebKeySet {
49 | pub fn get(url: String) -> Result {
50 | let res = outbound_http::send_request(
51 | http::Request::builder().method("GET").uri(url).body(None)?,
52 | )?;
53 | let res_body = match res.body().as_ref() {
54 | Some(bytes) => bytes.slice(..),
55 | None => bytes::Bytes::default(),
56 | };
57 | Ok(serde_json::from_slice::(&res_body)?)
58 | }
59 |
60 | pub fn verify(
61 | self,
62 | token: &str,
63 | options: Option,
64 | ) -> Result> {
65 | for key in self.keys {
66 | let key = key.to_rsa256_public_key()?;
67 |
68 | // add a required key id verification to options
69 | let options = options.clone().map(|o| VerificationOptions {
70 | // ensure the token is validated by this key specifically
71 | required_key_id: key.key_id().to_owned(),
72 | ..o
73 | });
74 |
75 | let claims = key.verify_token::(token, options);
76 | if claims.is_ok() {
77 | return claims;
78 | }
79 | }
80 | bail!("No key in the set was able to verify the token.")
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/api/post/middleware.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "errors"
6 | "fmt"
7 | "net/http"
8 | "strconv"
9 |
10 | "github.com/MicahParks/keyfunc"
11 | "github.com/go-chi/chi/v5"
12 | "github.com/golang-jwt/jwt/v4"
13 | "github.com/golang-jwt/jwt/v4/request"
14 | )
15 |
16 | // Authorization Middleware
17 |
18 | type claimsCtxKey struct{}
19 |
20 | func TokenVerification(next http.Handler) http.Handler {
21 | return http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
22 | // ensure RS256 was used to sign the token
23 | parserOpts := []request.ParseFromRequestOption{
24 | request.WithParser(jwt.NewParser(jwt.WithValidMethods([]string{
25 | jwt.SigningMethodRS256.Alg(),
26 | }))),
27 | }
28 | token, err := request.ParseFromRequest(req, request.OAuth2Extractor, fetchAuthSigningKey, parserOpts...)
29 | if err != nil {
30 | if errors.Is(err, jwt.ValidationError{}) {
31 | // token parsed but was invalid
32 | renderUnauthorized(res, err)
33 | } else {
34 | // unable to parse or verify signing
35 | renderErrorResponse(res, err)
36 | }
37 | return
38 | }
39 |
40 | claims := token.Claims.(jwt.MapClaims)
41 | if !token.Valid {
42 | renderUnauthorized(res, fmt.Errorf("token not valid"))
43 | return
44 | }
45 |
46 | if !claims.VerifyIssuer(getIssuer(), true) {
47 | renderUnauthorized(res, jwt.ErrTokenInvalidIssuer)
48 | return
49 | }
50 |
51 | if !claims.VerifyAudience(getAudience(), true) {
52 | renderUnauthorized(res, jwt.ErrTokenInvalidAudience)
53 | return
54 | }
55 |
56 | ctx := context.WithValue(req.Context(), claimsCtxKey{}, claims)
57 | next.ServeHTTP(res, req.WithContext(ctx))
58 | })
59 | }
60 |
61 | func fetchAuthSigningKey(t *jwt.Token) (interface{}, error) {
62 | jwksUri := getJwksUri()
63 | if jwks, err := keyfunc.Get(jwksUri, keyfunc.Options{
64 | Client: NewHttpClient(),
65 | }); err != nil {
66 | return nil, err
67 | } else {
68 | return jwks.Keyfunc(t)
69 | }
70 | }
71 |
72 | // Post Model Middleware
73 |
74 | var postIdCtxKey string = "id"
75 |
76 | type postCtxKey struct{}
77 |
78 | func PostCtx(next http.Handler) http.Handler {
79 | return http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
80 | var post Post
81 | var err error
82 |
83 | if req.ContentLength > 0 && req.Header.Get("Content-Type") == "application/json" {
84 | if post, err = DecodePost(req.Body); err != nil {
85 | // parsing failed end the request here
86 | msg := fmt.Sprintf("Failed to parse the post from request body: %v\n", err)
87 | renderBadRequestResponse(res, msg)
88 | return
89 | }
90 | if err = post.Validate(); err != nil {
91 | msg := fmt.Sprintf("Request body failed validation: %v\n", err)
92 | renderBadRequestResponse(res, msg)
93 | return
94 | }
95 | }
96 |
97 | ctx := context.WithValue(req.Context(), postCtxKey{}, post)
98 | next.ServeHTTP(res, req.WithContext(ctx))
99 | })
100 | }
101 |
102 | func getPostId(r *http.Request) (int, error) {
103 | idStr := chi.URLParam(r, postIdCtxKey)
104 | return strconv.Atoi(idStr)
105 | }
106 |
--------------------------------------------------------------------------------
/api/post/utils.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "net/http"
7 | "strconv"
8 | "time"
9 |
10 | spinhttp "github.com/fermyon/spin/sdk/go/http"
11 | "github.com/golang-jwt/jwt/v4"
12 | )
13 |
14 | // HTTP Response Helpers
15 |
16 | func renderBadRequestResponse(res http.ResponseWriter, msg string) {
17 | http.Error(res, msg, http.StatusBadRequest)
18 | }
19 |
20 | func renderErrorResponse(res http.ResponseWriter, err error) {
21 | //TODO: does this work if the response has already been partially written to?
22 | http.Error(res, err.Error(), http.StatusInternalServerError)
23 | }
24 |
25 | func renderForbiddenResponse(res http.ResponseWriter) {
26 | // intentionally make this one obscure in case of malicious intent
27 | http.Error(res, "Forbidden: You do not have permissions to perform this action", http.StatusForbidden)
28 | }
29 |
30 | func renderNotFound(res http.ResponseWriter) {
31 | http.Error(res, "Not found", http.StatusNotFound)
32 | }
33 |
34 | func renderUnauthorized(res http.ResponseWriter, err error) {
35 | http.Error(res, err.Error(), http.StatusUnauthorized)
36 | }
37 |
38 | func renderJsonResponse(res http.ResponseWriter, body any) {
39 | if err := json.NewEncoder(res).Encode(body); err != nil {
40 | renderErrorResponse(res, err)
41 | } else {
42 | res.Header().Add("Content-Type", "application/json")
43 | }
44 | }
45 |
46 | // Pagination Helpers
47 |
48 | func getPaginationParams(req *http.Request) (limit int, offset int) {
49 | // helper function to clamp the value
50 | clamp := func(val int, min int, max int) int {
51 | if val < min {
52 | return min
53 | } else if val > max {
54 | return max
55 | } else {
56 | return val
57 | }
58 | }
59 |
60 | // get the limit from the URL
61 | limit_param := req.URL.Query().Get("limit")
62 | if limit_val, err := strconv.Atoi(limit_param); err != nil {
63 | // error occurred, just use a default value
64 | fmt.Printf("Failed to parse the limit from URL: %v\n", err)
65 | limit = 5
66 | } else {
67 | // clamp the value in case of invalid parameters (intentional or otherwise)
68 | limit = clamp(limit_val, 0, 25)
69 | }
70 |
71 | // get the offset from the URL
72 | offset_param := req.URL.Query().Get("offset")
73 | if offset_val, err := strconv.Atoi(offset_param); err != nil {
74 | // error occurred, just use a default value
75 | fmt.Printf("Failed to parse the offset from URL: %v\n", err)
76 | offset = 0
77 | } else {
78 | // clamp the value in case of invalid parameters (intentional or otherwise)
79 | // limiting this one to 10,000 because I find it unlikely that anyone will post 10k times :)
80 | offset = clamp(offset_val, 0, 10000)
81 | }
82 |
83 | return limit, offset
84 | }
85 |
86 | // Auth Helpers
87 |
88 | func getUserId(req *http.Request) string {
89 | claims := req.Context().Value(claimsCtxKey{}).(jwt.MapClaims)
90 | return claims["sub"].(string)
91 | }
92 |
93 | // HTTP Client
94 | //TODO: this could be contributed to spin go sdk
95 |
96 | type spinRoundTripper struct{}
97 |
98 | func (t spinRoundTripper) RoundTrip(req *http.Request) (resp *http.Response, err error) {
99 | return spinhttp.Send(req)
100 | }
101 |
102 | func NewHttpClient() *http.Client {
103 | return &http.Client{
104 | Transport: spinRoundTripper{},
105 | Timeout: time.Duration(5) * time.Second,
106 | }
107 | }
108 |
--------------------------------------------------------------------------------
/api/post/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "net/http"
6 |
7 | spinhttp "github.com/fermyon/spin/sdk/go/http"
8 | "github.com/go-chi/chi/v5"
9 | )
10 |
11 | func main() {}
12 |
13 | func init() {
14 | spinhttp.Handle(func(res http.ResponseWriter, req *http.Request) {
15 | // we need to setup the router inside spin handler
16 | router := chi.NewRouter()
17 |
18 | router.Use(TokenVerification)
19 |
20 | // mount our routes using the prefix
21 | routePrefix := req.Header.Get("Spin-Component-Route")
22 | router.Mount(routePrefix, PostRouter())
23 |
24 | // hand the request/response off to chi
25 | router.ServeHTTP(res, req)
26 | })
27 | }
28 |
29 | // TODO: create wrapper function to handle errors?
30 | func PostRouter() chi.Router {
31 | posts := chi.NewRouter()
32 | idParamPattern := fmt.Sprintf("/{%v:[0-9]+}", postIdCtxKey)
33 | posts.Use(PostCtx)
34 | posts.Post("/", createPost)
35 | posts.Get("/", listPosts)
36 | posts.Get(idParamPattern, readPost)
37 | posts.Put(idParamPattern, updatePost)
38 | posts.Delete(idParamPattern, deletePost)
39 | return posts
40 | }
41 |
42 | func createPost(res http.ResponseWriter, req *http.Request) {
43 | post := req.Context().Value(postCtxKey{}).(Post)
44 |
45 | if getUserId(req) != post.AuthorID {
46 | renderForbiddenResponse(res)
47 | return
48 | }
49 |
50 | if err := DbInsert(&post); err != nil {
51 | renderErrorResponse(res, err)
52 | return
53 | }
54 |
55 | res.WriteHeader(http.StatusCreated)
56 | res.Header().Add("location", fmt.Sprintf("/api/post/%v", post.ID))
57 | renderJsonResponse(res, post)
58 | }
59 |
60 | func listPosts(res http.ResponseWriter, req *http.Request) {
61 | limit, offset := getPaginationParams(req)
62 | authorId := getUserId(req)
63 |
64 | if posts, err := DbReadAll(limit, offset, authorId); err != nil {
65 | renderErrorResponse(res, err)
66 | } else {
67 | renderJsonResponse(res, posts)
68 | }
69 | }
70 |
71 | func readPost(res http.ResponseWriter, req *http.Request) {
72 | id, err := getPostId(req)
73 | if err != nil {
74 | msg := fmt.Sprintf("Failed to parse URL param 'id': %v", id)
75 | renderBadRequestResponse(res, msg)
76 | return
77 | }
78 |
79 | authorId := getUserId(req)
80 |
81 | post, err := DbReadById(id, authorId)
82 | if err != nil {
83 | renderErrorResponse(res, err)
84 | return
85 | }
86 | if (post == Post{}) {
87 | renderNotFound(res)
88 | return
89 | }
90 |
91 | renderJsonResponse(res, post)
92 | }
93 |
94 | func updatePost(res http.ResponseWriter, req *http.Request) {
95 | post := req.Context().Value(postCtxKey{}).(Post)
96 |
97 | if id, err := getPostId(req); err != nil {
98 | msg := fmt.Sprintf("Failed to parse URL param 'id': %v", id)
99 | renderBadRequestResponse(res, msg)
100 | return
101 | } else {
102 | post.ID = id
103 | }
104 |
105 | if err := DbUpdate(post); err != nil {
106 | renderErrorResponse(res, err)
107 | }
108 | renderJsonResponse(res, post)
109 | }
110 |
111 | func deletePost(res http.ResponseWriter, req *http.Request) {
112 | id, err := getPostId(req)
113 | if err != nil {
114 | msg := fmt.Sprintf("Failed to parse URL param 'id': %v", id)
115 | renderBadRequestResponse(res, msg)
116 | return
117 | }
118 |
119 | authorId := getUserId(req)
120 |
121 | if err := DbDelete(id, authorId); err != nil {
122 | renderErrorResponse(res, err)
123 | }
124 | res.WriteHeader(http.StatusNoContent)
125 | }
126 |
--------------------------------------------------------------------------------
/web/playwright.config.ts:
--------------------------------------------------------------------------------
1 | import type { PlaywrightTestConfig } from "@playwright/test";
2 | import { devices } from "@playwright/test";
3 |
4 | /**
5 | * Read environment variables from file.
6 | * https://github.com/motdotla/dotenv
7 | */
8 | // require('dotenv').config();
9 |
10 | /**
11 | * See https://playwright.dev/docs/test-configuration.
12 | */
13 | const config: PlaywrightTestConfig = {
14 | testDir: "./e2e",
15 | /* Maximum time one test can run for. */
16 | timeout: 30 * 1000,
17 | expect: {
18 | /**
19 | * Maximum time expect() should wait for the condition to be met.
20 | * For example in `await expect(locator).toHaveText();`
21 | */
22 | timeout: 5000,
23 | },
24 | /* Fail the build on CI if you accidentally left test.only in the source code. */
25 | forbidOnly: !!process.env.CI,
26 | /* Retry on CI only */
27 | retries: process.env.CI ? 2 : 0,
28 | /* Opt out of parallel tests on CI. */
29 | workers: process.env.CI ? 1 : undefined,
30 | /* Reporter to use. See https://playwright.dev/docs/test-reporters */
31 | reporter: "html",
32 | /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
33 | use: {
34 | /* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */
35 | actionTimeout: 0,
36 | /* Base URL to use in actions like `await page.goto('/')`. */
37 | baseURL: "http://localhost:5173",
38 |
39 | /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
40 | trace: "on-first-retry",
41 |
42 | /* Only on CI systems run the tests headless */
43 | headless: !!process.env.CI,
44 | },
45 |
46 | /* Configure projects for major browsers */
47 | projects: [
48 | {
49 | name: "chromium",
50 | use: {
51 | ...devices["Desktop Chrome"],
52 | },
53 | },
54 | {
55 | name: "firefox",
56 | use: {
57 | ...devices["Desktop Firefox"],
58 | },
59 | },
60 | {
61 | name: "webkit",
62 | use: {
63 | ...devices["Desktop Safari"],
64 | },
65 | },
66 |
67 | /* Test against mobile viewports. */
68 | // {
69 | // name: 'Mobile Chrome',
70 | // use: {
71 | // ...devices['Pixel 5'],
72 | // },
73 | // },
74 | // {
75 | // name: 'Mobile Safari',
76 | // use: {
77 | // ...devices['iPhone 12'],
78 | // },
79 | // },
80 |
81 | /* Test against branded browsers. */
82 | // {
83 | // name: 'Microsoft Edge',
84 | // use: {
85 | // channel: 'msedge',
86 | // },
87 | // },
88 | // {
89 | // name: 'Google Chrome',
90 | // use: {
91 | // channel: 'chrome',
92 | // },
93 | // },
94 | ],
95 |
96 | /* Folder for test artifacts such as screenshots, videos, traces, etc. */
97 | // outputDir: 'test-results/',
98 |
99 | /* Run your local dev server before starting the tests */
100 | webServer: {
101 | /**
102 | * Use the dev server by default for faster feedback loop.
103 | * Use the preview server on CI for more realistic testing.
104 | Playwright will re-use the local server if there is already a dev-server running.
105 | */
106 | command: process.env.CI ? "vite preview --port 5173" : "vite dev",
107 | port: 5173,
108 | reuseExistingServer: !process.env.CI,
109 | },
110 | };
111 |
112 | export default config;
113 |
--------------------------------------------------------------------------------
/web/src/views/posts/MyPostsView.vue:
--------------------------------------------------------------------------------
1 |
78 |
79 |
80 |
81 |
82 |
My Posts
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 | {{ post.content }}
91 |
92 |
93 |
98 |
99 |
100 |
101 |
102 |
103 |
--------------------------------------------------------------------------------
/web/src/assets/logo-black.svg:
--------------------------------------------------------------------------------
1 |
2 |
4 |
61 |
--------------------------------------------------------------------------------
/web/src/assets/logo-white.svg:
--------------------------------------------------------------------------------
1 |
2 |
4 |
61 |
--------------------------------------------------------------------------------
/api/post/model.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "encoding/json"
5 | "errors"
6 | "fmt"
7 | "io"
8 | "strings"
9 | )
10 |
11 | // Post model
12 |
13 | type Post struct {
14 | ID int `json:"id,omitempty"` // auto-incremented by postgres
15 | AuthorID string `json:"author_id,omitempty"` // foreign key to user's id
16 | Content string `json:"content,omitempty"` // anything the poster wants to say about a piece of code they're sharing
17 | Type PostType `json:"type,omitempty"` // post could be a permalink, pasted code, gist, etc.
18 | Data string `json:"data,omitempty"` // actual permalink, code, gist link, etc.
19 | Visibility PostVisibility `json:"visibility,omitempty"` // basic visibility of public, followers, etc.
20 | }
21 |
22 | func (p Post) Validate() error {
23 | var errs []error
24 |
25 | if p.AuthorID == "" {
26 | errs = append(errs, fmt.Errorf("field 'author_id' is required"))
27 | }
28 |
29 | if p.Content == "" {
30 | errs = append(errs, fmt.Errorf("field 'content' is required"))
31 | }
32 |
33 | if p.Type == 0 {
34 | errs = append(errs, fmt.Errorf("field 'type' contains unknown value"))
35 | }
36 |
37 | if p.Data == "" {
38 | errs = append(errs, fmt.Errorf("field 'data' is required"))
39 | }
40 |
41 | if p.Visibility == 0 {
42 | errs = append(errs, fmt.Errorf("field 'visibility' contains unknown value"))
43 | }
44 |
45 | return errors.Join(errs...)
46 | }
47 |
48 | // Post Type enum
49 |
50 | type PostType uint8
51 |
52 | const (
53 | PostTypePermalinkRange PostType = iota + 1
54 | )
55 |
56 | var (
57 | PostType_name = map[PostType]string{
58 | PostTypePermalinkRange: "permalink-range",
59 | }
60 | PostType_value = map[string]PostType{
61 | "permalink-range": PostTypePermalinkRange,
62 | }
63 | )
64 |
65 | func (t PostType) String() string {
66 | return PostType_name[t]
67 | }
68 |
69 | func ParsePostType(s string) (PostType, error) {
70 | s = strings.TrimSpace(strings.ToLower(s))
71 | value, ok := PostType_value[s]
72 | if !ok {
73 | return PostType(0), fmt.Errorf("%q is not a valid post type", s)
74 | }
75 | return PostType(value), nil
76 | }
77 |
78 | func (t PostType) MarshalJSON() ([]byte, error) {
79 | return json.Marshal(t.String())
80 | }
81 |
82 | func (t *PostType) UnmarshalJSON(data []byte) (err error) {
83 | var postType string
84 | if err := json.Unmarshal(data, &postType); err != nil {
85 | return err
86 | }
87 | if *t, err = ParsePostType(postType); err != nil {
88 | return err
89 | }
90 | return nil
91 | }
92 |
93 | // Post Visibility enum
94 |
95 | type PostVisibility uint8
96 |
97 | const (
98 | PostVisibilityPublic PostVisibility = iota + 1
99 | PostVisibilityFollowers
100 | )
101 |
102 | var (
103 | PostVisibility_name = map[PostVisibility]string{
104 | PostVisibilityPublic: "public",
105 | PostVisibilityFollowers: "followers",
106 | }
107 | PostVisibility_value = map[string]PostVisibility{
108 | "public": PostVisibilityPublic,
109 | "followers": PostVisibilityFollowers,
110 | }
111 | )
112 |
113 | func (v PostVisibility) String() string {
114 | return PostVisibility_name[v]
115 | }
116 |
117 | func ParsePostVisibility(s string) (PostVisibility, error) {
118 | s = strings.TrimSpace(strings.ToLower(s))
119 | value, ok := PostVisibility_value[s]
120 | if !ok {
121 | return PostVisibility(0), fmt.Errorf("%q is not a valid post visibility", s)
122 | }
123 | return PostVisibility(value), nil
124 | }
125 |
126 | func (v PostVisibility) MarshalJSON() ([]byte, error) {
127 | return json.Marshal(v.String())
128 | }
129 |
130 | func (v *PostVisibility) UnmarshalJSON(data []byte) (err error) {
131 | var visibility string
132 | if err := json.Unmarshal(data, &visibility); err != nil {
133 | return err
134 | }
135 | if *v, err = ParsePostVisibility(visibility); err != nil {
136 | return err
137 | }
138 | return nil
139 | }
140 |
141 | // JSON helpers
142 |
143 | func DecodePost(r io.ReadCloser) (Post, error) {
144 | decoder := json.NewDecoder(r)
145 | var p Post
146 | err := decoder.Decode(&p)
147 | return p, err
148 | }
149 |
--------------------------------------------------------------------------------
/web/src/views/posts/NewPostView.vue:
--------------------------------------------------------------------------------
1 |
42 |
43 |
44 |
51 |
52 |
53 |
54 |
55 |
56 |
57 | Link
60 | Code
63 |
64 |
65 |
66 |
113 |
114 | Code Panel
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
--------------------------------------------------------------------------------
/web/src/views/ProfileView.vue:
--------------------------------------------------------------------------------
1 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
Profile
63 |
64 | This information will be displayed publicly so be careful what you
65 | share.
66 |
67 |
68 |
69 |
70 |
120 |
121 |
122 |
123 |
124 |
125 |
--------------------------------------------------------------------------------
/api/profile/src/model.rs:
--------------------------------------------------------------------------------
1 | use std::collections::HashMap;
2 |
3 | use anyhow::{anyhow, Result};
4 | use bytes::Bytes;
5 | use http::HeaderMap;
6 | use serde::{Deserialize, Serialize};
7 |
8 | use spin_sdk::pg::{self as db, Column, Decode, ParameterValue, Row};
9 |
10 | fn as_param<'a>(value: &'a Option) -> Option> {
11 | match value {
12 | Some(value) => Some(ParameterValue::Str(value.as_str())),
13 | None => None,
14 | }
15 | }
16 |
17 | fn as_nullable_param<'a>(value: &'a Option) -> ParameterValue<'a> {
18 | match as_param(value) {
19 | Some(value) => value,
20 | None => ParameterValue::DbNull,
21 | }
22 | }
23 |
24 | fn get_column_lookup<'a>(columns: &'a Vec) -> HashMap<&'a str, usize> {
25 | columns
26 | .iter()
27 | .enumerate()
28 | .map(|(i, c)| (c.name.as_str(), i))
29 | .collect::>()
30 | }
31 |
32 | fn get_params_from_route(route: &str) -> Vec {
33 | route
34 | .split('/')
35 | .flat_map(|s| if s == "" { None } else { Some(s.to_string()) })
36 | .collect::>()
37 | }
38 |
39 | fn get_last_param_from_route(route: &str) -> Option {
40 | get_params_from_route(route).last().cloned()
41 | }
42 |
43 | #[derive(Serialize, Deserialize, Debug)]
44 | pub(crate) struct Profile {
45 | pub id: Option,
46 | pub handle: String,
47 | pub avatar: Option,
48 | }
49 |
50 | impl Profile {
51 | pub(crate) fn from_path(headers: &HeaderMap) -> Result {
52 | let header = headers
53 | .get("spin-path-info")
54 | .ok_or(anyhow!("Error: Failed to discover path"))?;
55 | let path = header.to_str()?;
56 | match get_last_param_from_route(path) {
57 | Some(handle) => Ok(Profile {
58 | id: None,
59 | handle: handle,
60 | avatar: None,
61 | }),
62 | None => Err(anyhow!("Failed to parse handle from path")),
63 | }
64 | }
65 |
66 | pub(crate) fn from_bytes(b: &Bytes) -> Result {
67 | Ok(serde_json::from_slice(&b)?)
68 | }
69 |
70 | pub(crate) fn with_id(mut self, id: Option) -> Self {
71 | self.id = id;
72 | self
73 | }
74 |
75 | fn from_row(row: &Row, columns: &HashMap<&str, usize>) -> Result {
76 | let id = String::decode(&row[columns["id"]]).ok();
77 | let handle = String::decode(&row[columns["handle"]])?;
78 | let avatar = String::decode(&row[columns["avatar"]]).ok();
79 | Ok(Profile { id, handle, avatar })
80 | }
81 |
82 | pub(crate) fn insert(&self, db_url: &str) -> Result<()> {
83 | let params = vec![
84 | as_param(&self.id).ok_or(anyhow!("The id field is currently required for insert"))?,
85 | ParameterValue::Str(&self.handle),
86 | match as_param(&self.avatar) {
87 | Some(p) => p,
88 | None => ParameterValue::DbNull,
89 | },
90 | ];
91 | match db::execute(
92 | db_url,
93 | "INSERT INTO profiles (id, handle, avatar) VALUES ($1, $2, $3)",
94 | ¶ms,
95 | ) {
96 | Ok(_) => Ok(()),
97 | Err(e) => Err(anyhow!("Inserting profile failed: {:?}", e)),
98 | }
99 | }
100 |
101 | pub(crate) fn get_by_id(id: &str, db_url: &str) -> Result {
102 | let params = vec![ParameterValue::Str(id)];
103 | let row_set = match db::query(
104 | db_url,
105 | "SELECT id, handle, avatar from profiles WHERE id = $1",
106 | ¶ms,
107 | ) {
108 | Ok(row_set) => row_set,
109 | Err(e) => return Err(anyhow!("Failed to get profile by id '{:?}': {:?}", id, e)),
110 | };
111 |
112 | let columns = get_column_lookup(&row_set.columns);
113 |
114 | match row_set.rows.first() {
115 | Some(row) => Profile::from_row(row, &columns),
116 | None => Err(anyhow!("Profile not found for id '{:?}'", id)),
117 | }
118 | }
119 |
120 | pub(crate) fn update(&self, db_url: &str) -> Result<()> {
121 | let params = vec![
122 | ParameterValue::Str(&self.handle),
123 | as_nullable_param(&self.avatar),
124 | as_param(&self.id).ok_or(anyhow!("The id field is currently required for update"))?,
125 | ];
126 | match db::execute(
127 | db_url,
128 | "UPDATE profiles SET handle=$1, avatar=$2 WHERE id=$3",
129 | ¶ms,
130 | ) {
131 | Ok(_) => Ok(()),
132 | Err(e) => Err(anyhow!("Updating profile failed: {:?}", e)),
133 | }
134 | }
135 |
136 | pub(crate) fn delete_by_id(id: &str, db_url: &str) -> Result<()> {
137 | let params = vec![ParameterValue::Str(id)];
138 | match db::execute(db_url, "DELETE FROM profiles WHERE id=$1", ¶ms) {
139 | Ok(_) => Ok(()),
140 | Err(e) => Err(anyhow!("Deleting profile failed: {:?}", e)),
141 | }
142 | }
143 | }
144 |
--------------------------------------------------------------------------------
/api/post/data.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/fermyon/spin/sdk/go/postgres"
7 | )
8 |
9 | // Database Operations
10 |
11 | func DbInsert(post *Post) error {
12 | db_url := getDbUrl()
13 | statement := "INSERT INTO posts (author_id, content, type, data, visibility) VALUES ($1, $2, $3, $4, $5)"
14 | params := []postgres.ParameterValue{
15 | postgres.ParameterValueStr(post.AuthorID),
16 | postgres.ParameterValueStr(post.Content),
17 | postgres.ParameterValueStr(post.Type.String()),
18 | postgres.ParameterValueStr(post.Data),
19 | postgres.ParameterValueStr(post.Visibility.String()),
20 | }
21 |
22 | _, err := postgres.Execute(db_url, statement, params)
23 | if err != nil {
24 | return fmt.Errorf("Error inserting into database: %s", err.Error())
25 | }
26 |
27 | // this is a gross hack that will surely bite me later
28 | rowset, err := postgres.Query(db_url, "SELECT lastval()", []postgres.ParameterValue{})
29 | if err != nil || len(rowset.Rows) != 1 || len(rowset.Rows[0]) != 1 {
30 | return fmt.Errorf("Error querying id from database: %s", err.Error())
31 | }
32 |
33 | id_val := rowset.Rows[0][0]
34 | if id_val.Kind() == postgres.DbValueKindInt64 {
35 | post.ID = int(id_val.GetInt64())
36 | } else {
37 | fmt.Printf("Failed to populate created post's identifier, invalid kind returned from database: %v\n", id_val.Kind())
38 | }
39 |
40 | return nil
41 | }
42 |
43 | func DbReadById(id int, authorId string) (Post, error) {
44 | db_url := getDbUrl()
45 | statement := "SELECT id, author_id, content, type, data, visibility FROM posts WHERE id=$1 and author_id=$2"
46 | params := []postgres.ParameterValue{
47 | postgres.ParameterValueInt32(int32(id)),
48 | postgres.ParameterValueStr(authorId),
49 | }
50 |
51 | rowset, err := postgres.Query(db_url, statement, params)
52 | if err != nil {
53 | return Post{}, fmt.Errorf("Error reading from database: %s", err.Error())
54 | }
55 |
56 | if rowset.Rows == nil || len(rowset.Rows) == 0 {
57 | return Post{}, nil
58 | } else {
59 | return fromRow(rowset.Rows[0])
60 | }
61 | }
62 |
63 | func DbReadAll(limit int, offset int, authorId string) ([]Post, error) {
64 | db_url := getDbUrl()
65 | statement := "SELECT id, author_id, content, type, data, visibility FROM posts WHERE author_id=$3 ORDER BY id LIMIT $1 OFFSET $2"
66 | params := []postgres.ParameterValue{
67 | postgres.ParameterValueInt64(int64(limit)),
68 | postgres.ParameterValueInt64(int64(offset)),
69 | postgres.ParameterValueStr(authorId),
70 | }
71 | rowset, err := postgres.Query(db_url, statement, params)
72 | if err != nil {
73 | return []Post{}, fmt.Errorf("Error reading from database: %s", err.Error())
74 | }
75 |
76 | posts := make([]Post, len(rowset.Rows))
77 | for i, row := range rowset.Rows {
78 | if post, err := fromRow(row); err != nil {
79 | return []Post{}, err
80 | } else {
81 | posts[i] = post
82 | }
83 | }
84 |
85 | return posts, nil
86 | }
87 |
88 | func DbUpdate(post Post) error {
89 | db_url := getDbUrl()
90 | statement := "UPDATE posts SET content=$1, type=$2, data=$3, visibility=$4 WHERE id=$5"
91 | params := []postgres.ParameterValue{
92 | postgres.ParameterValueStr(post.Content),
93 | postgres.ParameterValueStr(post.Type.String()),
94 | postgres.ParameterValueStr(post.Data),
95 | postgres.ParameterValueStr(post.Visibility.String()),
96 | postgres.ParameterValueInt32(int32(post.ID)),
97 | }
98 |
99 | _, err := postgres.Execute(db_url, statement, params)
100 | if err != nil {
101 | return fmt.Errorf("Error updating database: %s", err.Error())
102 | }
103 |
104 | return nil
105 | }
106 |
107 | func DbDelete(id int, authorId string) error {
108 | db_url := getDbUrl()
109 | statement := "DELETE FROM posts WHERE id=$1 and author_id=$2"
110 | params := []postgres.ParameterValue{
111 | postgres.ParameterValueInt32(int32(id)),
112 | postgres.ParameterValueStr(authorId),
113 | }
114 |
115 | _, err := postgres.Execute(db_url, statement, params)
116 | return err
117 | }
118 |
119 | // Database Helper Functions
120 |
121 | func assertValueKind(row []postgres.DbValue, col int, expected postgres.DbValueKind) (postgres.DbValue, error) {
122 | if row[col].Kind() != expected {
123 | return postgres.DbValue{}, fmt.Errorf("Expected column %v to be %v kind but got %v\n", col, expected, row[col].Kind())
124 | }
125 | return row[col], nil
126 | }
127 |
128 | func fromRow(row []postgres.DbValue) (Post, error) {
129 | var post Post
130 |
131 | if val, err := assertValueKind(row, 0, postgres.DbValueKindInt32); err != nil {
132 | return post, err
133 | } else {
134 | post.ID = int(val.GetInt32())
135 | }
136 |
137 | if val, err := assertValueKind(row, 1, postgres.DbValueKindStr); err != nil {
138 | return post, err
139 | } else {
140 | post.AuthorID = val.GetStr()
141 | }
142 |
143 | if val, err := assertValueKind(row, 2, postgres.DbValueKindStr); err != nil {
144 | return post, err
145 | } else {
146 | post.Content = val.GetStr()
147 | }
148 |
149 | if val, err := assertValueKind(row, 3, postgres.DbValueKindStr); err != nil {
150 | return post, err
151 | } else if val, err := ParsePostType(val.GetStr()); err != nil {
152 | return post, err
153 | } else {
154 | post.Type = val
155 | }
156 |
157 | if val, err := assertValueKind(row, 4, postgres.DbValueKindStr); err != nil {
158 | return post, err
159 | } else {
160 | post.Data = val.GetStr()
161 | }
162 |
163 | if val, err := assertValueKind(row, 5, postgres.DbValueKindStr); err != nil {
164 | return post, err
165 | } else if val, err := ParsePostVisibility(val.GetStr()); err != nil {
166 | return post, err
167 | } else {
168 | post.Visibility = val
169 | }
170 |
171 | return post, nil
172 | }
173 |
--------------------------------------------------------------------------------
/web/tailwind.config.js:
--------------------------------------------------------------------------------
1 | const defaultColors = require('tailwindcss/colors');
2 |
3 | /*
4 | * NOTE: shades were generated using https://www.tailwindshades.com
5 | */
6 | const brandColors = {
7 | // core brand colors
8 | // this will effectively replace the default color pallette
9 | seagreen: {
10 | DEFAULT: "#34E8BD", // styleguide / seagreen
11 | 50: "#D9FBF3",
12 | 100: "#C7F9ED",
13 | 200: "#A2F4E1",
14 | 300: "#7DF0D5",
15 | 400: "#59ECC9",
16 | 500: "#34E8BD",
17 | 600: "#17CDA1",
18 | 700: "#119A7A",
19 | 800: "#0C6852",
20 | 900: "#06362A",
21 | },
22 | oxfordblue: {
23 | DEFAULT: '#0D203F', // styleguide / oxfordblue
24 | '50': '#95B5E9',
25 | '100': '#84A9E6',
26 | '200': '#6291DF',
27 | '300': '#407AD8',
28 | '400': '#2965C6',
29 | '500': '#2254A4',
30 | '600': '#1B4283',
31 | '700': '#143161',
32 | '800': '#0D203F',
33 | '900': '#030810'
34 | },
35 | // secondary accent colors
36 | rust: {
37 | DEFAULT: '#EF946C', // styleguide / rust
38 | '50': '#F9D7C8',
39 | '100': '#F7CAB6',
40 | '200': '#F3AF91',
41 | '300': '#EF946C',
42 | '400': '#E96F39',
43 | '500': '#D45117',
44 | '600': '#A13D12',
45 | '700': '#6F2A0C',
46 | '800': '#3C1707',
47 | '900': '#090401'
48 | },
49 | lavender: {
50 | DEFAULT: '#BEA7E5', // styleguide / lavender
51 | '50': '#F8F6FC',
52 | '100': '#EDE6F8',
53 | '200': '#D5C6EE',
54 | '300': '#BEA7E5',
55 | '400': '#9E7CD8',
56 | '500': '#7E50CB',
57 | '600': '#6234B0',
58 | '700': '#4A2784',
59 | '800': '#321A59',
60 | '900': '#1A0E2E'
61 | },
62 | colablue: {
63 | DEFAULT: '#0E8FDD', // styleguide / colablue
64 | '50': '#A9DBFA',
65 | '100': '#96D3F8',
66 | '200': '#6FC3F6',
67 | '300': '#49B3F3',
68 | '400': '#23A3F1',
69 | '500': '#0E8FDD',
70 | '600': '#0B6DA8',
71 | '700': '#074B73',
72 | '800': '#04293F',
73 | '900': '#01060A'
74 | },
75 | // complimentary tones
76 | darkspace: {
77 | DEFAULT: '#213762', // styleguide / darkspace
78 | '50': '#AABDE2',
79 | '100': '#9BB1DD',
80 | '200': '#7C99D3',
81 | '300': '#5E82C9',
82 | '400': '#406ABE',
83 | '500': '#36599F',
84 | '600': '#2B4881',
85 | '700': '#213762',
86 | '800': '#131F38',
87 | '900': '#05080E'
88 | },
89 | darkocean: {
90 | DEFAULT: '#0A455A', // styleguide / darkocean
91 | '50': '#A1DFF5',
92 | '100': '#8FD8F3',
93 | '200': '#6ACCEE',
94 | '300': '#46BFEA',
95 | '400': '#21B2E6',
96 | '500': '#1699C8',
97 | '600': '#127DA3',
98 | '700': '#0E617F',
99 | '800': '#0A455A',
100 | '900': '#041E28'
101 | },
102 | darkolive: {
103 | DEFAULT: '#1F7A8C', // styleguide / darkolive
104 | '50': '#C3EAF2',
105 | '100': '#B2E4EE',
106 | '200': '#90D8E7',
107 | '300': '#6FCDDF',
108 | '400': '#4EC1D8',
109 | '500': '#2EB4CF',
110 | '600': '#2697AD',
111 | '700': '#1F7A8C',
112 | '800': '#15525E',
113 | '900': '#0B2A30'
114 | },
115 | darkplum: {
116 | DEFAULT: '#525776', // styleguide / darkplum
117 | '50': '#CCCFDC',
118 | '100': '#C0C3D4',
119 | '200': '#A8ACC3',
120 | '300': '#9095B2',
121 | '400': '#787EA1',
122 | '500': '#63698E',
123 | '600': '#525776',
124 | '700': '#3B3F55',
125 | '800': '#242634',
126 | '900': '#0D0E13'
127 | },
128 | midgreen: {
129 | DEFAULT: '#1FBCA0', // styleguide / midgreen
130 | '50': '#C6F6ED',
131 | '100': '#B4F3E8',
132 | '200': '#91EDDD',
133 | '300': '#6EE7D2',
134 | '400': '#4BE1C7',
135 | '500': '#28DCBC',
136 | '600': '#1FBCA0',
137 | '700': '#178C77',
138 | '800': '#0F5C4E',
139 | '900': '#072C25'
140 | },
141 | midblue: {
142 | DEFAULT: '#345995', // styleguide / midblue
143 | '50': '#C0D0E9',
144 | '100': '#B1C4E4',
145 | '200': '#93AED9',
146 | '300': '#7597CF',
147 | '400': '#5680C4',
148 | '500': '#3F6BB3',
149 | '600': '#345995',
150 | '700': '#25406B',
151 | '800': '#172742',
152 | '900': '#080E18'
153 | },
154 | lightplum: {
155 | DEFAULT: '#D3C3D9', // styleguide / lightplum
156 | '50': '#EEE8F1',
157 | '100': '#E5DCE9',
158 | '200': '#D3C3D9',
159 | '300': '#BAA1C3',
160 | '400': '#A17EAD',
161 | '500': '#865E95',
162 | '600': '#674973',
163 | '700': '#483351',
164 | '800': '#2A1D2E',
165 | '900': '#0B070C'
166 | },
167 | lightgrey: {
168 | '50': '#FBFAFC',
169 | '100': '#EFEFF5',
170 | '200': '#D9DBE8', //lightgrey
171 | '300': '#B6BFD3',
172 | '400': '#93A7BE',
173 | '500': '#7094A9',
174 | '600': '#55818C',
175 | '700': '#406869',
176 | '800': '#2A4642',
177 | '900': '#15231F'
178 | },
179 | lightlavendar: {
180 | DEFAULT: '#ECE5EE', // styleguide / lightlavendar
181 | '50': '#F5F1F6',
182 | '100': '#ECE5EE',
183 | '200': '#D3C3D8',
184 | '300': '#BAA1C2',
185 | '400': '#A27FAB',
186 | '500': '#876093',
187 | '600': '#684A71',
188 | '700': '#49344F',
189 | '800': '#291D2D',
190 | '900': '#0A070B'
191 | },
192 | lightlemon: {
193 | DEFAULT: '#F9F7EE', // styleguide / lightlemon
194 | '50': '#FEFEFD',
195 | '100': '#F9F7EE',
196 | '200': '#EAE3C5',
197 | '300': '#DCD09B',
198 | '400': '#CDBC72',
199 | '500': '#BEA948',
200 | '600': '#998736',
201 | '700': '#6F6227',
202 | '800': '#463E19',
203 | '900': '#1C190A'
204 | },
205 | };
206 |
207 | /** @type {import('tailwindcss').Config} */
208 | module.exports = {
209 | content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx,vue}"],
210 | theme: {
211 | colors: {
212 | // include all of our brand colors
213 | ...brandColors,
214 | // pass through default color aliases
215 | gray: brandColors.lightgrey, //TODO: either refactor the html to change colors or change the gray pallette here
216 | white: defaultColors.white,
217 | black: defaultColors.black,
218 | transparent: defaultColors.transparent,
219 | current: defaultColors.current,
220 | inherit: defaultColors.inherit,
221 | neutral: defaultColors.neutral,
222 | }
223 | },
224 | plugins: [
225 | require("@tailwindcss/forms"),
226 | require("prettier-plugin-tailwindcss"),
227 | ],
228 | };
229 |
--------------------------------------------------------------------------------
/api/profile/src/lib.rs:
--------------------------------------------------------------------------------
1 | mod auth;
2 | mod config;
3 | mod model;
4 |
5 | use anyhow::{anyhow, Context, Result};
6 | use bytes::Bytes;
7 | use config::Config;
8 | use jwt_simple::{
9 | claims::{JWTClaims, NoCustomClaims},
10 | prelude::VerificationOptions,
11 | };
12 | use model::Profile;
13 | use spin_sdk::{
14 | http::{Request, Response},
15 | http_component,
16 | };
17 |
18 | enum Api {
19 | Create(model::Profile),
20 | ReadById(String),
21 | Update(model::Profile),
22 | DeleteById(String),
23 | MethodNotAllowed,
24 | NotFound,
25 | }
26 |
27 | #[http_component]
28 | fn profile_api(req: Request) -> Result {
29 | let cfg = Config::default();
30 |
31 | // parse the profile from the request
32 | let method = req.method();
33 | let profile = match parse_profile(method, &req) {
34 | Ok(profile) => profile,
35 | Err(e) => return bad_request(e),
36 | };
37 |
38 | // guard against unauthenticated requests
39 | let claims = match claims_from_request(&cfg, &req, &profile.id) {
40 | Ok(claims) if claims.subject.is_some() => claims,
41 | Ok(_) => return forbidden("Token is missing 'sub'.".to_string()),
42 | Err(e) => return forbidden(e.to_string()),
43 | };
44 |
45 | // add the subject to the profile
46 | let profile = profile.with_id(claims.subject);
47 |
48 | // match api action to handler
49 | let result = match api_from_profile(method, profile) {
50 | Api::Create(profile) => handle_create(&cfg.db_url, profile),
51 | Api::Update(profile) => handle_update(&cfg.db_url, profile),
52 | Api::ReadById(id) => handle_read_by_id(&cfg.db_url, id),
53 | Api::DeleteById(id) => handle_delete_by_id(&cfg.db_url, id),
54 | Api::MethodNotAllowed => method_not_allowed(),
55 | Api::NotFound => not_found(),
56 | };
57 |
58 | match result {
59 | Ok(response) => Ok(response),
60 | Err(e) => internal_server_error(e.to_string()),
61 | }
62 | }
63 |
64 | fn claims_from_request(
65 | cfg: &Config,
66 | req: &Request,
67 | subject: &Option,
68 | ) -> Result> {
69 | let keys = auth::JsonWebKeySet::get(cfg.jwks_url.to_owned())
70 | .context(format!("Failed to retrieve JWKS from {:?}", cfg.jwks_url))?;
71 |
72 | let token = get_access_token(req.headers()).ok_or(anyhow!(
73 | "Failed to get access token from Authorization header"
74 | ))?;
75 |
76 | let options = VerificationOptions {
77 | max_validity: cfg.auth_max_validity,
78 | allowed_audiences: Some(cfg.auth_audiences.to_owned()),
79 | allowed_issuers: Some(cfg.auth_issuers.to_owned()),
80 | required_subject: subject.to_owned(),
81 | ..Default::default()
82 | };
83 |
84 | let claims = keys
85 | .verify(token, Some(options))
86 | .context("Failed to verify token")?;
87 |
88 | Ok(claims)
89 | }
90 |
91 | fn parse_profile(method: &http::Method, req: &Request) -> Result {
92 | // parse the data model from body or url
93 | let profile = match method {
94 | &http::Method::GET | &http::Method::DELETE => Profile::from_path(&req.headers()),
95 | &http::Method::PUT | &http::Method::POST => {
96 | Profile::from_bytes(req.body().as_ref().unwrap_or(&Bytes::new()))
97 | }
98 | _ => Err(anyhow!("Unsupported Http Method")),
99 | }?;
100 | Ok(profile)
101 | }
102 |
103 | fn api_from_profile(method: &http::Method, profile: Profile) -> Api {
104 | match (method, profile) {
105 | (&http::Method::POST, profile) => Api::Create(profile),
106 | (&http::Method::GET, profile) if profile.id.is_some() => Api::ReadById(profile.id.unwrap()),
107 | (&http::Method::GET, _) => Api::NotFound,
108 | (&http::Method::PUT, profile) => Api::Update(profile),
109 | (&http::Method::DELETE, profile) if profile.id.is_some() => {
110 | Api::DeleteById(profile.id.unwrap())
111 | }
112 | (&http::Method::DELETE, _) => Api::NotFound,
113 | _ => Api::MethodNotAllowed,
114 | }
115 | }
116 |
117 | fn handle_create(db_url: &str, model: Profile) -> Result {
118 | model.insert(db_url)?;
119 | Ok(http::Response::builder()
120 | .status(http::StatusCode::CREATED)
121 | .header(
122 | http::header::LOCATION,
123 | format!("/api/profile/{}", model.handle),
124 | )
125 | .body(None)?)
126 | }
127 |
128 | fn handle_read_by_id(db_url: &str, id: String) -> Result {
129 | match Profile::get_by_id(id.as_str(), &db_url) {
130 | Ok(model) => ok(serde_json::to_string(&model)?),
131 | Err(_) => not_found(),
132 | }
133 | }
134 |
135 | fn handle_update(db_url: &str, model: Profile) -> Result {
136 | model.update(&db_url)?;
137 | handle_read_by_id(&db_url, model.id.expect("Profile id is required"))
138 | }
139 |
140 | fn handle_delete_by_id(db_url: &str, id: String) -> Result {
141 | match Profile::delete_by_id(&id, &db_url) {
142 | Ok(_) => no_content(),
143 | Err(_) => internal_server_error(String::from("Error while deleting profile")),
144 | }
145 | }
146 |
147 | fn get_access_token(headers: &http::HeaderMap) -> Option<&str> {
148 | headers
149 | .get("Authorization")?
150 | .to_str()
151 | .unwrap()
152 | .strip_prefix("Bearer ")
153 | }
154 |
155 | fn internal_server_error(err: String) -> Result {
156 | Ok(http::Response::builder()
157 | .status(http::StatusCode::INTERNAL_SERVER_ERROR)
158 | .header(http::header::CONTENT_TYPE, "text/plain")
159 | .body(Some(err.into()))?)
160 | }
161 |
162 | fn ok(payload: String) -> Result {
163 | Ok(http::Response::builder()
164 | .status(http::StatusCode::OK)
165 | .header(http::header::CONTENT_TYPE, "application/json")
166 | .body(Some(payload.into()))?)
167 | }
168 |
169 | fn method_not_allowed() -> Result {
170 | quick_response(http::StatusCode::METHOD_NOT_ALLOWED)
171 | }
172 |
173 | fn bad_request(err: anyhow::Error) -> Result {
174 | Ok(http::Response::builder()
175 | .status(http::StatusCode::BAD_REQUEST)
176 | .body(Some(err.to_string().into()))?)
177 | }
178 |
179 | fn not_found() -> Result {
180 | quick_response(http::StatusCode::NOT_FOUND)
181 | }
182 |
183 | fn no_content() -> Result {
184 | quick_response(http::StatusCode::NO_CONTENT)
185 | }
186 |
187 | fn forbidden(reason: String) -> Result {
188 | Ok(http::Response::builder()
189 | .status(http::StatusCode::FORBIDDEN)
190 | .header(http::header::CONTENT_TYPE, "text/plain")
191 | .body(Some(reason.into()))?)
192 | }
193 |
194 | fn quick_response(s: http::StatusCode) -> Result {
195 | Ok(http::Response::builder().status(s).body(None)?)
196 | }
197 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [yyyy] [name of copyright owner]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/api/profile/Cargo.lock:
--------------------------------------------------------------------------------
1 | # This file is automatically @generated by Cargo.
2 | # It is not intended for manual editing.
3 | version = 3
4 |
5 | [[package]]
6 | name = "anyhow"
7 | version = "1.0.68"
8 | source = "registry+https://github.com/rust-lang/crates.io-index"
9 | checksum = "2cb2f989d18dd141ab8ae82f64d1a8cdd37e0840f73a406896cf5e99502fab61"
10 |
11 | [[package]]
12 | name = "async-trait"
13 | version = "0.1.61"
14 | source = "registry+https://github.com/rust-lang/crates.io-index"
15 | checksum = "705339e0e4a9690e2908d2b3d049d85682cf19fbd5782494498fbf7003a6a282"
16 | dependencies = [
17 | "proc-macro2",
18 | "quote",
19 | "syn",
20 | ]
21 |
22 | [[package]]
23 | name = "autocfg"
24 | version = "1.1.0"
25 | source = "registry+https://github.com/rust-lang/crates.io-index"
26 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
27 |
28 | [[package]]
29 | name = "base16ct"
30 | version = "0.1.1"
31 | source = "registry+https://github.com/rust-lang/crates.io-index"
32 | checksum = "349a06037c7bf932dd7e7d1f653678b2038b9ad46a74102f1fc7bd7872678cce"
33 |
34 | [[package]]
35 | name = "base64"
36 | version = "0.21.0"
37 | source = "registry+https://github.com/rust-lang/crates.io-index"
38 | checksum = "a4a4ddaa51a5bc52a6948f74c06d20aaaddb71924eab79b8c97a8c556e942d6a"
39 |
40 | [[package]]
41 | name = "base64ct"
42 | version = "1.5.3"
43 | source = "registry+https://github.com/rust-lang/crates.io-index"
44 | checksum = "b645a089122eccb6111b4f81cbc1a49f5900ac4666bb93ac027feaecf15607bf"
45 |
46 | [[package]]
47 | name = "binstring"
48 | version = "0.1.1"
49 | source = "registry+https://github.com/rust-lang/crates.io-index"
50 | checksum = "7e0d60973d9320722cb1206f412740e162a33b8547ea8d6be75d7cff237c7a85"
51 |
52 | [[package]]
53 | name = "bitflags"
54 | version = "1.3.2"
55 | source = "registry+https://github.com/rust-lang/crates.io-index"
56 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
57 |
58 | [[package]]
59 | name = "block-buffer"
60 | version = "0.10.3"
61 | source = "registry+https://github.com/rust-lang/crates.io-index"
62 | checksum = "69cce20737498f97b993470a6e536b8523f0af7892a4f928cceb1ac5e52ebe7e"
63 | dependencies = [
64 | "generic-array",
65 | ]
66 |
67 | [[package]]
68 | name = "bumpalo"
69 | version = "3.12.0"
70 | source = "registry+https://github.com/rust-lang/crates.io-index"
71 | checksum = "0d261e256854913907f67ed06efbc3338dfe6179796deefc1ff763fc1aee5535"
72 |
73 | [[package]]
74 | name = "byteorder"
75 | version = "1.4.3"
76 | source = "registry+https://github.com/rust-lang/crates.io-index"
77 | checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"
78 |
79 | [[package]]
80 | name = "bytes"
81 | version = "1.3.0"
82 | source = "registry+https://github.com/rust-lang/crates.io-index"
83 | checksum = "dfb24e866b15a1af2a1b663f10c6b6b8f397a84aadb828f12e5b289ec23a3a3c"
84 |
85 | [[package]]
86 | name = "cfg-if"
87 | version = "1.0.0"
88 | source = "registry+https://github.com/rust-lang/crates.io-index"
89 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
90 |
91 | [[package]]
92 | name = "coarsetime"
93 | version = "0.1.22"
94 | source = "registry+https://github.com/rust-lang/crates.io-index"
95 | checksum = "454038500439e141804c655b4cd1bc6a70bcb95cd2bc9463af5661b6956f0e46"
96 | dependencies = [
97 | "libc",
98 | "once_cell",
99 | "wasi",
100 | "wasm-bindgen",
101 | ]
102 |
103 | [[package]]
104 | name = "const-oid"
105 | version = "0.9.1"
106 | source = "registry+https://github.com/rust-lang/crates.io-index"
107 | checksum = "cec318a675afcb6a1ea1d4340e2d377e56e47c266f28043ceccbf4412ddfdd3b"
108 |
109 | [[package]]
110 | name = "cpufeatures"
111 | version = "0.2.5"
112 | source = "registry+https://github.com/rust-lang/crates.io-index"
113 | checksum = "28d997bd5e24a5928dd43e46dc529867e207907fe0b239c3477d924f7f2ca320"
114 | dependencies = [
115 | "libc",
116 | ]
117 |
118 | [[package]]
119 | name = "crypto-bigint"
120 | version = "0.4.9"
121 | source = "registry+https://github.com/rust-lang/crates.io-index"
122 | checksum = "ef2b4b23cddf68b89b8f8069890e8c270d54e2d5fe1b143820234805e4cb17ef"
123 | dependencies = [
124 | "generic-array",
125 | "rand_core",
126 | "subtle",
127 | "zeroize",
128 | ]
129 |
130 | [[package]]
131 | name = "crypto-common"
132 | version = "0.1.6"
133 | source = "registry+https://github.com/rust-lang/crates.io-index"
134 | checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
135 | dependencies = [
136 | "generic-array",
137 | "typenum",
138 | ]
139 |
140 | [[package]]
141 | name = "ct-codecs"
142 | version = "1.1.1"
143 | source = "registry+https://github.com/rust-lang/crates.io-index"
144 | checksum = "f3b7eb4404b8195a9abb6356f4ac07d8ba267045c8d6d220ac4dc992e6cc75df"
145 |
146 | [[package]]
147 | name = "der"
148 | version = "0.6.1"
149 | source = "registry+https://github.com/rust-lang/crates.io-index"
150 | checksum = "f1a467a65c5e759bce6e65eaf91cc29f466cdc57cb65777bd646872a8a1fd4de"
151 | dependencies = [
152 | "const-oid",
153 | "pem-rfc7468",
154 | "zeroize",
155 | ]
156 |
157 | [[package]]
158 | name = "digest"
159 | version = "0.10.6"
160 | source = "registry+https://github.com/rust-lang/crates.io-index"
161 | checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f"
162 | dependencies = [
163 | "block-buffer",
164 | "const-oid",
165 | "crypto-common",
166 | "subtle",
167 | ]
168 |
169 | [[package]]
170 | name = "ecdsa"
171 | version = "0.15.1"
172 | source = "registry+https://github.com/rust-lang/crates.io-index"
173 | checksum = "12844141594ad74185a926d030f3b605f6a903b4e3fec351f3ea338ac5b7637e"
174 | dependencies = [
175 | "der",
176 | "elliptic-curve",
177 | "rfc6979",
178 | "signature 2.0.0",
179 | ]
180 |
181 | [[package]]
182 | name = "ed25519-compact"
183 | version = "2.0.4"
184 | source = "registry+https://github.com/rust-lang/crates.io-index"
185 | checksum = "6a3d382e8464107391c8706b4c14b087808ecb909f6c15c34114bc42e53a9e4c"
186 | dependencies = [
187 | "ct-codecs",
188 | "getrandom",
189 | ]
190 |
191 | [[package]]
192 | name = "elliptic-curve"
193 | version = "0.12.3"
194 | source = "registry+https://github.com/rust-lang/crates.io-index"
195 | checksum = "e7bb888ab5300a19b8e5bceef25ac745ad065f3c9f7efc6de1b91958110891d3"
196 | dependencies = [
197 | "base16ct",
198 | "crypto-bigint",
199 | "der",
200 | "digest",
201 | "ff",
202 | "generic-array",
203 | "group",
204 | "hkdf",
205 | "pem-rfc7468",
206 | "pkcs8",
207 | "rand_core",
208 | "sec1",
209 | "subtle",
210 | "zeroize",
211 | ]
212 |
213 | [[package]]
214 | name = "ff"
215 | version = "0.12.1"
216 | source = "registry+https://github.com/rust-lang/crates.io-index"
217 | checksum = "d013fc25338cc558c5c2cfbad646908fb23591e2404481826742b651c9af7160"
218 | dependencies = [
219 | "rand_core",
220 | "subtle",
221 | ]
222 |
223 | [[package]]
224 | name = "fnv"
225 | version = "1.0.7"
226 | source = "registry+https://github.com/rust-lang/crates.io-index"
227 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
228 |
229 | [[package]]
230 | name = "form_urlencoded"
231 | version = "1.1.0"
232 | source = "registry+https://github.com/rust-lang/crates.io-index"
233 | checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8"
234 | dependencies = [
235 | "percent-encoding",
236 | ]
237 |
238 | [[package]]
239 | name = "generic-array"
240 | version = "0.14.6"
241 | source = "registry+https://github.com/rust-lang/crates.io-index"
242 | checksum = "bff49e947297f3312447abdca79f45f4738097cc82b06e72054d2223f601f1b9"
243 | dependencies = [
244 | "typenum",
245 | "version_check",
246 | ]
247 |
248 | [[package]]
249 | name = "getrandom"
250 | version = "0.2.8"
251 | source = "registry+https://github.com/rust-lang/crates.io-index"
252 | checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31"
253 | dependencies = [
254 | "cfg-if",
255 | "libc",
256 | "wasi",
257 | ]
258 |
259 | [[package]]
260 | name = "group"
261 | version = "0.12.1"
262 | source = "registry+https://github.com/rust-lang/crates.io-index"
263 | checksum = "5dfbfb3a6cfbd390d5c9564ab283a0349b9b9fcd46a706c1eb10e0db70bfbac7"
264 | dependencies = [
265 | "ff",
266 | "rand_core",
267 | "subtle",
268 | ]
269 |
270 | [[package]]
271 | name = "heck"
272 | version = "0.3.3"
273 | source = "registry+https://github.com/rust-lang/crates.io-index"
274 | checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c"
275 | dependencies = [
276 | "unicode-segmentation",
277 | ]
278 |
279 | [[package]]
280 | name = "hkdf"
281 | version = "0.12.3"
282 | source = "registry+https://github.com/rust-lang/crates.io-index"
283 | checksum = "791a029f6b9fc27657f6f188ec6e5e43f6911f6f878e0dc5501396e09809d437"
284 | dependencies = [
285 | "hmac",
286 | ]
287 |
288 | [[package]]
289 | name = "hmac"
290 | version = "0.12.1"
291 | source = "registry+https://github.com/rust-lang/crates.io-index"
292 | checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
293 | dependencies = [
294 | "digest",
295 | ]
296 |
297 | [[package]]
298 | name = "hmac-sha1-compact"
299 | version = "1.1.3"
300 | source = "registry+https://github.com/rust-lang/crates.io-index"
301 | checksum = "05e2440a0078e20c3b68ca01234cea4219f23e64b0c0bdb1200c5550d54239bb"
302 |
303 | [[package]]
304 | name = "hmac-sha256"
305 | version = "1.1.6"
306 | source = "registry+https://github.com/rust-lang/crates.io-index"
307 | checksum = "fc736091aacb31ddaa4cd5f6988b3c21e99913ac846b41f32538c5fae5d71bfe"
308 | dependencies = [
309 | "digest",
310 | ]
311 |
312 | [[package]]
313 | name = "hmac-sha512"
314 | version = "1.1.4"
315 | source = "registry+https://github.com/rust-lang/crates.io-index"
316 | checksum = "520c9c3f6040661669bc5c91e551b605a520c8e0a63a766a91a65adef734d151"
317 | dependencies = [
318 | "digest",
319 | ]
320 |
321 | [[package]]
322 | name = "http"
323 | version = "0.2.8"
324 | source = "registry+https://github.com/rust-lang/crates.io-index"
325 | checksum = "75f43d41e26995c17e71ee126451dd3941010b0514a81a9d11f3b341debc2399"
326 | dependencies = [
327 | "bytes",
328 | "fnv",
329 | "itoa",
330 | ]
331 |
332 | [[package]]
333 | name = "id-arena"
334 | version = "2.2.1"
335 | source = "registry+https://github.com/rust-lang/crates.io-index"
336 | checksum = "25a2bc672d1148e28034f176e01fffebb08b35768468cc954630da77a1449005"
337 |
338 | [[package]]
339 | name = "itoa"
340 | version = "1.0.5"
341 | source = "registry+https://github.com/rust-lang/crates.io-index"
342 | checksum = "fad582f4b9e86b6caa621cabeb0963332d92eea04729ab12892c2533951e6440"
343 |
344 | [[package]]
345 | name = "jwt-simple"
346 | version = "0.11.3"
347 | source = "registry+https://github.com/rust-lang/crates.io-index"
348 | checksum = "21a4c8e544a27e20e2fe4b82a93a9e823f01ebcc1e4e135e839db66df0e7dc54"
349 | dependencies = [
350 | "anyhow",
351 | "binstring",
352 | "coarsetime",
353 | "ct-codecs",
354 | "ed25519-compact",
355 | "hmac-sha1-compact",
356 | "hmac-sha256",
357 | "hmac-sha512",
358 | "k256",
359 | "p256",
360 | "p384",
361 | "rand",
362 | "rsa",
363 | "serde",
364 | "serde_json",
365 | "spki",
366 | "thiserror",
367 | "zeroize",
368 | ]
369 |
370 | [[package]]
371 | name = "k256"
372 | version = "0.12.0"
373 | source = "registry+https://github.com/rust-lang/crates.io-index"
374 | checksum = "92a55e0ff3b72c262bcf041d9e97f1b84492b68f1c1a384de2323d3dc9403397"
375 | dependencies = [
376 | "cfg-if",
377 | "ecdsa",
378 | "elliptic-curve",
379 | "once_cell",
380 | "sha2",
381 | "signature 2.0.0",
382 | ]
383 |
384 | [[package]]
385 | name = "lazy_static"
386 | version = "1.4.0"
387 | source = "registry+https://github.com/rust-lang/crates.io-index"
388 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
389 | dependencies = [
390 | "spin",
391 | ]
392 |
393 | [[package]]
394 | name = "libc"
395 | version = "0.2.139"
396 | source = "registry+https://github.com/rust-lang/crates.io-index"
397 | checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79"
398 |
399 | [[package]]
400 | name = "libm"
401 | version = "0.2.6"
402 | source = "registry+https://github.com/rust-lang/crates.io-index"
403 | checksum = "348108ab3fba42ec82ff6e9564fc4ca0247bdccdc68dd8af9764bbc79c3c8ffb"
404 |
405 | [[package]]
406 | name = "log"
407 | version = "0.4.17"
408 | source = "registry+https://github.com/rust-lang/crates.io-index"
409 | checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e"
410 | dependencies = [
411 | "cfg-if",
412 | ]
413 |
414 | [[package]]
415 | name = "memchr"
416 | version = "2.5.0"
417 | source = "registry+https://github.com/rust-lang/crates.io-index"
418 | checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
419 |
420 | [[package]]
421 | name = "num-bigint-dig"
422 | version = "0.8.2"
423 | source = "registry+https://github.com/rust-lang/crates.io-index"
424 | checksum = "2399c9463abc5f909349d8aa9ba080e0b88b3ce2885389b60b993f39b1a56905"
425 | dependencies = [
426 | "byteorder",
427 | "lazy_static",
428 | "libm",
429 | "num-integer",
430 | "num-iter",
431 | "num-traits",
432 | "rand",
433 | "smallvec",
434 | "zeroize",
435 | ]
436 |
437 | [[package]]
438 | name = "num-integer"
439 | version = "0.1.45"
440 | source = "registry+https://github.com/rust-lang/crates.io-index"
441 | checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9"
442 | dependencies = [
443 | "autocfg",
444 | "num-traits",
445 | ]
446 |
447 | [[package]]
448 | name = "num-iter"
449 | version = "0.1.43"
450 | source = "registry+https://github.com/rust-lang/crates.io-index"
451 | checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252"
452 | dependencies = [
453 | "autocfg",
454 | "num-integer",
455 | "num-traits",
456 | ]
457 |
458 | [[package]]
459 | name = "num-traits"
460 | version = "0.2.15"
461 | source = "registry+https://github.com/rust-lang/crates.io-index"
462 | checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd"
463 | dependencies = [
464 | "autocfg",
465 | "libm",
466 | ]
467 |
468 | [[package]]
469 | name = "once_cell"
470 | version = "1.17.0"
471 | source = "registry+https://github.com/rust-lang/crates.io-index"
472 | checksum = "6f61fba1741ea2b3d6a1e3178721804bb716a68a6aeba1149b5d52e3d464ea66"
473 |
474 | [[package]]
475 | name = "p256"
476 | version = "0.12.0"
477 | source = "registry+https://github.com/rust-lang/crates.io-index"
478 | checksum = "49c124b3cbce43bcbac68c58ec181d98ed6cc7e6d0aa7c3ba97b2563410b0e55"
479 | dependencies = [
480 | "ecdsa",
481 | "elliptic-curve",
482 | "primeorder",
483 | "sha2",
484 | ]
485 |
486 | [[package]]
487 | name = "p384"
488 | version = "0.12.0"
489 | source = "registry+https://github.com/rust-lang/crates.io-index"
490 | checksum = "630a4a9b2618348ececfae61a4905f564b817063bf2d66cdfc2ced523fe1d2d4"
491 | dependencies = [
492 | "ecdsa",
493 | "elliptic-curve",
494 | "primeorder",
495 | "sha2",
496 | ]
497 |
498 | [[package]]
499 | name = "pem-rfc7468"
500 | version = "0.6.0"
501 | source = "registry+https://github.com/rust-lang/crates.io-index"
502 | checksum = "24d159833a9105500e0398934e205e0773f0b27529557134ecfc51c27646adac"
503 | dependencies = [
504 | "base64ct",
505 | ]
506 |
507 | [[package]]
508 | name = "percent-encoding"
509 | version = "2.2.0"
510 | source = "registry+https://github.com/rust-lang/crates.io-index"
511 | checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e"
512 |
513 | [[package]]
514 | name = "pkcs1"
515 | version = "0.4.1"
516 | source = "registry+https://github.com/rust-lang/crates.io-index"
517 | checksum = "eff33bdbdfc54cc98a2eca766ebdec3e1b8fb7387523d5c9c9a2891da856f719"
518 | dependencies = [
519 | "der",
520 | "pkcs8",
521 | "spki",
522 | "zeroize",
523 | ]
524 |
525 | [[package]]
526 | name = "pkcs8"
527 | version = "0.9.0"
528 | source = "registry+https://github.com/rust-lang/crates.io-index"
529 | checksum = "9eca2c590a5f85da82668fa685c09ce2888b9430e83299debf1f34b65fd4a4ba"
530 | dependencies = [
531 | "der",
532 | "spki",
533 | ]
534 |
535 | [[package]]
536 | name = "ppv-lite86"
537 | version = "0.2.17"
538 | source = "registry+https://github.com/rust-lang/crates.io-index"
539 | checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
540 |
541 | [[package]]
542 | name = "primeorder"
543 | version = "0.12.1"
544 | source = "registry+https://github.com/rust-lang/crates.io-index"
545 | checksum = "0b54f7131b3dba65a2f414cf5bd25b66d4682e4608610668eae785750ba4c5b2"
546 | dependencies = [
547 | "elliptic-curve",
548 | ]
549 |
550 | [[package]]
551 | name = "proc-macro2"
552 | version = "1.0.50"
553 | source = "registry+https://github.com/rust-lang/crates.io-index"
554 | checksum = "6ef7d57beacfaf2d8aee5937dab7b7f28de3cb8b1828479bb5de2a7106f2bae2"
555 | dependencies = [
556 | "unicode-ident",
557 | ]
558 |
559 | [[package]]
560 | name = "profile-api"
561 | version = "0.1.0"
562 | dependencies = [
563 | "anyhow",
564 | "base64",
565 | "bytes",
566 | "http",
567 | "jwt-simple",
568 | "serde",
569 | "serde_json",
570 | "spin-sdk",
571 | "wit-bindgen-rust",
572 | ]
573 |
574 | [[package]]
575 | name = "pulldown-cmark"
576 | version = "0.8.0"
577 | source = "registry+https://github.com/rust-lang/crates.io-index"
578 | checksum = "ffade02495f22453cd593159ea2f59827aae7f53fa8323f756799b670881dcf8"
579 | dependencies = [
580 | "bitflags",
581 | "memchr",
582 | "unicase",
583 | ]
584 |
585 | [[package]]
586 | name = "quote"
587 | version = "1.0.23"
588 | source = "registry+https://github.com/rust-lang/crates.io-index"
589 | checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b"
590 | dependencies = [
591 | "proc-macro2",
592 | ]
593 |
594 | [[package]]
595 | name = "rand"
596 | version = "0.8.5"
597 | source = "registry+https://github.com/rust-lang/crates.io-index"
598 | checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
599 | dependencies = [
600 | "libc",
601 | "rand_chacha",
602 | "rand_core",
603 | ]
604 |
605 | [[package]]
606 | name = "rand_chacha"
607 | version = "0.3.1"
608 | source = "registry+https://github.com/rust-lang/crates.io-index"
609 | checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
610 | dependencies = [
611 | "ppv-lite86",
612 | "rand_core",
613 | ]
614 |
615 | [[package]]
616 | name = "rand_core"
617 | version = "0.6.4"
618 | source = "registry+https://github.com/rust-lang/crates.io-index"
619 | checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
620 | dependencies = [
621 | "getrandom",
622 | ]
623 |
624 | [[package]]
625 | name = "rfc6979"
626 | version = "0.3.1"
627 | source = "registry+https://github.com/rust-lang/crates.io-index"
628 | checksum = "7743f17af12fa0b03b803ba12cd6a8d9483a587e89c69445e3909655c0b9fabb"
629 | dependencies = [
630 | "crypto-bigint",
631 | "hmac",
632 | "zeroize",
633 | ]
634 |
635 | [[package]]
636 | name = "rsa"
637 | version = "0.7.2"
638 | source = "registry+https://github.com/rust-lang/crates.io-index"
639 | checksum = "094052d5470cbcef561cb848a7209968c9f12dfa6d668f4bca048ac5de51099c"
640 | dependencies = [
641 | "byteorder",
642 | "digest",
643 | "num-bigint-dig",
644 | "num-integer",
645 | "num-iter",
646 | "num-traits",
647 | "pkcs1",
648 | "pkcs8",
649 | "rand_core",
650 | "signature 1.6.4",
651 | "smallvec",
652 | "subtle",
653 | "zeroize",
654 | ]
655 |
656 | [[package]]
657 | name = "ryu"
658 | version = "1.0.12"
659 | source = "registry+https://github.com/rust-lang/crates.io-index"
660 | checksum = "7b4b9743ed687d4b4bcedf9ff5eaa7398495ae14e61cba0a295704edbc7decde"
661 |
662 | [[package]]
663 | name = "sec1"
664 | version = "0.3.0"
665 | source = "registry+https://github.com/rust-lang/crates.io-index"
666 | checksum = "3be24c1842290c45df0a7bf069e0c268a747ad05a192f2fd7dcfdbc1cba40928"
667 | dependencies = [
668 | "base16ct",
669 | "der",
670 | "generic-array",
671 | "pkcs8",
672 | "subtle",
673 | "zeroize",
674 | ]
675 |
676 | [[package]]
677 | name = "serde"
678 | version = "1.0.152"
679 | source = "registry+https://github.com/rust-lang/crates.io-index"
680 | checksum = "bb7d1f0d3021d347a83e556fc4683dea2ea09d87bccdf88ff5c12545d89d5efb"
681 | dependencies = [
682 | "serde_derive",
683 | ]
684 |
685 | [[package]]
686 | name = "serde_derive"
687 | version = "1.0.152"
688 | source = "registry+https://github.com/rust-lang/crates.io-index"
689 | checksum = "af487d118eecd09402d70a5d72551860e788df87b464af30e5ea6a38c75c541e"
690 | dependencies = [
691 | "proc-macro2",
692 | "quote",
693 | "syn",
694 | ]
695 |
696 | [[package]]
697 | name = "serde_json"
698 | version = "1.0.91"
699 | source = "registry+https://github.com/rust-lang/crates.io-index"
700 | checksum = "877c235533714907a8c2464236f5c4b2a17262ef1bd71f38f35ea592c8da6883"
701 | dependencies = [
702 | "itoa",
703 | "ryu",
704 | "serde",
705 | ]
706 |
707 | [[package]]
708 | name = "sha2"
709 | version = "0.10.6"
710 | source = "registry+https://github.com/rust-lang/crates.io-index"
711 | checksum = "82e6b795fe2e3b1e845bafcb27aa35405c4d47cdfc92af5fc8d3002f76cebdc0"
712 | dependencies = [
713 | "cfg-if",
714 | "cpufeatures",
715 | "digest",
716 | ]
717 |
718 | [[package]]
719 | name = "signature"
720 | version = "1.6.4"
721 | source = "registry+https://github.com/rust-lang/crates.io-index"
722 | checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c"
723 | dependencies = [
724 | "digest",
725 | "rand_core",
726 | ]
727 |
728 | [[package]]
729 | name = "signature"
730 | version = "2.0.0"
731 | source = "registry+https://github.com/rust-lang/crates.io-index"
732 | checksum = "8fe458c98333f9c8152221191a77e2a44e8325d0193484af2e9421a53019e57d"
733 | dependencies = [
734 | "digest",
735 | "rand_core",
736 | ]
737 |
738 | [[package]]
739 | name = "smallvec"
740 | version = "1.10.0"
741 | source = "registry+https://github.com/rust-lang/crates.io-index"
742 | checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0"
743 |
744 | [[package]]
745 | name = "spin"
746 | version = "0.5.2"
747 | source = "registry+https://github.com/rust-lang/crates.io-index"
748 | checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d"
749 |
750 | [[package]]
751 | name = "spin-macro"
752 | version = "0.1.0"
753 | source = "git+https://github.com/fermyon/spin?tag=v1.0.0#df99be238267b498451993d47b7e42e17da95c09"
754 | dependencies = [
755 | "anyhow",
756 | "bytes",
757 | "http",
758 | "proc-macro2",
759 | "quote",
760 | "syn",
761 | "wit-bindgen-gen-core",
762 | "wit-bindgen-gen-rust-wasm",
763 | "wit-bindgen-rust",
764 | ]
765 |
766 | [[package]]
767 | name = "spin-sdk"
768 | version = "1.0.0"
769 | source = "git+https://github.com/fermyon/spin?tag=v1.0.0#df99be238267b498451993d47b7e42e17da95c09"
770 | dependencies = [
771 | "anyhow",
772 | "bytes",
773 | "form_urlencoded",
774 | "http",
775 | "spin-macro",
776 | "thiserror",
777 | "wit-bindgen-rust",
778 | ]
779 |
780 | [[package]]
781 | name = "spki"
782 | version = "0.6.0"
783 | source = "registry+https://github.com/rust-lang/crates.io-index"
784 | checksum = "67cf02bbac7a337dc36e4f5a693db6c21e7863f45070f7064577eb4367a3212b"
785 | dependencies = [
786 | "base64ct",
787 | "der",
788 | ]
789 |
790 | [[package]]
791 | name = "subtle"
792 | version = "2.4.1"
793 | source = "registry+https://github.com/rust-lang/crates.io-index"
794 | checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601"
795 |
796 | [[package]]
797 | name = "syn"
798 | version = "1.0.107"
799 | source = "registry+https://github.com/rust-lang/crates.io-index"
800 | checksum = "1f4064b5b16e03ae50984a5a8ed5d4f8803e6bc1fd170a3cda91a1be4b18e3f5"
801 | dependencies = [
802 | "proc-macro2",
803 | "quote",
804 | "unicode-ident",
805 | ]
806 |
807 | [[package]]
808 | name = "thiserror"
809 | version = "1.0.38"
810 | source = "registry+https://github.com/rust-lang/crates.io-index"
811 | checksum = "6a9cd18aa97d5c45c6603caea1da6628790b37f7a34b6ca89522331c5180fed0"
812 | dependencies = [
813 | "thiserror-impl",
814 | ]
815 |
816 | [[package]]
817 | name = "thiserror-impl"
818 | version = "1.0.38"
819 | source = "registry+https://github.com/rust-lang/crates.io-index"
820 | checksum = "1fb327af4685e4d03fa8cbcf1716380da910eeb2bb8be417e7f9fd3fb164f36f"
821 | dependencies = [
822 | "proc-macro2",
823 | "quote",
824 | "syn",
825 | ]
826 |
827 | [[package]]
828 | name = "tinyvec"
829 | version = "1.6.0"
830 | source = "registry+https://github.com/rust-lang/crates.io-index"
831 | checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50"
832 | dependencies = [
833 | "tinyvec_macros",
834 | ]
835 |
836 | [[package]]
837 | name = "tinyvec_macros"
838 | version = "0.1.0"
839 | source = "registry+https://github.com/rust-lang/crates.io-index"
840 | checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
841 |
842 | [[package]]
843 | name = "typenum"
844 | version = "1.16.0"
845 | source = "registry+https://github.com/rust-lang/crates.io-index"
846 | checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba"
847 |
848 | [[package]]
849 | name = "unicase"
850 | version = "2.6.0"
851 | source = "registry+https://github.com/rust-lang/crates.io-index"
852 | checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6"
853 | dependencies = [
854 | "version_check",
855 | ]
856 |
857 | [[package]]
858 | name = "unicode-ident"
859 | version = "1.0.6"
860 | source = "registry+https://github.com/rust-lang/crates.io-index"
861 | checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc"
862 |
863 | [[package]]
864 | name = "unicode-normalization"
865 | version = "0.1.22"
866 | source = "registry+https://github.com/rust-lang/crates.io-index"
867 | checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921"
868 | dependencies = [
869 | "tinyvec",
870 | ]
871 |
872 | [[package]]
873 | name = "unicode-segmentation"
874 | version = "1.10.0"
875 | source = "registry+https://github.com/rust-lang/crates.io-index"
876 | checksum = "0fdbf052a0783de01e944a6ce7a8cb939e295b1e7be835a1112c3b9a7f047a5a"
877 |
878 | [[package]]
879 | name = "unicode-xid"
880 | version = "0.2.4"
881 | source = "registry+https://github.com/rust-lang/crates.io-index"
882 | checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c"
883 |
884 | [[package]]
885 | name = "version_check"
886 | version = "0.9.4"
887 | source = "registry+https://github.com/rust-lang/crates.io-index"
888 | checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
889 |
890 | [[package]]
891 | name = "wasi"
892 | version = "0.11.0+wasi-snapshot-preview1"
893 | source = "registry+https://github.com/rust-lang/crates.io-index"
894 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
895 |
896 | [[package]]
897 | name = "wasm-bindgen"
898 | version = "0.2.84"
899 | source = "registry+https://github.com/rust-lang/crates.io-index"
900 | checksum = "31f8dcbc21f30d9b8f2ea926ecb58f6b91192c17e9d33594b3df58b2007ca53b"
901 | dependencies = [
902 | "cfg-if",
903 | "wasm-bindgen-macro",
904 | ]
905 |
906 | [[package]]
907 | name = "wasm-bindgen-backend"
908 | version = "0.2.84"
909 | source = "registry+https://github.com/rust-lang/crates.io-index"
910 | checksum = "95ce90fd5bcc06af55a641a86428ee4229e44e07033963a2290a8e241607ccb9"
911 | dependencies = [
912 | "bumpalo",
913 | "log",
914 | "once_cell",
915 | "proc-macro2",
916 | "quote",
917 | "syn",
918 | "wasm-bindgen-shared",
919 | ]
920 |
921 | [[package]]
922 | name = "wasm-bindgen-macro"
923 | version = "0.2.84"
924 | source = "registry+https://github.com/rust-lang/crates.io-index"
925 | checksum = "4c21f77c0bedc37fd5dc21f897894a5ca01e7bb159884559461862ae90c0b4c5"
926 | dependencies = [
927 | "quote",
928 | "wasm-bindgen-macro-support",
929 | ]
930 |
931 | [[package]]
932 | name = "wasm-bindgen-macro-support"
933 | version = "0.2.84"
934 | source = "registry+https://github.com/rust-lang/crates.io-index"
935 | checksum = "2aff81306fcac3c7515ad4e177f521b5c9a15f2b08f4e32d823066102f35a5f6"
936 | dependencies = [
937 | "proc-macro2",
938 | "quote",
939 | "syn",
940 | "wasm-bindgen-backend",
941 | "wasm-bindgen-shared",
942 | ]
943 |
944 | [[package]]
945 | name = "wasm-bindgen-shared"
946 | version = "0.2.84"
947 | source = "registry+https://github.com/rust-lang/crates.io-index"
948 | checksum = "0046fef7e28c3804e5e38bfa31ea2a0f73905319b677e57ebe37e49358989b5d"
949 |
950 | [[package]]
951 | name = "wit-bindgen-gen-core"
952 | version = "0.2.0"
953 | source = "git+https://github.com/bytecodealliance/wit-bindgen?rev=cb871cfa1ee460b51eb1d144b175b9aab9c50aba#cb871cfa1ee460b51eb1d144b175b9aab9c50aba"
954 | dependencies = [
955 | "anyhow",
956 | "wit-parser",
957 | ]
958 |
959 | [[package]]
960 | name = "wit-bindgen-gen-rust"
961 | version = "0.2.0"
962 | source = "git+https://github.com/bytecodealliance/wit-bindgen?rev=cb871cfa1ee460b51eb1d144b175b9aab9c50aba#cb871cfa1ee460b51eb1d144b175b9aab9c50aba"
963 | dependencies = [
964 | "heck",
965 | "wit-bindgen-gen-core",
966 | ]
967 |
968 | [[package]]
969 | name = "wit-bindgen-gen-rust-wasm"
970 | version = "0.2.0"
971 | source = "git+https://github.com/bytecodealliance/wit-bindgen?rev=cb871cfa1ee460b51eb1d144b175b9aab9c50aba#cb871cfa1ee460b51eb1d144b175b9aab9c50aba"
972 | dependencies = [
973 | "heck",
974 | "wit-bindgen-gen-core",
975 | "wit-bindgen-gen-rust",
976 | ]
977 |
978 | [[package]]
979 | name = "wit-bindgen-rust"
980 | version = "0.2.0"
981 | source = "git+https://github.com/bytecodealliance/wit-bindgen?rev=cb871cfa1ee460b51eb1d144b175b9aab9c50aba#cb871cfa1ee460b51eb1d144b175b9aab9c50aba"
982 | dependencies = [
983 | "async-trait",
984 | "bitflags",
985 | "wit-bindgen-rust-impl",
986 | ]
987 |
988 | [[package]]
989 | name = "wit-bindgen-rust-impl"
990 | version = "0.2.0"
991 | source = "git+https://github.com/bytecodealliance/wit-bindgen?rev=cb871cfa1ee460b51eb1d144b175b9aab9c50aba#cb871cfa1ee460b51eb1d144b175b9aab9c50aba"
992 | dependencies = [
993 | "proc-macro2",
994 | "syn",
995 | "wit-bindgen-gen-core",
996 | "wit-bindgen-gen-rust-wasm",
997 | ]
998 |
999 | [[package]]
1000 | name = "wit-parser"
1001 | version = "0.2.0"
1002 | source = "git+https://github.com/bytecodealliance/wit-bindgen?rev=cb871cfa1ee460b51eb1d144b175b9aab9c50aba#cb871cfa1ee460b51eb1d144b175b9aab9c50aba"
1003 | dependencies = [
1004 | "anyhow",
1005 | "id-arena",
1006 | "pulldown-cmark",
1007 | "unicode-normalization",
1008 | "unicode-xid",
1009 | ]
1010 |
1011 | [[package]]
1012 | name = "zeroize"
1013 | version = "1.5.7"
1014 | source = "registry+https://github.com/rust-lang/crates.io-index"
1015 | checksum = "c394b5bd0c6f669e7275d9c20aa90ae064cb22e75a1cad54e1b34088034b149f"
1016 |
--------------------------------------------------------------------------------