magic login basics
This commit is contained in:
parent
94edec4954
commit
a0835c9de4
|
@ -0,0 +1,3 @@
|
|||
/target
|
||||
Config.toml
|
||||
pg_data
|
|
@ -0,0 +1,2 @@
|
|||
yarn 1.22.19
|
||||
nodejs 20.7.0
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"editor.formatOnSave": true,
|
||||
"rust-analyzer.showUnlinkedFileNotification": false
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,41 @@
|
|||
[package]
|
||||
name = "omegafm"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1"
|
||||
async-trait = "0.1.77"
|
||||
axum = "0.7"
|
||||
celery = "0.5.5"
|
||||
figment = { version = "0.10.13", features = [
|
||||
"env",
|
||||
"toml",
|
||||
"yaml",
|
||||
] }
|
||||
josekit = "0.8.5"
|
||||
lazy_static = "1.4.0"
|
||||
rand = "0.8.5"
|
||||
serde = { version = "1.0.195", features = [
|
||||
"derive",
|
||||
] }
|
||||
sqlx = { version = "0.7.3", features = [
|
||||
"postgres",
|
||||
"runtime-tokio",
|
||||
"uuid",
|
||||
] }
|
||||
surrealdb = "1.1.0"
|
||||
thiserror = "1.0.56"
|
||||
tokio = { version = "1.35.1", features = [
|
||||
"full",
|
||||
] }
|
||||
tower = "0.4.13"
|
||||
tower-http = { version = "0.5", features = [
|
||||
"fs",
|
||||
] }
|
||||
tracing = "0.1.40"
|
||||
tracing-subscriber = { version = "0.3.18", features = [
|
||||
"env-filter",
|
||||
] }
|
|
@ -0,0 +1,13 @@
|
|||
services:
|
||||
postgres:
|
||||
image: postgres:15
|
||||
ports:
|
||||
- 5432:5432
|
||||
environment:
|
||||
- POSTGRES_PASSWORD=password
|
||||
volumes:
|
||||
- ./pg_data:/var/lib/postgresql/data:z
|
||||
# rabbit:
|
||||
# image: rabbitmq:3
|
||||
# ports:
|
||||
# - 5672:5672
|
|
@ -0,0 +1,13 @@
|
|||
.DS_Store
|
||||
node_modules
|
||||
/build
|
||||
/.svelte-kit
|
||||
/package
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
# Ignore files for PNPM, NPM and YARN
|
||||
pnpm-lock.yaml
|
||||
package-lock.json
|
||||
yarn.lock
|
|
@ -0,0 +1,30 @@
|
|||
module.exports = {
|
||||
root: true,
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:svelte/recommended',
|
||||
'prettier'
|
||||
],
|
||||
parser: '@typescript-eslint/parser',
|
||||
plugins: ['@typescript-eslint'],
|
||||
parserOptions: {
|
||||
sourceType: 'module',
|
||||
ecmaVersion: 2020,
|
||||
extraFileExtensions: ['.svelte']
|
||||
},
|
||||
env: {
|
||||
browser: true,
|
||||
es2017: true,
|
||||
node: true
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
files: ['*.svelte'],
|
||||
parser: 'svelte-eslint-parser',
|
||||
parserOptions: {
|
||||
parser: '@typescript-eslint/parser'
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
|
@ -0,0 +1,10 @@
|
|||
.DS_Store
|
||||
node_modules
|
||||
/build
|
||||
/.svelte-kit
|
||||
/package
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
vite.config.js.timestamp-*
|
||||
vite.config.ts.timestamp-*
|
|
@ -0,0 +1 @@
|
|||
engine-strict=true
|
|
@ -0,0 +1,13 @@
|
|||
.DS_Store
|
||||
node_modules
|
||||
/build
|
||||
/.svelte-kit
|
||||
/package
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
# Ignore files for PNPM, NPM and YARN
|
||||
pnpm-lock.yaml
|
||||
package-lock.json
|
||||
yarn.lock
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"useTabs": true,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "none",
|
||||
"printWidth": 100,
|
||||
"plugins": ["prettier-plugin-svelte"],
|
||||
"pluginSearchDirs": ["."],
|
||||
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
|
||||
}
|
|
@ -0,0 +1,100 @@
|
|||
{
|
||||
"prettier.documentSelectors": [
|
||||
"**/*.svelte"
|
||||
],
|
||||
"tailwindCSS.classAttributes": [
|
||||
"class",
|
||||
"accent",
|
||||
"active",
|
||||
"aspectRatio",
|
||||
"background",
|
||||
"bgBackdrop",
|
||||
"bgDark",
|
||||
"bgDrawer",
|
||||
"bgLight",
|
||||
"blur",
|
||||
"border",
|
||||
"button",
|
||||
"buttonClasses",
|
||||
"buttonTextFirst",
|
||||
"buttonTextLast",
|
||||
"buttonTextNext",
|
||||
"buttonTextPrevious",
|
||||
"caretClosed",
|
||||
"caretOpen",
|
||||
"color",
|
||||
"controlSeparator",
|
||||
"controlVariant",
|
||||
"cursor",
|
||||
"display",
|
||||
"element",
|
||||
"fill",
|
||||
"fillDark",
|
||||
"fillLight",
|
||||
"flex",
|
||||
"gap",
|
||||
"gridColumns",
|
||||
"height",
|
||||
"hover",
|
||||
"inactive",
|
||||
"indent",
|
||||
"justify",
|
||||
"meter",
|
||||
"padding",
|
||||
"regionAnchor",
|
||||
"regionBackdrop",
|
||||
"regionBody",
|
||||
"regionCaption",
|
||||
"regionCaret",
|
||||
"regionCell",
|
||||
"regionChildren",
|
||||
"regionCone",
|
||||
"regionContent",
|
||||
"regionControl",
|
||||
"regionDefault",
|
||||
"regionDrawer",
|
||||
"regionFoot",
|
||||
"regionFootCell",
|
||||
"regionHead",
|
||||
"regionHeadCell",
|
||||
"regionHeader",
|
||||
"regionIcon",
|
||||
"regionInterface",
|
||||
"regionInterfaceText",
|
||||
"regionLabel",
|
||||
"regionLead",
|
||||
"regionLegend",
|
||||
"regionList",
|
||||
"regionListItem",
|
||||
"regionNavigation",
|
||||
"regionPage",
|
||||
"regionPanel",
|
||||
"regionRowHeadline",
|
||||
"regionRowMain",
|
||||
"regionSummary",
|
||||
"regionSymbol",
|
||||
"regionTab",
|
||||
"regionTrail",
|
||||
"ring",
|
||||
"rounded",
|
||||
"select",
|
||||
"shadow",
|
||||
"slotDefault",
|
||||
"slotFooter",
|
||||
"slotHeader",
|
||||
"slotLead",
|
||||
"slotMessage",
|
||||
"slotMeta",
|
||||
"slotPageContent",
|
||||
"slotPageFooter",
|
||||
"slotPageHeader",
|
||||
"slotSidebarLeft",
|
||||
"slotSidebarRight",
|
||||
"slotTrail",
|
||||
"spacing",
|
||||
"text",
|
||||
"track",
|
||||
"width",
|
||||
"zIndex"
|
||||
]
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
# create-svelte
|
||||
|
||||
Everything you need to build a Svelte project, powered by [`create-svelte`](https://github.com/sveltejs/kit/tree/master/packages/create-svelte).
|
||||
|
||||
## Creating a project
|
||||
|
||||
If you're seeing this, you've probably already done this step. Congrats!
|
||||
|
||||
```bash
|
||||
# create a new project in the current directory
|
||||
npm create svelte@latest
|
||||
|
||||
# create a new project in my-app
|
||||
npm create svelte@latest my-app
|
||||
```
|
||||
|
||||
## Developing
|
||||
|
||||
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
|
||||
# or start the server and open the app in a new browser tab
|
||||
npm run dev -- --open
|
||||
```
|
||||
|
||||
## Building
|
||||
|
||||
To create a production version of your app:
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
You can preview the production build with `npm run preview`.
|
||||
|
||||
> To deploy your app, you may need to install an [adapter](https://kit.svelte.dev/docs/adapters) for your target environment.
|
|
@ -0,0 +1,2 @@
|
|||
build:
|
||||
yarn build
|
|
@ -0,0 +1,46 @@
|
|||
{
|
||||
"name": "",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"test": "playwright test",
|
||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||
"lint": "prettier --plugin-search-dir . --check . && eslint .",
|
||||
"format": "prettier --plugin-search-dir . --write ."
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.28.1",
|
||||
"@skeletonlabs/skeleton": "2.1.0",
|
||||
"@skeletonlabs/tw-plugin": "0.2.0",
|
||||
"@sveltejs/adapter-auto": "^2.0.0",
|
||||
"@sveltejs/adapter-static": "^2.0.3",
|
||||
"@sveltejs/kit": "^1.20.4",
|
||||
"@tailwindcss/forms": "0.5.6",
|
||||
"@tailwindcss/typography": "0.5.10",
|
||||
"@types/node": "20.7.0",
|
||||
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
||||
"@typescript-eslint/parser": "^6.0.0",
|
||||
"autoprefixer": "10.4.16",
|
||||
"eslint": "^8.28.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-plugin-svelte": "^2.30.0",
|
||||
"postcss": "8.4.30",
|
||||
"prettier": "^2.8.0",
|
||||
"prettier-plugin-svelte": "^2.10.1",
|
||||
"svelte": "^4.0.5",
|
||||
"svelte-check": "^3.4.3",
|
||||
"tailwindcss": "3.3.3",
|
||||
"tslib": "^2.4.1",
|
||||
"typescript": "^5.0.0",
|
||||
"vite": "^4.4.2",
|
||||
"vite-plugin-tailwind-purgecss": "0.1.3"
|
||||
},
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@floating-ui/dom": "1.5.3"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
import type { PlaywrightTestConfig } from '@playwright/test';
|
||||
|
||||
const config: PlaywrightTestConfig = {
|
||||
webServer: {
|
||||
command: 'npm run build && npm run preview',
|
||||
port: 4173
|
||||
},
|
||||
testDir: 'tests',
|
||||
testMatch: /(.+\.)?(test|spec)\.[jt]s/
|
||||
};
|
||||
|
||||
export default config;
|
|
@ -0,0 +1,6 @@
|
|||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
// See https://kit.svelte.dev/docs/types#app
|
||||
// for information about these interfaces
|
||||
// and what to do when importing types
|
||||
declare namespace App {
|
||||
// interface Locals {}
|
||||
// interface PageData {}
|
||||
// interface Error {}
|
||||
// interface Platform {}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en" class="dark">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
<body data-sveltekit-preload-data="hover" data-theme="vintage">
|
||||
<div style="display: contents" class="h-full overflow-hidden">%sveltekit.body%</div>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,16 @@
|
|||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
@tailwind variants;
|
||||
|
||||
html,
|
||||
body {
|
||||
@apply h-full overflow-hidden;
|
||||
}
|
||||
|
||||
/* vintage theme */
|
||||
@font-face {
|
||||
font-family: 'Abril Fatface';
|
||||
src: url('/fonts/AbrilFatface.ttf');
|
||||
font-display: swap;
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
// place files you want to import through the `$lib` alias in this folder.
|
|
@ -0,0 +1,49 @@
|
|||
<script lang="ts">
|
||||
import '../app.postcss';
|
||||
import { AppShell, AppBar } from '@skeletonlabs/skeleton';
|
||||
|
||||
// Floating UI for Popups
|
||||
import { computePosition, autoUpdate, flip, shift, offset, arrow } from '@floating-ui/dom';
|
||||
import { storePopup } from '@skeletonlabs/skeleton';
|
||||
storePopup.set({ computePosition, autoUpdate, flip, shift, offset, arrow });
|
||||
</script>
|
||||
|
||||
<!-- App Shell -->
|
||||
<AppShell>
|
||||
<svelte:fragment slot="header">
|
||||
<!-- App Bar -->
|
||||
<AppBar>
|
||||
<svelte:fragment slot="lead">
|
||||
<strong class="text-xl uppercase">Skeleton</strong>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="trail">
|
||||
<a
|
||||
class="btn btn-sm variant-ghost-surface"
|
||||
href="https://discord.gg/EXqV7W8MtY"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Discord
|
||||
</a>
|
||||
<a
|
||||
class="btn btn-sm variant-ghost-surface"
|
||||
href="https://twitter.com/SkeletonUI"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Twitter
|
||||
</a>
|
||||
<a
|
||||
class="btn btn-sm variant-ghost-surface"
|
||||
href="https://github.com/skeletonlabs/skeleton"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
GitHub
|
||||
</a>
|
||||
</svelte:fragment>
|
||||
</AppBar>
|
||||
</svelte:fragment>
|
||||
<!-- Page Route Content -->
|
||||
<slot />
|
||||
</AppShell>
|
|
@ -0,0 +1 @@
|
|||
export const prerender = true;
|
|
@ -0,0 +1,71 @@
|
|||
<!-- YOU CAN DELETE EVERYTHING IN THIS PAGE -->
|
||||
|
||||
<div class="container h-full mx-auto flex justify-center items-center">
|
||||
<div class="space-y-10 text-center flex flex-col items-center">
|
||||
<h2 class="h2">Welcome to Skeleton.</h2>
|
||||
<!-- Animated Logo -->
|
||||
<figure>
|
||||
<section class="img-bg" />
|
||||
<svg
|
||||
class="fill-token -scale-x-[100%]"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 200 200"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M98.77 50.95c25.1 0 46.54 8.7 61.86 23a41.34 41.34 0 0 0 5.19-1.93c4.35-2.02 10.06-6.17 17.13-12.43-1.15 10.91-2.38 18.93-3.7 24.04-.7 2.75-1.8 6.08-3.3 10a80.04 80.04 0 0 1 8.42 23.33c6.04 30.3-4.3 43.7-28.33 51.18.18.9.32 1.87.42 2.9.86 8.87-3.62 23.19-9 23.19-3.54 0-5.84-4.93-8.3-12.13-.78 8.34-4.58 17.9-8.98 17.9-4.73 0-7.25-8.84-10.93-20.13a214 214 0 0 1-.64 2.93l-.16.71-.16.71-.17.71c-1.84 7.58-4.46 15.07-8.5 15.07-5.06 0-2.29-15.9-10.8-22.63-43.14 2.36-79.43-13.6-79.43-59.62 0-8.48 2-16.76 5.69-24.45a93.72 93.72 0 0 1-1.77-3.68c-2.87-6.32-6.3-15.88-10.31-28.7 10.26 7.66 18.12 12.22 23.6 13.68.5.14 1.02.26 1.57.36 14.36-14.44 35.88-24.01 60.6-24.01Zm-9.99 62.3c-14.57 0-26.39 11.45-26.39 25.58 0 14.14 11.82 25.6 26.39 25.6s26.39-11.46 26.39-25.6c0-13.99-11.58-25.35-25.95-25.58Zm37.45 31.95c-4.4 0-6.73 9.4-6.73 13.62 0 3.3 1.1 5.12 2.9 5.45 4.39.4 3.05-5.97 5.23-5.97 1.06 0 2.2 1.35 3.34 2.73l.34.42c1.25 1.52 2.5 2.93 3.64 2.49 2.7-1.61 1.67-5.12.74-7.88-3.3-6.96-5.05-10.86-9.46-10.86Zm-36.85-28.45c12.57 0 22.76 9.78 22.76 21.85 0 12.07-10.2 21.85-22.76 21.85-.77 0-1.53-.04-2.29-.11 11.5-1.1 20.46-10.42 20.46-21.74 0-11.32-8.97-20.63-20.46-21.74.76-.07 1.52-.1 2.3-.1Zm65.54-5c-10.04 0-18.18 10.06-18.18 22.47 0 12.4 8.14 22.47 18.18 22.47s18.18-10.06 18.18-22.47c0-12.41-8.14-22.48-18.18-22.48Zm.6 3.62c8.38 0 15.16 8.4 15.16 18.74 0 10.35-6.78 18.74-15.16 18.74-.77 0-1.54-.07-2.28-.21 7.3-1.36 12.89-9.14 12.89-18.53 0-9.4-5.6-17.17-12.89-18.53.74-.14 1.5-.2 2.28-.2Zm3.34-72.27.12.07c.58.38.75 1.16.37 1.74l-2.99 4.6c-.35.55-1.05.73-1.61.44l-.12-.07a1.26 1.26 0 0 1-.37-1.74l2.98-4.6a1.26 1.26 0 0 1 1.62-.44ZM39.66 42l.08.1 2.76 3.93a1.26 1.26 0 0 1-2.06 1.45l-2.76-3.94A1.26 1.26 0 0 1 39.66 42Zm63.28-42 2.85 24.13 10.62-11.94.28 29.72-2.1-.47a77.8 77.8 0 0 0-16.72-2.04c-4.96 0-9.61.67-13.96 2l-2.34.73L83.5 4.96l9.72 18.37L102.94 0Zm-1.87 13.39-7.5 17.96-7.3-13.8-1.03 19.93.22-.06a51.56 51.56 0 0 1 12.1-1.45h.31c4.58 0 9.58.54 15 1.61l.35.07-.15-16.54-9.79 11-2.21-18.72Zm38.86 19.23c.67.2 1.05.89.86 1.56l-.38 1.32c-.17.62-.8 1-1.42.89l-.13-.03a1.26 1.26 0 0 1-.86-1.56l.38-1.32c.19-.66.88-1.05 1.55-.86ZM63.95 31.1l.05.12.7 2.17a1.26 1.26 0 0 1-2.34.9l-.04-.12-.71-2.17a1.26 1.26 0 0 1 2.34-.9Z"
|
||||
/>
|
||||
</svg>
|
||||
</figure>
|
||||
<!-- / -->
|
||||
<div class="flex justify-center space-x-2">
|
||||
<a
|
||||
class="btn variant-filled"
|
||||
href="https://skeleton.dev/"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Launch Documentation
|
||||
</a>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<p>Try editing the following:</p>
|
||||
<p><code class="code">/src/routes/+layout.svelte</code></p>
|
||||
<p><code class="code">/src/routes/+page.svelte</code></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="postcss">
|
||||
figure {
|
||||
@apply flex relative flex-col;
|
||||
}
|
||||
figure svg,
|
||||
.img-bg {
|
||||
@apply w-64 h-64 md:w-80 md:h-80;
|
||||
}
|
||||
.img-bg {
|
||||
@apply absolute z-[-1] rounded-full blur-[50px] transition-all;
|
||||
animation: pulse 5s cubic-bezier(0, 0, 0, 0.5) infinite,
|
||||
glow 5s linear infinite;
|
||||
}
|
||||
@keyframes glow {
|
||||
0% {
|
||||
@apply bg-primary-400/50;
|
||||
}
|
||||
33% {
|
||||
@apply bg-secondary-400/50;
|
||||
}
|
||||
66% {
|
||||
@apply bg-tertiary-400/50;
|
||||
}
|
||||
100% {
|
||||
@apply bg-primary-400/50;
|
||||
}
|
||||
}
|
||||
@keyframes pulse {
|
||||
50% {
|
||||
transform: scale(1.5);
|
||||
}
|
||||
}
|
||||
</style>
|
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
Binary file not shown.
|
@ -0,0 +1,19 @@
|
|||
import adapter from '@sveltejs/adapter-static';
|
||||
import { vitePreprocess } from '@sveltejs/kit/vite';
|
||||
|
||||
|
||||
/** @type {import('@sveltejs/kit').Config} */
|
||||
const config = {
|
||||
extensions: ['.svelte'],
|
||||
// Consult https://kit.svelte.dev/docs/integrations#preprocessors
|
||||
// for more information about preprocessors
|
||||
preprocess: [ vitePreprocess()],
|
||||
|
||||
kit: {
|
||||
// adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list.
|
||||
// If your environment is not supported or you settled on a specific environment, switch out the adapter.
|
||||
// See https://kit.svelte.dev/docs/adapters for more information about adapters.
|
||||
adapter: adapter()
|
||||
}
|
||||
};
|
||||
export default config;
|
|
@ -0,0 +1,27 @@
|
|||
import { join } from 'path'
|
||||
import type { Config } from 'tailwindcss'
|
||||
import forms from '@tailwindcss/forms';
|
||||
import typography from '@tailwindcss/typography';
|
||||
import { skeleton } from '@skeletonlabs/tw-plugin'
|
||||
|
||||
export default {
|
||||
darkMode: 'class',
|
||||
content: ['./src/**/*.{html,js,svelte,ts}', join(require.resolve('@skeletonlabs/skeleton'), '../**/*.{html,js,svelte,ts}')],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [
|
||||
forms,
|
||||
typography,
|
||||
skeleton({
|
||||
themes: {
|
||||
preset: [
|
||||
{
|
||||
name: 'vintage',
|
||||
enhancements: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
],
|
||||
} satisfies Config;
|
|
@ -0,0 +1,6 @@
|
|||
import { expect, test } from '@playwright/test';
|
||||
|
||||
test('index page has expected h1', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await expect(page.getByRole('heading', { name: 'Welcome to SvelteKit' })).toBeVisible();
|
||||
});
|
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"extends": "./.svelte-kit/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"allowJs": true,
|
||||
"checkJs": true,
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"skipLibCheck": true,
|
||||
"sourceMap": true,
|
||||
"strict": true
|
||||
}
|
||||
// Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias
|
||||
//
|
||||
// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
|
||||
// from the referenced tsconfig.json - TypeScript does not merge them in
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
import { purgeCss } from 'vite-plugin-tailwind-purgecss';
|
||||
import { sveltekit } from '@sveltejs/kit/vite';
|
||||
import { defineConfig } from 'vite';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [sveltekit(), purgeCss()]
|
||||
});
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,11 @@
|
|||
set dotenv-load := true
|
||||
|
||||
FRONTEND_OUT := 'frontend/build'
|
||||
|
||||
dev:
|
||||
cargo run
|
||||
|
||||
build:
|
||||
just frontend/build
|
||||
cargo build --release
|
||||
cp -r {FRONTEND_OUT} public
|
|
@ -0,0 +1,27 @@
|
|||
-- Add migration script here
|
||||
CREATE TABLE Person (
|
||||
id UUID PRIMARY KEY,
|
||||
username VARCHAR(64) NOT NULL,
|
||||
displayname VARCHAR(64),
|
||||
is_admin BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
host VARCHAR(64) NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE Play (
|
||||
id UUID PRIMARY KEY,
|
||||
person UUID REFERENCES Person(id),
|
||||
started_at TIMESTAMP NOT NULL,
|
||||
mbid VARCHAR(64),
|
||||
track_name VARCHAR(64) NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE Magic (
|
||||
token CHAR(64) PRIMARY KEY,
|
||||
id UUID NOT NULL REFERENCES Person(id),
|
||||
created_at TIMESTAMP NOT NULL,
|
||||
max_age INTERVAL NOT NULL DEFAULT '1 day'
|
||||
);
|
||||
|
||||
CREATE TABLE Token (
|
||||
refresh_token VARCHAR PRIMARY KEY
|
||||
);
|
|
@ -0,0 +1,75 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use axum::{
|
||||
extract::Path,
|
||||
http::StatusCode,
|
||||
response::IntoResponse,
|
||||
routing::{get, post},
|
||||
Json, Router,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::PgPool;
|
||||
|
||||
use crate::{
|
||||
config::Config,
|
||||
db::{AvailableDb, Database},
|
||||
jwt::make_jwt,
|
||||
utils::internal_error,
|
||||
worker::{Message, Queue},
|
||||
AppCtx,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
struct JwtResponse {
|
||||
token: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
struct LoginRequest {
|
||||
username: String,
|
||||
}
|
||||
|
||||
pub fn routes() -> Router<AppCtx> {
|
||||
Router::new()
|
||||
.route("/finish/:token", get(handle_login))
|
||||
.route("/generate", post(start_login))
|
||||
}
|
||||
|
||||
async fn start_login(
|
||||
q: Queue,
|
||||
Database(mut conn): Database,
|
||||
Json(LoginRequest { username }): Json<LoginRequest>,
|
||||
) -> Result<(), (StatusCode, String)> {
|
||||
let user = conn
|
||||
.get_or_create_user_by_name(&username)
|
||||
.await
|
||||
.map_err(internal_error)?;
|
||||
|
||||
let token = conn
|
||||
.create_magic_token(&user.id)
|
||||
.await
|
||||
.map_err(internal_error)?;
|
||||
|
||||
q.send(Message::SendToken {
|
||||
user: user.username,
|
||||
token: token,
|
||||
})
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_login(
|
||||
Path(token): Path<String>,
|
||||
cfg: Arc<Config>,
|
||||
Database(mut conn): Database,
|
||||
) -> Result<Json<JwtResponse>, (StatusCode, String)> {
|
||||
let user = conn.check_magic_token(&token).await.map_err(|e| {
|
||||
tracing::error!("Error logging in with token '{token}': ");
|
||||
(StatusCode::UNAUTHORIZED, "Invalid Token".to_owned())
|
||||
})?;
|
||||
|
||||
let jwt = make_jwt(&*cfg, &user.to_string()).map_err(internal_error)?;
|
||||
|
||||
Ok(Json(JwtResponse { token: jwt }))
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
use axum::Router;
|
||||
use sqlx::PgPool;
|
||||
|
||||
use crate::{db::AvailableDb, AppCtx};
|
||||
|
||||
mod magic;
|
||||
mod model;
|
||||
mod track;
|
||||
|
||||
pub fn routes() -> Router<AppCtx> {
|
||||
Router::new()
|
||||
// .nest("/track", track::routes())
|
||||
.nest("/magic", magic::routes())
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct Play {
|
||||
mbid: String,
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
use axum::Router;
|
||||
use sqlx::PgPool;
|
||||
|
||||
pub fn routes() -> Router<PgPool> {
|
||||
Router::new()
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use axum::{
|
||||
extract::FromRequestParts,
|
||||
http::{request::Parts, StatusCode},
|
||||
};
|
||||
use figment::{
|
||||
providers::{Env, Format, Toml, Yaml},
|
||||
Figment,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::AppCtx;
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, Default)]
|
||||
pub struct Config {
|
||||
pub hmac_key: String,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl FromRequestParts<AppCtx> for Arc<Config> {
|
||||
type Rejection = StatusCode;
|
||||
|
||||
async fn from_request_parts(
|
||||
_parts: &mut Parts,
|
||||
state: &AppCtx,
|
||||
) -> Result<Self, Self::Rejection> {
|
||||
Ok(state.config.clone())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn make_config() -> Result<Config, anyhow::Error> {
|
||||
Ok(Figment::new()
|
||||
.join(Toml::file("Config.toml"))
|
||||
.merge(Yaml::file("Config.yml"))
|
||||
.merge(Yaml::file("Config.yaml"))
|
||||
.merge(Env::prefixed("OMEGA__"))
|
||||
.extract()?)
|
||||
}
|
|
@ -0,0 +1,79 @@
|
|||
mod model;
|
||||
pub mod postgres;
|
||||
pub mod surreal;
|
||||
|
||||
use crate::error::Result;
|
||||
use async_trait::async_trait;
|
||||
use axum::{
|
||||
extract::FromRequestParts,
|
||||
http::{request::Parts, StatusCode},
|
||||
};
|
||||
use sqlx::{types::Uuid, PgPool};
|
||||
use surrealdb::{engine::any::Any, Surreal};
|
||||
use tracing::error;
|
||||
|
||||
use crate::AppCtx;
|
||||
|
||||
use self::model::{DbUser, NewUser};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum AvailableDb {
|
||||
Postgres(PgPool),
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub trait DatabaseAdapter {
|
||||
async fn get_user_by_name(&mut self, name: &str) -> Result<DbUser>;
|
||||
async fn get_or_create_user_by_name(&mut self, name: &str) -> Result<DbUser>;
|
||||
async fn create_user(&mut self, name: NewUser) -> Result<DbUser>;
|
||||
async fn create_track(&self);
|
||||
async fn create_magic_token(&mut self, user_id: &Uuid) -> Result<String>;
|
||||
async fn check_magic_token(&mut self, token: &str) -> Result<Uuid>;
|
||||
}
|
||||
pub struct Database(pub Box<dyn DatabaseAdapter + Send + Sync + 'static>);
|
||||
|
||||
#[async_trait]
|
||||
impl FromRequestParts<AppCtx> for Database {
|
||||
type Rejection = (StatusCode, String);
|
||||
|
||||
async fn from_request_parts(
|
||||
_parts: &mut Parts,
|
||||
state: &AppCtx,
|
||||
) -> Result<Self, Self::Rejection> {
|
||||
match &state.db {
|
||||
AvailableDb::Postgres(pool) => {
|
||||
let conn = pool.acquire().await.map_err(|e| {
|
||||
error!("{e}");
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
"Internal Server Error".to_owned(),
|
||||
)
|
||||
})?;
|
||||
|
||||
Ok(Self(Box::new(conn)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// #[async_trait]
|
||||
// impl FromRequestParts<Surreal<Any>> for DatabaseConnection {
|
||||
// type Rejection = (StatusCode, String);
|
||||
|
||||
// async fn from_request_parts(
|
||||
// _parts: &mut Parts,
|
||||
// state: &Surreal<Any>,
|
||||
// ) -> Result<Self, Self::Rejection> {
|
||||
// let conn = Surreal::from_ref(state);
|
||||
|
||||
// // let conn = pool.acquire().await.map_err(|e| {
|
||||
// // error!("{e}");
|
||||
// // (
|
||||
// // StatusCode::INTERNAL_SERVER_ERROR,
|
||||
// // "Internal Server Error".to_owned(),
|
||||
// // )
|
||||
// // })?;
|
||||
|
||||
// Ok(Self(Box::new(conn)))
|
||||
// }
|
||||
// }
|
|
@ -0,0 +1,28 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::types::Uuid;
|
||||
use surrealdb::sql::Thing;
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct DbUser {
|
||||
pub id: Uuid,
|
||||
pub username: String,
|
||||
pub displayname: Option<String>,
|
||||
pub host: String,
|
||||
pub is_admin: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct DbPlay<'a> {
|
||||
user: Thing,
|
||||
started_at: &'a str,
|
||||
mbid: Option<&'a str>,
|
||||
track_name: &'a str,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct NewUser {
|
||||
pub username: String,
|
||||
pub host: String,
|
||||
pub name: Option<String>,
|
||||
pub is_admin: bool,
|
||||
}
|
|
@ -0,0 +1,98 @@
|
|||
use lazy_static::lazy_static;
|
||||
use rand::{distributions::Alphanumeric, prelude::*};
|
||||
use std::{ops::DerefMut, str::FromStr};
|
||||
|
||||
use crate::error::Result;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use sqlx::{pool::PoolConnection, query, query_as, types::Uuid, PgPool, Postgres};
|
||||
|
||||
use super::{
|
||||
model::{DbUser, NewUser},
|
||||
DatabaseAdapter,
|
||||
};
|
||||
|
||||
pub async fn connect(url: impl AsRef<str>) -> Result<PgPool> {
|
||||
let pool = sqlx::PgPool::connect(url.as_ref()).await?;
|
||||
sqlx::migrate!().run(&mut pool.acquire().await?).await?;
|
||||
Ok(pool)
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl DatabaseAdapter for PoolConnection<Postgres> {
|
||||
async fn get_user_by_name(&mut self, name: &str) -> Result<DbUser> {
|
||||
let res = query_as!(
|
||||
DbUser,
|
||||
"SELECT * FROM Person WHERE username = $1 LIMIT 1",
|
||||
name
|
||||
)
|
||||
.fetch_one(self.deref_mut())
|
||||
.await?;
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
async fn get_or_create_user_by_name(&mut self, name: &str) -> Result<DbUser> {
|
||||
match query_as!(
|
||||
DbUser,
|
||||
"SELECT * FROM Person WHERE username = $1 LIMIT 1",
|
||||
name
|
||||
)
|
||||
.fetch_one(self.deref_mut())
|
||||
.await
|
||||
{
|
||||
Err(sqlx::Error::RowNotFound) => {
|
||||
let Some((displayname, host)) = name.split_once('@') else {
|
||||
return Err(sqlx::Error::RowNotFound.into());
|
||||
};
|
||||
self.create_user(NewUser {
|
||||
username: name.to_string(),
|
||||
host: host.to_string(),
|
||||
name: Some(displayname.to_string()),
|
||||
is_admin: false,
|
||||
})
|
||||
.await
|
||||
}
|
||||
other => Ok(other?),
|
||||
}
|
||||
}
|
||||
async fn create_user(&mut self, user: NewUser) -> Result<DbUser> {
|
||||
Ok(query_as!(
|
||||
DbUser,
|
||||
"INSERT INTO Person VALUES (gen_random_uuid(), $1, $2, $3, $4) RETURNING *",
|
||||
user.username,
|
||||
user.name,
|
||||
user.is_admin,
|
||||
user.host,
|
||||
)
|
||||
.fetch_one(self.deref_mut())
|
||||
.await?)
|
||||
}
|
||||
|
||||
async fn create_track(&self) {}
|
||||
async fn create_magic_token(&mut self, user_id: &Uuid) -> Result<String> {
|
||||
let token: String = thread_rng()
|
||||
.sample_iter(Alphanumeric)
|
||||
.take(64)
|
||||
.map(char::from)
|
||||
.collect();
|
||||
|
||||
query!(
|
||||
"INSERT INTO Magic VALUES ($1, $2, now(), '15 min')",
|
||||
token,
|
||||
user_id
|
||||
)
|
||||
.execute(self.deref_mut())
|
||||
.await?;
|
||||
Ok(token)
|
||||
}
|
||||
async fn check_magic_token(&mut self, token: &str) -> Result<Uuid> {
|
||||
let user = query!(
|
||||
"SELECT id FROM Magic WHERE token = $1 AND now() - created_at < max_age ",
|
||||
token
|
||||
)
|
||||
.fetch_one(self.deref_mut())
|
||||
.await?;
|
||||
|
||||
Ok(user.id)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
use surrealdb::{engine::any::Any, opt::auth::Root, Error, Surreal};
|
||||
use tracing::error;
|
||||
|
||||
use super::{
|
||||
model::{DbUser, NewUser},
|
||||
DatabaseAdapter,
|
||||
};
|
||||
|
||||
pub async fn connect(remote: &str) -> Result<Surreal<Any>> {
|
||||
let conn: Surreal<Any> = Surreal::init();
|
||||
conn.connect(remote).await?;
|
||||
|
||||
//TODO: configurable here
|
||||
conn.signin(Root {
|
||||
username: "root",
|
||||
password: "root",
|
||||
})
|
||||
.await?;
|
||||
conn.use_ns("omegafm").use_db("omegafm").await?;
|
||||
|
||||
Ok(conn)
|
||||
}
|
||||
|
||||
// #[async_trait]
|
||||
// impl DatabaseAdapter for Surreal<Any> {
|
||||
// async fn migrate(&self) {
|
||||
// if let Err(e) = self.query("DEFINE TABLE user;").await {
|
||||
// error!("{e:?}");
|
||||
// }
|
||||
|
||||
// if let Err(e) = self.query("DEFINE TABLE play;").await {
|
||||
// error!("{e:?}");
|
||||
// }
|
||||
// }
|
||||
|
||||
// async fn creat_user(&self, user: NewUser) {
|
||||
// let rec: Result<Vec, Error> = self.create("user").content(user).await;
|
||||
// }
|
||||
|
||||
// async fn create_track(&self) {}
|
||||
// }
|
|
@ -0,0 +1,17 @@
|
|||
use std::string::FromUtf8Error;
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
pub type Result<T, E = Error> = core::result::Result<T, E>;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum Error {
|
||||
#[error("Error accessing database: {0}")]
|
||||
DatabaseError(#[from] sqlx::Error),
|
||||
#[error("Error migrating database: {0}")]
|
||||
MigrationError(#[from] sqlx::migrate::MigrateError),
|
||||
#[error("Error parsing UUID: {0}")]
|
||||
UuidError(#[from] sqlx::types::uuid::Error),
|
||||
#[error("String wasn't valid UTF8: {0}")]
|
||||
Utf8Error(#[from] FromUtf8Error),
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
use std::{
|
||||
alloc::System,
|
||||
time::{Duration, SystemTime},
|
||||
};
|
||||
|
||||
use anyhow::anyhow;
|
||||
use josekit::{
|
||||
jws::{JwsHeader, HS256},
|
||||
jwt::{self, JwtPayload, JwtPayloadValidator},
|
||||
JoseError,
|
||||
};
|
||||
use sqlx::types::Uuid;
|
||||
|
||||
use crate::config::Config;
|
||||
|
||||
pub fn make_jwt(cfg: &Config, user: &str) -> Result<String, JoseError> {
|
||||
let signer = HS256.signer_from_bytes(cfg.hmac_key.as_bytes())?;
|
||||
|
||||
let mut header = JwsHeader::new();
|
||||
header.set_token_type("JWT");
|
||||
|
||||
let mut payload = JwtPayload::new();
|
||||
payload.set_subject(user);
|
||||
payload.set_expires_at(&(SystemTime::now() + Duration::from_secs(432000))); // expires in 5 days
|
||||
payload.set_issued_at(&SystemTime::now());
|
||||
payload.set_audience(vec!["user_token"]);
|
||||
|
||||
let token = jwt::encode_with_signer(&payload, &header, &signer)?;
|
||||
|
||||
Ok(token)
|
||||
}
|
||||
|
||||
pub fn verify_jwt(cfg: &Config, token: &str) -> Result<Uuid, anyhow::Error> {
|
||||
let verifier = HS256.verifier_from_bytes(cfg.hmac_key.as_bytes())?;
|
||||
|
||||
let (payload, header) = jwt::decode_with_verifier(token, &verifier)?;
|
||||
let mut validator = JwtPayloadValidator::new();
|
||||
validator.set_base_time(SystemTime::now());
|
||||
validator.validate(&payload)?;
|
||||
|
||||
let Some(id) = payload.subject() else {
|
||||
return Err(anyhow!("Missing Subject claim"));
|
||||
};
|
||||
|
||||
Ok(Uuid::try_parse(id)?)
|
||||
}
|
|
@ -0,0 +1,142 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use axum::{extract::Query, http::StatusCode, routing::get, Json, Router};
|
||||
use config::{make_config, Config};
|
||||
use db::{AvailableDb, Database};
|
||||
use error::Error;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tower_http::services::{ServeDir, ServeFile};
|
||||
use tracing::{error, info};
|
||||
use tracing_subscriber::prelude::*;
|
||||
use utils::internal_error;
|
||||
use worker::Queue;
|
||||
|
||||
mod api;
|
||||
mod config;
|
||||
mod db;
|
||||
mod error;
|
||||
mod jwt;
|
||||
mod utils;
|
||||
mod worker;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct AppCtx {
|
||||
db: AvailableDb,
|
||||
q: Queue,
|
||||
config: Arc<Config>,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
// init logging
|
||||
tracing_subscriber::registry()
|
||||
.with(tracing_subscriber::EnvFilter::from_default_env())
|
||||
.with(tracing_subscriber::fmt::layer())
|
||||
.init();
|
||||
|
||||
let q = worker::start().await.expect("Worker");
|
||||
|
||||
// let db_conn = db::surreal::connect("localhost:8000").await.expect("DB");
|
||||
let db_conn = db::postgres::connect("postgres://postgres:password@localhost")
|
||||
.await
|
||||
.expect("db connection");
|
||||
|
||||
let router = Router::new()
|
||||
.nest("/api", api::routes())
|
||||
.route("/.well-known/webfinger", get(webfinger))
|
||||
.with_state(AppCtx {
|
||||
db: db::AvailableDb::Postgres(db_conn),
|
||||
config: Arc::new(make_config().expect("config")),
|
||||
q,
|
||||
})
|
||||
.fallback_service(
|
||||
ServeDir::new("public").not_found_service(ServeFile::new("public/index.html")),
|
||||
);
|
||||
|
||||
match tokio::net::TcpListener::bind("0.0.0.0:8675").await {
|
||||
Ok(socket) => {
|
||||
info!("Starting server on {}", socket.local_addr().expect("addr"));
|
||||
axum::serve(socket, router)
|
||||
.with_graceful_shutdown(shutdown())
|
||||
.await
|
||||
.expect("Error running server");
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Error binding address: {e}");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn shutdown() {
|
||||
tokio::signal::ctrl_c().await.expect("sigint listener");
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
struct WFQuery {
|
||||
resource: String,
|
||||
}
|
||||
/// WebFinger result that may serialized or deserialized to JSON
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq)]
|
||||
pub struct Webfinger {
|
||||
/// The subject of this WebFinger result.
|
||||
///
|
||||
/// It is an `acct:` URI
|
||||
pub subject: String,
|
||||
|
||||
/// A list of aliases for this WebFinger result.
|
||||
#[serde(default)]
|
||||
pub aliases: Vec<String>,
|
||||
|
||||
/// Links to places where you may find more information about this resource.
|
||||
pub links: Vec<Link>,
|
||||
}
|
||||
|
||||
/// Structure to represent a WebFinger link
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq)]
|
||||
pub struct Link {
|
||||
/// Tells what this link represents
|
||||
pub rel: String,
|
||||
|
||||
/// The actual URL of the link
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub href: Option<String>,
|
||||
|
||||
/// The Link may also contain an URL template, instead of an actual URL
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub template: Option<String>,
|
||||
|
||||
/// The mime-type of this link.
|
||||
///
|
||||
/// If you fetch this URL, you may want to use this value for the Accept header of your HTTP
|
||||
/// request.
|
||||
#[serde(rename = "type", skip_serializing_if = "Option::is_none")]
|
||||
pub mime_type: Option<String>,
|
||||
}
|
||||
|
||||
async fn webfinger(
|
||||
Query(q): Query<WFQuery>,
|
||||
Database(mut conn): Database,
|
||||
) -> Result<Json<Webfinger>, (StatusCode, String)> {
|
||||
let (_, name) = q
|
||||
.resource
|
||||
.split_once(':')
|
||||
.ok_or_else(|| (StatusCode::BAD_REQUEST, "Malformed Resource".to_owned()))?;
|
||||
|
||||
match conn.get_user_by_name(name).await {
|
||||
Err(Error::DatabaseError(sqlx::Error::ColumnNotFound(_))) => {
|
||||
Err((StatusCode::NOT_FOUND, "Unknown user".into()))
|
||||
}
|
||||
Err(e) => Err(internal_error(e)),
|
||||
Ok(user) => Ok(Json(Webfinger {
|
||||
subject: q.resource,
|
||||
aliases: vec![user.username.clone(), user.displayname.unwrap_or_default()],
|
||||
links: vec![Link {
|
||||
rel: "http://webfinger.net/rel/profile-page".to_string(),
|
||||
href: format!("http://localhost:8675/{}", user.username).into(),
|
||||
template: None,
|
||||
mime_type: None,
|
||||
}],
|
||||
})),
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
use axum::http::StatusCode;
|
||||
use tracing::error;
|
||||
|
||||
pub fn internal_error<E: std::error::Error>(err: E) -> (StatusCode, String) {
|
||||
error!("{err:?}");
|
||||
(StatusCode::INTERNAL_SERVER_ERROR, "Server Error".into())
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
use anyhow::Result;
|
||||
|
||||
pub async fn send_token(user: &str, token: &str) -> Result<()> {
|
||||
println!("{token}");
|
||||
Ok(())
|
||||
}
|
|
@ -0,0 +1,126 @@
|
|||
use std::{
|
||||
collections::VecDeque,
|
||||
sync::Arc,
|
||||
time::{Duration, SystemTime},
|
||||
};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use axum::{
|
||||
extract::FromRequestParts,
|
||||
http::{request::Parts, StatusCode},
|
||||
};
|
||||
use celery::Celery;
|
||||
use tokio::{
|
||||
spawn,
|
||||
sync::{Mutex, Notify},
|
||||
task::spawn_blocking,
|
||||
};
|
||||
use tracing::{debug, error, info, warn};
|
||||
|
||||
use crate::AppCtx;
|
||||
|
||||
pub mod ap;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Queue {
|
||||
q: Arc<Mutex<VecDeque<Task>>>,
|
||||
notify: Arc<tokio::sync::Notify>,
|
||||
}
|
||||
|
||||
impl Queue {
|
||||
pub async fn send(&self, msg: Message) {
|
||||
self.q.lock().await.push_back(Task::new(msg));
|
||||
self.notify.notify_one();
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl FromRequestParts<AppCtx> for Queue {
|
||||
type Rejection = StatusCode;
|
||||
|
||||
async fn from_request_parts(
|
||||
_parts: &mut Parts,
|
||||
state: &AppCtx,
|
||||
) -> Result<Self, Self::Rejection> {
|
||||
Ok(state.q.clone())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Message {
|
||||
SendToken { user: String, token: String },
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct Task {
|
||||
tries: u16,
|
||||
delay: Duration,
|
||||
last_attempt: Option<SystemTime>,
|
||||
msg: Message,
|
||||
}
|
||||
|
||||
impl Task {
|
||||
fn new(msg: Message) -> Self {
|
||||
Self {
|
||||
tries: 0,
|
||||
delay: Duration::ZERO,
|
||||
last_attempt: None,
|
||||
msg,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn start() -> Result<Queue, anyhow::Error> {
|
||||
info!("Starting worker task");
|
||||
// let app = celery::app!(
|
||||
// broker = AMQPBroker { "amqp://127.0.0.1:5672".to_string() },
|
||||
// tasks = [
|
||||
// send_token
|
||||
// ],
|
||||
// task_routes = [
|
||||
// "send_token" => "outbound",
|
||||
// "*" => "celery",
|
||||
// ],
|
||||
// prefetch_count = 2,
|
||||
// heartbeat = Some(10),
|
||||
// )
|
||||
// .await?;
|
||||
|
||||
let q = Arc::new(Mutex::new(VecDeque::<Task>::new()));
|
||||
let notify = Arc::new(Notify::new());
|
||||
|
||||
let consumer = q.clone();
|
||||
let listener = notify.clone();
|
||||
|
||||
spawn(async move {
|
||||
loop {
|
||||
listener.notified().await;
|
||||
|
||||
if let Some(task) = consumer.lock().await.pop_front() {
|
||||
debug!("Recieved message {:?}", task.msg);
|
||||
if let Err(e) = dispatch(&task.msg).await {
|
||||
error!("Worker errorL {e}");
|
||||
consumer.lock().await.push_back(task);
|
||||
}
|
||||
} else {
|
||||
warn!("Notified on empty queue");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// let consumer = app.clone();
|
||||
// spawn(async move {
|
||||
// if let Err(e) = consumer.consume_from(&["celery", "outbound"]).await {
|
||||
// error!("Worker error: {e}");
|
||||
// }
|
||||
// });
|
||||
|
||||
Ok(Queue { q, notify })
|
||||
}
|
||||
|
||||
async fn dispatch(msg: &Message) -> anyhow::Result<()> {
|
||||
match msg {
|
||||
Message::SendToken { user, token } => ap::send_token(user, token).await?,
|
||||
}
|
||||
Ok(())
|
||||
}
|
Loading…
Reference in New Issue