mirror of
https://github.com/versia-pub/server.git
synced 2025-12-06 08:28:19 +01:00
guh
This commit is contained in:
parent
aad3ee78d1
commit
8162a5050c
13
README.md
13
README.md
|
|
@ -1,15 +1,2 @@
|
||||||
# fedi-project
|
# fedi-project
|
||||||
|
|
||||||
To install dependencies:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
bun install
|
|
||||||
```
|
|
||||||
|
|
||||||
To run:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
bun run index.ts
|
|
||||||
```
|
|
||||||
|
|
||||||
This project was created using `bun init` in bun v0.8.1. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime.
|
|
||||||
|
|
|
||||||
2
index.ts
2
index.ts
|
|
@ -19,8 +19,6 @@ Bun.serve({
|
||||||
async fetch(req) {
|
async fetch(req) {
|
||||||
const matchedRoute = router.match(req);
|
const matchedRoute = router.match(req);
|
||||||
|
|
||||||
console.log(req.url);
|
|
||||||
|
|
||||||
if (matchedRoute) {
|
if (matchedRoute) {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
|
||||||
return (await import(matchedRoute.filePath)).default(
|
return (await import(matchedRoute.filePath)).default(
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,11 @@
|
||||||
"module": "index.ts",
|
"module": "index.ts",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@julr/unocss-preset-forms": "^0.0.5",
|
||||||
"@types/jsonld": "^1.5.9",
|
"@types/jsonld": "^1.5.9",
|
||||||
"@typescript-eslint/eslint-plugin": "^6.6.0",
|
"@typescript-eslint/eslint-plugin": "^6.6.0",
|
||||||
"@typescript-eslint/parser": "^6.6.0",
|
"@typescript-eslint/parser": "^6.6.0",
|
||||||
|
"@unocss/cli": "^0.55.7",
|
||||||
"activitypub-types": "^1.0.3",
|
"activitypub-types": "^1.0.3",
|
||||||
"bun-types": "latest",
|
"bun-types": "latest",
|
||||||
"eslint": "^8.49.0",
|
"eslint": "^8.49.0",
|
||||||
|
|
@ -14,7 +16,8 @@
|
||||||
"eslint-formatter-summary": "^1.1.0",
|
"eslint-formatter-summary": "^1.1.0",
|
||||||
"eslint-plugin-prettier": "^5.0.0",
|
"eslint-plugin-prettier": "^5.0.0",
|
||||||
"prettier": "^3.0.3",
|
"prettier": "^3.0.3",
|
||||||
"typescript": "^5.2.2"
|
"typescript": "^5.2.2",
|
||||||
|
"unocss": "^0.55.7"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"typescript": "^5.0.0"
|
"typescript": "^5.0.0"
|
||||||
|
|
|
||||||
446
pages/login.html
446
pages/login.html
|
|
@ -1,13 +1,445 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<title>Login with FediProject</title>
|
<title>Login with FediProject</title>
|
||||||
<meta charset="utf-8">
|
{{STYLES}}
|
||||||
|
<style>
|
||||||
|
/*
|
||||||
|
1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4)
|
||||||
|
2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116)
|
||||||
|
2. [UnoCSS]: allow to override the default border color with css var `--un-default-border-color`
|
||||||
|
*/
|
||||||
|
|
||||||
|
*,
|
||||||
|
::before,
|
||||||
|
::after {
|
||||||
|
box-sizing: border-box;
|
||||||
|
/* 1 */
|
||||||
|
border-width: 0;
|
||||||
|
/* 2 */
|
||||||
|
border-style: solid;
|
||||||
|
/* 2 */
|
||||||
|
border-color: var(--un-default-border-color, #e5e7eb);
|
||||||
|
/* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
1. Use a consistent sensible line-height in all browsers.
|
||||||
|
2. Prevent adjustments of font size after orientation changes in iOS.
|
||||||
|
3. Use a more readable tab size.
|
||||||
|
4. Use the user's configured `sans` font-family by default.
|
||||||
|
*/
|
||||||
|
|
||||||
|
html {
|
||||||
|
line-height: 1.5;
|
||||||
|
/* 1 */
|
||||||
|
-webkit-text-size-adjust: 100%;
|
||||||
|
/* 2 */
|
||||||
|
text-size-adjust: 100%;
|
||||||
|
/* 2 */
|
||||||
|
-moz-tab-size: 4;
|
||||||
|
/* 3 */
|
||||||
|
tab-size: 4;
|
||||||
|
/* 3 */
|
||||||
|
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||||
|
/* 4 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
1. Remove the margin in all browsers.
|
||||||
|
2. Inherit line-height from `html` so users can set them as a class directly on the `html` element.
|
||||||
|
*/
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
/* 1 */
|
||||||
|
line-height: inherit;
|
||||||
|
/* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
1. Add the correct height in Firefox.
|
||||||
|
2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655)
|
||||||
|
3. Ensure horizontal rules are visible by default.
|
||||||
|
*/
|
||||||
|
|
||||||
|
hr {
|
||||||
|
height: 0;
|
||||||
|
/* 1 */
|
||||||
|
color: inherit;
|
||||||
|
/* 2 */
|
||||||
|
border-top-width: 1px;
|
||||||
|
/* 3 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Add the correct text decoration in Chrome, Edge, and Safari.
|
||||||
|
*/
|
||||||
|
|
||||||
|
abbr:where([title]) {
|
||||||
|
text-decoration: underline dotted;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Remove the default font size and weight for headings.
|
||||||
|
*/
|
||||||
|
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4,
|
||||||
|
h5,
|
||||||
|
h6 {
|
||||||
|
font-size: inherit;
|
||||||
|
font-weight: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Reset links to optimize for opt-in styling instead of opt-out.
|
||||||
|
*/
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Add the correct font weight in Edge and Safari.
|
||||||
|
*/
|
||||||
|
|
||||||
|
b,
|
||||||
|
strong {
|
||||||
|
font-weight: bolder;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
1. Use the user's configured `mono` font family by default.
|
||||||
|
2. Correct the odd `em` font sizing in all browsers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
code,
|
||||||
|
kbd,
|
||||||
|
samp,
|
||||||
|
pre {
|
||||||
|
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||||
|
/* 1 */
|
||||||
|
font-size: 1em;
|
||||||
|
/* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Add the correct font size in all browsers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
small {
|
||||||
|
font-size: 80%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Prevent `sub` and `sup` elements from affecting the line height in all browsers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
sub,
|
||||||
|
sup {
|
||||||
|
font-size: 75%;
|
||||||
|
line-height: 0;
|
||||||
|
position: relative;
|
||||||
|
vertical-align: baseline;
|
||||||
|
}
|
||||||
|
|
||||||
|
sub {
|
||||||
|
bottom: -0.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
sup {
|
||||||
|
top: -0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297)
|
||||||
|
2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016)
|
||||||
|
3. Remove gaps between table borders by default.
|
||||||
|
*/
|
||||||
|
|
||||||
|
table {
|
||||||
|
text-indent: 0;
|
||||||
|
/* 1 */
|
||||||
|
border-color: inherit;
|
||||||
|
/* 2 */
|
||||||
|
border-collapse: collapse;
|
||||||
|
/* 3 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
1. Change the font styles in all browsers.
|
||||||
|
2. Remove the margin in Firefox and Safari.
|
||||||
|
3. Remove default padding in all browsers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
button,
|
||||||
|
input,
|
||||||
|
optgroup,
|
||||||
|
select,
|
||||||
|
textarea {
|
||||||
|
font-family: inherit;
|
||||||
|
/* 1 */
|
||||||
|
font-feature-settings: inherit;
|
||||||
|
/* 1 */
|
||||||
|
font-variation-settings: inherit;
|
||||||
|
/* 1 */
|
||||||
|
font-size: 100%;
|
||||||
|
/* 1 */
|
||||||
|
font-weight: inherit;
|
||||||
|
/* 1 */
|
||||||
|
line-height: inherit;
|
||||||
|
/* 1 */
|
||||||
|
color: inherit;
|
||||||
|
/* 1 */
|
||||||
|
margin: 0;
|
||||||
|
/* 2 */
|
||||||
|
padding: 0;
|
||||||
|
/* 3 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Remove the inheritance of text transform in Edge and Firefox.
|
||||||
|
*/
|
||||||
|
|
||||||
|
button,
|
||||||
|
select {
|
||||||
|
text-transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
1. Correct the inability to style clickable types in iOS and Safari.
|
||||||
|
2. Remove default button styles.
|
||||||
|
*/
|
||||||
|
|
||||||
|
button,
|
||||||
|
[type='button'],
|
||||||
|
[type='reset'],
|
||||||
|
[type='submit'] {
|
||||||
|
-webkit-appearance: button;
|
||||||
|
/* 1 */
|
||||||
|
background-color: transparent;
|
||||||
|
/* 2 */
|
||||||
|
background-image: none;
|
||||||
|
/* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Use the modern Firefox focus style for all focusable elements.
|
||||||
|
*/
|
||||||
|
|
||||||
|
:-moz-focusring {
|
||||||
|
outline: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737)
|
||||||
|
*/
|
||||||
|
|
||||||
|
:-moz-ui-invalid {
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Add the correct vertical alignment in Chrome and Firefox.
|
||||||
|
*/
|
||||||
|
|
||||||
|
progress {
|
||||||
|
vertical-align: baseline;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Correct the cursor style of increment and decrement buttons in Safari.
|
||||||
|
*/
|
||||||
|
|
||||||
|
::-webkit-inner-spin-button,
|
||||||
|
::-webkit-outer-spin-button {
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
1. Correct the odd appearance in Chrome and Safari.
|
||||||
|
2. Correct the outline style in Safari.
|
||||||
|
*/
|
||||||
|
|
||||||
|
[type='search'] {
|
||||||
|
-webkit-appearance: textfield;
|
||||||
|
/* 1 */
|
||||||
|
outline-offset: -2px;
|
||||||
|
/* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Remove the inner padding in Chrome and Safari on macOS.
|
||||||
|
*/
|
||||||
|
|
||||||
|
::-webkit-search-decoration {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
1. Correct the inability to style clickable types in iOS and Safari.
|
||||||
|
2. Change font properties to `inherit` in Safari.
|
||||||
|
*/
|
||||||
|
|
||||||
|
::-webkit-file-upload-button {
|
||||||
|
-webkit-appearance: button;
|
||||||
|
/* 1 */
|
||||||
|
font: inherit;
|
||||||
|
/* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Add the correct display in Chrome and Safari.
|
||||||
|
*/
|
||||||
|
|
||||||
|
summary {
|
||||||
|
display: list-item;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Removes the default spacing and border for appropriate elements.
|
||||||
|
*/
|
||||||
|
|
||||||
|
blockquote,
|
||||||
|
dl,
|
||||||
|
dd,
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4,
|
||||||
|
h5,
|
||||||
|
h6,
|
||||||
|
hr,
|
||||||
|
figure,
|
||||||
|
p,
|
||||||
|
pre {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
fieldset {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
legend {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
ol,
|
||||||
|
ul,
|
||||||
|
menu {
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Prevent resizing textareas horizontally by default.
|
||||||
|
*/
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300)
|
||||||
|
2. Set the default placeholder color to the user's configured gray 400 color.
|
||||||
|
*/
|
||||||
|
|
||||||
|
input::placeholder,
|
||||||
|
textarea::placeholder {
|
||||||
|
opacity: 1;
|
||||||
|
/* 1 */
|
||||||
|
color: #9ca3af;
|
||||||
|
/* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Set the default cursor for buttons.
|
||||||
|
*/
|
||||||
|
|
||||||
|
button,
|
||||||
|
[role="button"] {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Make sure disabled buttons don't get the pointer cursor.
|
||||||
|
*/
|
||||||
|
:disabled {
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14)
|
||||||
|
2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210)
|
||||||
|
This can trigger a poorly considered lint error in some tools but is included by design.
|
||||||
|
*/
|
||||||
|
|
||||||
|
img,
|
||||||
|
svg,
|
||||||
|
video,
|
||||||
|
canvas,
|
||||||
|
audio,
|
||||||
|
iframe,
|
||||||
|
embed,
|
||||||
|
object {
|
||||||
|
display: block;
|
||||||
|
/* 1 */
|
||||||
|
vertical-align: middle;
|
||||||
|
/* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14)
|
||||||
|
*/
|
||||||
|
|
||||||
|
img,
|
||||||
|
video {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Make elements with the HTML hidden attribute stay hidden by default */
|
||||||
|
[hidden] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<meta charset="utf-8">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<form method="post" action="{{URL}}">
|
<div class="flex min-h-screen flex-col justify-center px-6 py-12 lg:px-8">
|
||||||
<input type="text" name="username" placeholder="Username" required />
|
<div class="mt-10 sm:mx-auto sm:w-full sm:max-w-sm">
|
||||||
<input type="password" name="password" placeholder="Password" required />
|
<form class="space-y-6" method="POST" action="{{URL}}">
|
||||||
<input type="submit" value="Login" />
|
<div>
|
||||||
</form>
|
<label for="email" class="block text-sm font-medium leading-6 text-gray-900">Email address</label>
|
||||||
|
<div class="mt-2">
|
||||||
|
<input id="email" name="email" type="email" autocomplete="email" required
|
||||||
|
class="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<label for="password" class="block text-sm font-medium leading-6 text-gray-900">Password</label>
|
||||||
|
</div>
|
||||||
|
<div class="mt-2">
|
||||||
|
<input id="password" name="password" type="password" autocomplete="current-password" required
|
||||||
|
class="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<button type="submit"
|
||||||
|
class="flex w-full justify-center rounded-md !bg-indigo-600 px-3 py-1.5 text-sm font-semibold leading-6 text-white shadow-sm hover:!bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600">Sign
|
||||||
|
in</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</body>
|
</body>
|
||||||
155
pages/uno.css
Normal file
155
pages/uno.css
Normal file
|
|
@ -0,0 +1,155 @@
|
||||||
|
/* layer: preflights */
|
||||||
|
*,::before,::after{--un-rotate:0;--un-rotate-x:0;--un-rotate-y:0;--un-rotate-z:0;--un-scale-x:1;--un-scale-y:1;--un-scale-z:1;--un-skew-x:0;--un-skew-y:0;--un-translate-x:0;--un-translate-y:0;--un-translate-z:0;--un-pan-x: ;--un-pan-y: ;--un-pinch-zoom: ;--un-scroll-snap-strictness:proximity;--un-ordinal: ;--un-slashed-zero: ;--un-numeric-figure: ;--un-numeric-spacing: ;--un-numeric-fraction: ;--un-border-spacing-x:0;--un-border-spacing-y:0;--un-ring-offset-shadow:0 0 rgba(0,0,0,0);--un-ring-shadow:0 0 rgba(0,0,0,0);--un-shadow-inset: ;--un-shadow:0 0 rgba(0,0,0,0);--un-ring-inset: ;--un-ring-offset-width:0px;--un-ring-offset-color:#fff;--un-ring-width:0px;--un-ring-color:rgba(147,197,253,0.5);--un-blur: ;--un-brightness: ;--un-contrast: ;--un-drop-shadow: ;--un-grayscale: ;--un-hue-rotate: ;--un-invert: ;--un-saturate: ;--un-sepia: ;--un-backdrop-blur: ;--un-backdrop-brightness: ;--un-backdrop-contrast: ;--un-backdrop-grayscale: ;--un-backdrop-hue-rotate: ;--un-backdrop-invert: ;--un-backdrop-opacity: ;--un-backdrop-saturate: ;--un-backdrop-sepia: ;}::backdrop{--un-rotate:0;--un-rotate-x:0;--un-rotate-y:0;--un-rotate-z:0;--un-scale-x:1;--un-scale-y:1;--un-scale-z:1;--un-skew-x:0;--un-skew-y:0;--un-translate-x:0;--un-translate-y:0;--un-translate-z:0;--un-pan-x: ;--un-pan-y: ;--un-pinch-zoom: ;--un-scroll-snap-strictness:proximity;--un-ordinal: ;--un-slashed-zero: ;--un-numeric-figure: ;--un-numeric-spacing: ;--un-numeric-fraction: ;--un-border-spacing-x:0;--un-border-spacing-y:0;--un-ring-offset-shadow:0 0 rgba(0,0,0,0);--un-ring-shadow:0 0 rgba(0,0,0,0);--un-shadow-inset: ;--un-shadow:0 0 rgba(0,0,0,0);--un-ring-inset: ;--un-ring-offset-width:0px;--un-ring-offset-color:#fff;--un-ring-width:0px;--un-ring-color:rgba(147,197,253,0.5);--un-blur: ;--un-brightness: ;--un-contrast: ;--un-drop-shadow: ;--un-grayscale: ;--un-hue-rotate: ;--un-invert: ;--un-saturate: ;--un-sepia: ;--un-backdrop-blur: ;--un-backdrop-brightness: ;--un-backdrop-contrast: ;--un-backdrop-grayscale: ;--un-backdrop-hue-rotate: ;--un-backdrop-invert: ;--un-backdrop-opacity: ;--un-backdrop-saturate: ;--un-backdrop-sepia: ;}
|
||||||
|
[type='text'], [type='email'], [type='url'], [type='password'], [type='number'], [type='date'], [type='datetime-local'], [type='month'], [type='search'], [type='tel'], [type='time'], [type='week'], [multiple], textarea, select { appearance: none;
|
||||||
|
background-color: #fff;
|
||||||
|
border-color: #6b7280;
|
||||||
|
border-width: 1px;
|
||||||
|
border-radius: 0;
|
||||||
|
padding-top: 0.5rem;
|
||||||
|
padding-right: 0.75rem;
|
||||||
|
padding-bottom: 0.5rem;
|
||||||
|
padding-left: 0.75rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
line-height: 1.5rem;
|
||||||
|
--un-shadow: 0 0 #0000; }
|
||||||
|
[type='text']:focus, [type='email']:focus, [type='url']:focus, [type='password']:focus, [type='number']:focus, [type='date']:focus, [type='datetime-local']:focus, [type='month']:focus, [type='search']:focus, [type='tel']:focus, [type='time']:focus, [type='week']:focus, [multiple]:focus, textarea:focus, select:focus { outline: 2px solid transparent;
|
||||||
|
outline-offset: 2px;
|
||||||
|
--un-ring-inset: var(--un-empty,/*!*/ /*!*/);
|
||||||
|
--un-ring-offset-width: 0px;
|
||||||
|
--un-ring-offset-color: #fff;
|
||||||
|
--un-ring-color: #2563eb;
|
||||||
|
--un-ring-offset-shadow: var(--un-ring-inset) 0 0 0 var(--un-ring-offset-width) var(--un-ring-offset-color);
|
||||||
|
--un-ring-shadow: var(--un-ring-inset) 0 0 0 calc(1px + var(--un-ring-offset-width)) var(--un-ring-color);
|
||||||
|
box-shadow: var(--un-ring-offset-shadow), var(--un-ring-shadow), var(--un-shadow);
|
||||||
|
border-color: #2563eb; }
|
||||||
|
input::placeholder, textarea::placeholder { color: #6b7280;
|
||||||
|
opacity: 1; }
|
||||||
|
::-webkit-datetime-edit-fields-wrapper { padding: 0; }
|
||||||
|
::-webkit-date-and-time-value { min-height: 1.5em; }
|
||||||
|
::-webkit-datetime-edit, ::-webkit-datetime-edit-year-field, ::-webkit-datetime-edit-month-field, ::-webkit-datetime-edit-day-field, ::-webkit-datetime-edit-hour-field, ::-webkit-datetime-edit-minute-field, ::-webkit-datetime-edit-second-field, ::-webkit-datetime-edit-millisecond-field, ::-webkit-datetime-edit-meridiem-field { padding-top: 0;
|
||||||
|
padding-bottom: 0; }
|
||||||
|
select { background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e");
|
||||||
|
background-position: right 0.5rem center;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-size: 1.5em 1.5em;
|
||||||
|
padding-right: 2.5rem;
|
||||||
|
print-color-adjust: exact; }
|
||||||
|
[multiple] { background-image: initial;
|
||||||
|
background-position: initial;
|
||||||
|
background-repeat: unset;
|
||||||
|
background-size: initial;
|
||||||
|
padding-right: 0.75rem;
|
||||||
|
print-color-adjust: unset; }
|
||||||
|
[type='checkbox'], [type='radio'] { appearance: none;
|
||||||
|
padding: 0;
|
||||||
|
print-color-adjust: exact;
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
background-origin: border-box;
|
||||||
|
user-select: none;
|
||||||
|
flex-shrink: 0;
|
||||||
|
height: 1rem;
|
||||||
|
width: 1rem;
|
||||||
|
color: #2563eb;
|
||||||
|
background-color: #fff;
|
||||||
|
border-color: #6b7280;
|
||||||
|
border-width: 1px;
|
||||||
|
--un-shadow: 0 0 #0000; }
|
||||||
|
[type='checkbox'] { border-radius: 0; }
|
||||||
|
[type='radio'] { border-radius: 100%; }
|
||||||
|
[type='checkbox']:focus, [type='radio']:focus { outline: 2px solid transparent;
|
||||||
|
outline-offset: 2px;
|
||||||
|
--un-ring-inset: var(--un-empty,/*!*/ /*!*/);
|
||||||
|
--un-ring-offset-width: 2px;
|
||||||
|
--un-ring-offset-color: #fff;
|
||||||
|
--un-ring-color: #2563eb;
|
||||||
|
--un-ring-offset-shadow: var(--un-ring-inset) 0 0 0 var(--un-ring-offset-width) var(--un-ring-offset-color);
|
||||||
|
--un-ring-shadow: var(--un-ring-inset) 0 0 0 calc(2px + var(--un-ring-offset-width)) var(--un-ring-color);
|
||||||
|
box-shadow: var(--un-ring-offset-shadow), var(--un-ring-shadow), var(--un-shadow); }
|
||||||
|
[type='checkbox']:checked, [type='radio']:checked { border-color: transparent;
|
||||||
|
background-color: currentColor;
|
||||||
|
background-size: 100% 100%;
|
||||||
|
background-position: center;
|
||||||
|
background-repeat: no-repeat; }
|
||||||
|
[type='checkbox']:checked { background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M12.207 4.793a1 1 0 010 1.414l-5 5a1 1 0 01-1.414 0l-2-2a1 1 0 011.414-1.414L6.5 9.086l4.293-4.293a1 1 0 011.414 0z'/%3e%3c/svg%3e"); }
|
||||||
|
[type='radio']:checked { background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3ccircle cx='8' cy='8' r='3'/%3e%3c/svg%3e"); }
|
||||||
|
[type='checkbox']:checked:hover, [type='checkbox']:checked:focus, [type='radio']:checked:hover, [type='radio']:checked:focus { border-color: transparent;
|
||||||
|
background-color: currentColor; }
|
||||||
|
[type='checkbox']:indeterminate { background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 16 16'%3e%3cpath stroke='white' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M4 8h8'/%3e%3c/svg%3e");
|
||||||
|
border-color: transparent;
|
||||||
|
background-color: currentColor;
|
||||||
|
background-size: 100% 100%;
|
||||||
|
background-position: center;
|
||||||
|
background-repeat: no-repeat; }
|
||||||
|
[type='checkbox']:indeterminate:hover, [type='checkbox']:indeterminate:focus { border-color: transparent;
|
||||||
|
background-color: currentColor; }
|
||||||
|
[type='file'] { background: unset;
|
||||||
|
border-color: inherit;
|
||||||
|
border-width: 0;
|
||||||
|
border-radius: 0;
|
||||||
|
padding: 0;
|
||||||
|
font-size: unset;
|
||||||
|
line-height: inherit; }
|
||||||
|
[type='file']:focus { outline: 1px solid ButtonText , 1px auto -webkit-focus-ring-color; }
|
||||||
|
/* layer: default */
|
||||||
|
.visible{visibility:visible;}
|
||||||
|
.relative{position:relative;}
|
||||||
|
.mt-10{margin-top:2.5rem;}
|
||||||
|
.mt-2{margin-top:0.5rem;}
|
||||||
|
.block{display:block;}
|
||||||
|
.contents{display:contents;}
|
||||||
|
.list-item{display:list-item;}
|
||||||
|
.hidden{display:none;}
|
||||||
|
.h6{height:1.5rem;}
|
||||||
|
.min-h-screen{min-height:100vh;}
|
||||||
|
.w-full{width:100%;}
|
||||||
|
.flex{display:flex;}
|
||||||
|
.flex-col{flex-direction:column;}
|
||||||
|
.table{display:table;}
|
||||||
|
.border-collapse{border-collapse:collapse;}
|
||||||
|
.transform{transform:translateX(var(--un-translate-x)) translateY(var(--un-translate-y)) translateZ(var(--un-translate-z)) rotate(var(--un-rotate)) rotateX(var(--un-rotate-x)) rotateY(var(--un-rotate-y)) rotateZ(var(--un-rotate-z)) skewX(var(--un-skew-x)) skewY(var(--un-skew-y)) scaleX(var(--un-scale-x)) scaleY(var(--un-scale-y)) scaleZ(var(--un-scale-z));}
|
||||||
|
.resize{resize:both;}
|
||||||
|
.items-center{align-items:center;}
|
||||||
|
.justify-center{justify-content:center;}
|
||||||
|
.justify-between{justify-content:space-between;}
|
||||||
|
.space-y-6>:not([hidden])~:not([hidden]){--un-space-y-reverse:0;margin-top:calc(1.5rem * calc(1 - var(--un-space-y-reverse)));margin-bottom:calc(1.5rem * var(--un-space-y-reverse));}
|
||||||
|
.border{border-width:1px;}
|
||||||
|
.border-0{border-width:0;}
|
||||||
|
.rounded-md{border-radius:0.375rem;}
|
||||||
|
.\!bg-indigo-600{--un-bg-opacity:1 !important;background-color:rgba(79,70,229,var(--un-bg-opacity)) !important;}
|
||||||
|
.hover\:\!bg-indigo-500:hover{--un-bg-opacity:1 !important;background-color:rgba(99,102,241,var(--un-bg-opacity)) !important;}
|
||||||
|
.px-3{padding-left:0.75rem;padding-right:0.75rem;}
|
||||||
|
.px-6{padding-left:1.5rem;padding-right:1.5rem;}
|
||||||
|
.py-1\.5{padding-top:0.375rem;padding-bottom:0.375rem;}
|
||||||
|
.py-12{padding-top:3rem;padding-bottom:3rem;}
|
||||||
|
.text-sm{font-size:0.875rem;line-height:1.25rem;}
|
||||||
|
.font-medium{font-weight:500;}
|
||||||
|
.font-semibold{font-weight:600;}
|
||||||
|
.leading-6{line-height:1.5rem;}
|
||||||
|
.text-gray-900{--un-text-opacity:1;color:rgba(17,24,39,var(--un-text-opacity));}
|
||||||
|
.text-white{--un-text-opacity:1;color:rgba(255,255,255,var(--un-text-opacity));}
|
||||||
|
.placeholder\:text-gray-400::placeholder{--un-text-opacity:1;color:rgba(156,163,175,var(--un-text-opacity));}
|
||||||
|
.underline{text-decoration-line:underline;}
|
||||||
|
.tab{-moz-tab-size:4;-o-tab-size:4;tab-size:4;}
|
||||||
|
.shadow-sm{--un-shadow:var(--un-shadow-inset) 0 1px 2px 0 var(--un-shadow-color, rgba(0,0,0,0.05));box-shadow:var(--un-ring-offset-shadow), var(--un-ring-shadow), var(--un-shadow);}
|
||||||
|
.focus-visible\:outline-2:focus-visible{outline-width:2px;}
|
||||||
|
.focus-visible\:outline-indigo-600:focus-visible{--un-outline-color-opacity:1;outline-color:rgba(79,70,229,var(--un-outline-color-opacity));}
|
||||||
|
.focus-visible\:outline-offset-2:focus-visible{outline-offset:2px;}
|
||||||
|
.outline{outline-style:solid;}
|
||||||
|
.focus-visible\:outline:focus-visible{outline-style:solid;}
|
||||||
|
.ring-1{--un-ring-width:1px;--un-ring-offset-shadow:var(--un-ring-inset) 0 0 0 var(--un-ring-offset-width) var(--un-ring-offset-color);--un-ring-shadow:var(--un-ring-inset) 0 0 0 calc(var(--un-ring-width) + var(--un-ring-offset-width)) var(--un-ring-color);box-shadow:var(--un-ring-offset-shadow), var(--un-ring-shadow), var(--un-shadow);}
|
||||||
|
.focus\:ring-2:focus{--un-ring-width:2px;--un-ring-offset-shadow:var(--un-ring-inset) 0 0 0 var(--un-ring-offset-width) var(--un-ring-offset-color);--un-ring-shadow:var(--un-ring-inset) 0 0 0 calc(var(--un-ring-width) + var(--un-ring-offset-width)) var(--un-ring-color);box-shadow:var(--un-ring-offset-shadow), var(--un-ring-shadow), var(--un-shadow);}
|
||||||
|
.ring-gray-300{--un-ring-opacity:1;--un-ring-color:rgba(209,213,219,var(--un-ring-opacity));}
|
||||||
|
.focus\:ring-indigo-600:focus{--un-ring-opacity:1;--un-ring-color:rgba(79,70,229,var(--un-ring-opacity));}
|
||||||
|
.ring-inset{--un-ring-inset:inset;}
|
||||||
|
.focus\:ring-inset:focus{--un-ring-inset:inset;}
|
||||||
|
@media (min-width: 640px){
|
||||||
|
.sm\:mx-auto{margin-left:auto;margin-right:auto;}
|
||||||
|
.sm\:max-w-sm{max-width:24rem;}
|
||||||
|
.sm\:w-full{width:100%;}
|
||||||
|
.sm\:text-sm{font-size:0.875rem;line-height:1.25rem;}
|
||||||
|
.sm\:leading-6{line-height:1.5rem;}
|
||||||
|
}
|
||||||
|
@media (min-width: 1024px){
|
||||||
|
.lg\:px-8{padding-left:2rem;padding-right:2rem;}
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||||
|
import { getConfig } from "@config";
|
||||||
import { errorResponse, jsonResponse } from "@response";
|
import { errorResponse, jsonResponse } from "@response";
|
||||||
import {
|
import {
|
||||||
APAccept,
|
APAccept,
|
||||||
|
|
@ -8,6 +9,7 @@ import {
|
||||||
APFollow,
|
APFollow,
|
||||||
APObject,
|
APObject,
|
||||||
APReject,
|
APReject,
|
||||||
|
APTombstone,
|
||||||
APUpdate,
|
APUpdate,
|
||||||
} from "activitypub-types";
|
} from "activitypub-types";
|
||||||
import { MatchedRoute } from "bun";
|
import { MatchedRoute } from "bun";
|
||||||
|
|
@ -26,6 +28,8 @@ export default async (
|
||||||
return errorResponse("Method not allowed", 405);
|
return errorResponse("Method not allowed", 405);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const config = getConfig();
|
||||||
|
|
||||||
// Process request body
|
// Process request body
|
||||||
const body: APActivity = await req.json();
|
const body: APActivity = await req.json();
|
||||||
|
|
||||||
|
|
@ -113,7 +117,35 @@ export default async (
|
||||||
|
|
||||||
if (!object) return errorResponse("Object not found", 404);
|
if (!object) return errorResponse("Object not found", 404);
|
||||||
|
|
||||||
await object.remove();
|
const activities = await RawActivity.findBy({
|
||||||
|
objects: {
|
||||||
|
id: (body.object as RawObject).id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (config.activitypub.use_tombstones) {
|
||||||
|
object.data = {
|
||||||
|
...object.data,
|
||||||
|
type: "Tombstone",
|
||||||
|
deleted: new Date(),
|
||||||
|
formerType: object.data.type,
|
||||||
|
} as APTombstone;
|
||||||
|
|
||||||
|
await object.save();
|
||||||
|
} else {
|
||||||
|
activities.forEach(
|
||||||
|
activity =>
|
||||||
|
(activity.objects = activity.objects.filter(
|
||||||
|
o => o.id !== object.id
|
||||||
|
))
|
||||||
|
);
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
activities.map(async activity => await activity.save())
|
||||||
|
);
|
||||||
|
|
||||||
|
await object.remove();
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "Accept" as APAccept: {
|
case "Accept" as APAccept: {
|
||||||
|
|
|
||||||
25
server/api/oauth/authorize/index.ts
Normal file
25
server/api/oauth/authorize/index.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
import { MatchedRoute } from "bun";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an HTML login form
|
||||||
|
*/
|
||||||
|
export default async (
|
||||||
|
req: Request,
|
||||||
|
matchedRoute: MatchedRoute
|
||||||
|
): Promise<Response> => {
|
||||||
|
const html = Bun.file("./pages/login.html");
|
||||||
|
const css = Bun.file("./pages/uno.css");
|
||||||
|
return new Response(
|
||||||
|
(await html.text())
|
||||||
|
.replace(
|
||||||
|
"{{URL}}",
|
||||||
|
`/auth/login?redirect_uri=${matchedRoute.query.redirect_uri}&response_type=${matchedRoute.query.response_type}&client_id=${matchedRoute.query.client_id}&scopes=${matchedRoute.query.scopes}`
|
||||||
|
)
|
||||||
|
.replace("{{STYLES}}", `<style>${await css.text()}</style>`),
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "text/html",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { parseRequest } from "@request";
|
||||||
import { errorResponse, jsonResponse } from "@response";
|
import { errorResponse, jsonResponse } from "@response";
|
||||||
import { Token } from "~database/entities/Token";
|
import { Token } from "~database/entities/Token";
|
||||||
|
|
||||||
|
|
@ -5,14 +6,15 @@ import { Token } from "~database/entities/Token";
|
||||||
* Allows getting token from OAuth code
|
* Allows getting token from OAuth code
|
||||||
*/
|
*/
|
||||||
export default async (req: Request): Promise<Response> => {
|
export default async (req: Request): Promise<Response> => {
|
||||||
const body = await req.formData();
|
const { grant_type, code, redirect_uri, client_id, client_secret, scope } =
|
||||||
|
await parseRequest<{
|
||||||
const grant_type = body.get("grant_type")?.toString() || null;
|
grant_type: string;
|
||||||
const code = body.get("code")?.toString() || "";
|
code: string;
|
||||||
const redirect_uri = body.get("redirect_uri")?.toString() || "";
|
redirect_uri: string;
|
||||||
const client_id = body.get("client_id")?.toString() || "";
|
client_id: string;
|
||||||
const client_secret = body.get("client_secret")?.toString() || "";
|
client_secret: string;
|
||||||
const scope = body.get("scope")?.toString() || null;
|
scope: string;
|
||||||
|
}>(req);
|
||||||
|
|
||||||
if (grant_type !== "authorization_code")
|
if (grant_type !== "authorization_code")
|
||||||
return errorResponse(
|
return errorResponse(
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { getConfig } from "@config";
|
import { getConfig } from "@config";
|
||||||
|
import { parseRequest } from "@request";
|
||||||
import { jsonResponse } from "@response";
|
import { jsonResponse } from "@response";
|
||||||
import { tempmailDomains } from "@tempmail";
|
import { tempmailDomains } from "@tempmail";
|
||||||
import { User } from "~database/entities/User";
|
import { User } from "~database/entities/User";
|
||||||
|
|
@ -9,14 +10,14 @@ import { User } from "~database/entities/User";
|
||||||
export default async (req: Request): Promise<Response> => {
|
export default async (req: Request): Promise<Response> => {
|
||||||
// TODO: Add Authorization check
|
// TODO: Add Authorization check
|
||||||
|
|
||||||
const body: {
|
const body = await parseRequest<{
|
||||||
username: string;
|
username: string;
|
||||||
email: string;
|
email: string;
|
||||||
password: string;
|
password: string;
|
||||||
agreement: boolean;
|
agreement: boolean;
|
||||||
locale: string;
|
locale: string;
|
||||||
reason: string;
|
reason: string;
|
||||||
} = await req.json();
|
}>(req);
|
||||||
|
|
||||||
const config = getConfig();
|
const config = getConfig();
|
||||||
|
|
||||||
|
|
@ -63,28 +64,28 @@ export default async (req: Request): Promise<Response> => {
|
||||||
config.validation.max_username_size;
|
config.validation.max_username_size;
|
||||||
|
|
||||||
// Check if username is valid
|
// Check if username is valid
|
||||||
if (!body.username.match(/^[a-zA-Z0-9_]+$/))
|
if (!body.username?.match(/^[a-zA-Z0-9_]+$/))
|
||||||
errors.details.username.push({
|
errors.details.username.push({
|
||||||
error: "ERR_INVALID",
|
error: "ERR_INVALID",
|
||||||
description: `must only contain letters, numbers, and underscores`,
|
description: `must only contain letters, numbers, and underscores`,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Check if username is too long
|
// Check if username is too long
|
||||||
if (body.username.length > config.validation.max_username_size)
|
if ((body.username?.length ?? 0) > config.validation.max_username_size)
|
||||||
errors.details.username.push({
|
errors.details.username.push({
|
||||||
error: "ERR_TOO_LONG",
|
error: "ERR_TOO_LONG",
|
||||||
description: `is too long (maximum is ${config.validation.max_username_size} characters)`,
|
description: `is too long (maximum is ${config.validation.max_username_size} characters)`,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Check if username is too short
|
// Check if username is too short
|
||||||
if (body.username.length < 3)
|
if ((body.username?.length ?? 0) < 3)
|
||||||
errors.details.username.push({
|
errors.details.username.push({
|
||||||
error: "ERR_TOO_SHORT",
|
error: "ERR_TOO_SHORT",
|
||||||
description: `is too short (minimum is 3 characters)`,
|
description: `is too short (minimum is 3 characters)`,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Check if username is reserved
|
// Check if username is reserved
|
||||||
if (config.validation.username_blacklist.includes(body.username))
|
if (config.validation.username_blacklist.includes(body.username ?? ""))
|
||||||
errors.details.username.push({
|
errors.details.username.push({
|
||||||
error: "ERR_RESERVED",
|
error: "ERR_RESERVED",
|
||||||
description: `is reserved`,
|
description: `is reserved`,
|
||||||
|
|
@ -99,7 +100,7 @@ export default async (req: Request): Promise<Response> => {
|
||||||
|
|
||||||
// Check if email is valid
|
// Check if email is valid
|
||||||
if (
|
if (
|
||||||
!body.email.match(
|
!body.email?.match(
|
||||||
/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
|
/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
@ -110,9 +111,9 @@ export default async (req: Request): Promise<Response> => {
|
||||||
|
|
||||||
// Check if email is blocked
|
// Check if email is blocked
|
||||||
if (
|
if (
|
||||||
config.validation.email_blacklist.includes(body.email) ||
|
config.validation.email_blacklist.includes(body.email ?? "") ||
|
||||||
(config.validation.blacklist_tempmail &&
|
(config.validation.blacklist_tempmail &&
|
||||||
tempmailDomains.domains.includes(body.email.split("@")[1]))
|
tempmailDomains.domains.includes((body.email ?? "").split("@")[1]))
|
||||||
)
|
)
|
||||||
errors.details.email.push({
|
errors.details.email.push({
|
||||||
error: "ERR_BLOCKED",
|
error: "ERR_BLOCKED",
|
||||||
|
|
@ -148,9 +149,9 @@ export default async (req: Request): Promise<Response> => {
|
||||||
|
|
||||||
const newUser = new User();
|
const newUser = new User();
|
||||||
|
|
||||||
newUser.username = body.username;
|
newUser.username = body.username ?? "";
|
||||||
newUser.email = body.email;
|
newUser.email = body.email ?? "";
|
||||||
newUser.password = await Bun.password.hash(body.password);
|
newUser.password = await Bun.password.hash(body.password ?? "");
|
||||||
|
|
||||||
// TODO: Return access token
|
// TODO: Return access token
|
||||||
return new Response();
|
return new Response();
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { getUserByToken } from "@auth";
|
import { getUserByToken } from "@auth";
|
||||||
import { getConfig } from "@config";
|
import { getConfig } from "@config";
|
||||||
|
import { parseRequest } from "@request";
|
||||||
import { errorResponse, jsonResponse } from "@response";
|
import { errorResponse, jsonResponse } from "@response";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -21,16 +22,18 @@ export default async (req: Request): Promise<Response> => {
|
||||||
if (!user) return errorResponse("Unauthorized", 401);
|
if (!user) return errorResponse("Unauthorized", 401);
|
||||||
|
|
||||||
const config = getConfig();
|
const config = getConfig();
|
||||||
const body = await req.formData();
|
|
||||||
|
|
||||||
const display_name = body.get("display_name")?.toString() || null;
|
const { display_name, note, avatar, header, locked, bot, discoverable } =
|
||||||
const note = body.get("note")?.toString() || null;
|
await parseRequest<{
|
||||||
// Avatar is a file element
|
display_name: string;
|
||||||
const avatar = (body.get("avatar") as File | null) || null;
|
note: string;
|
||||||
const header = (body.get("header") as File | null) || null;
|
avatar: File;
|
||||||
const locked = body.get("locked")?.toString() || null;
|
header: File;
|
||||||
const bot = body.get("bot")?.toString() || null;
|
locked: string;
|
||||||
const discoverable = body.get("discoverable")?.toString() || null;
|
bot: string;
|
||||||
|
discoverable: string;
|
||||||
|
}>(req);
|
||||||
|
|
||||||
// TODO: Implement other options like field or source
|
// TODO: Implement other options like field or source
|
||||||
// const source_privacy = body.get("source[privacy]")?.toString() || null;
|
// const source_privacy = body.get("source[privacy]")?.toString() || null;
|
||||||
// const source_sensitive = body.get("source[sensitive]")?.toString() || null;
|
// const source_sensitive = body.get("source[sensitive]")?.toString() || null;
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { parseRequest } from "@request";
|
||||||
import { errorResponse, jsonResponse } from "@response";
|
import { errorResponse, jsonResponse } from "@response";
|
||||||
import { randomBytes } from "crypto";
|
import { randomBytes } from "crypto";
|
||||||
import { Application } from "~database/entities/Application";
|
import { Application } from "~database/entities/Application";
|
||||||
|
|
@ -6,12 +7,12 @@ import { Application } from "~database/entities/Application";
|
||||||
* Creates a new application to obtain OAuth 2 credentials
|
* Creates a new application to obtain OAuth 2 credentials
|
||||||
*/
|
*/
|
||||||
export default async (req: Request): Promise<Response> => {
|
export default async (req: Request): Promise<Response> => {
|
||||||
const body = await req.formData();
|
const { client_name, redirect_uris, scopes, website } = await parseRequest<{
|
||||||
|
client_name: string;
|
||||||
const client_name = body.get("client_name")?.toString() || null;
|
redirect_uris: string;
|
||||||
const redirect_uris = body.get("redirect_uris")?.toString() || null;
|
scopes: string;
|
||||||
const scopes = body.get("scopes")?.toString() || null;
|
website: string;
|
||||||
const website = body.get("website")?.toString() || null;
|
}>(req);
|
||||||
|
|
||||||
const application = new Application();
|
const application = new Application();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,22 +0,0 @@
|
||||||
import { MatchedRoute } from "bun";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns an HTML login form
|
|
||||||
*/
|
|
||||||
export default async (
|
|
||||||
req: Request,
|
|
||||||
matchedRoute: MatchedRoute
|
|
||||||
): Promise<Response> => {
|
|
||||||
const html = Bun.file("./pages/login.html");
|
|
||||||
return new Response(
|
|
||||||
(await html.text()).replace(
|
|
||||||
"{{URL}}",
|
|
||||||
`/auth/login?redirect_uri=${matchedRoute.query.redirect_uri}&response_type=${matchedRoute.query.response_type}&client_id=${matchedRoute.query.client_id}&scopes=${matchedRoute.query.scopes}`
|
|
||||||
),
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "text/html",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -150,6 +150,62 @@ describe("POST /@test/inbox", () => {
|
||||||
published: "2021-01-01T00:00:00.000Z",
|
published: "2021-01-01T00:00:00.000Z",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("should delete the Note object", async () => {
|
||||||
|
const response = await fetch(
|
||||||
|
`${config.http.base_url}:${config.http.port}/@test/inbox/`,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/activity+json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
"@context": "https://www.w3.org/ns/activitystreams",
|
||||||
|
type: "Delete",
|
||||||
|
id: "https://example.com/notes/1/activity",
|
||||||
|
actor: `${config.http.base_url}:${config.http.port}/@test`,
|
||||||
|
to: ["https://www.w3.org/ns/activitystreams#Public"],
|
||||||
|
cc: [],
|
||||||
|
published: "2021-01-03T00:00:00.000Z",
|
||||||
|
object: {
|
||||||
|
"@context": "https://www.w3.org/ns/activitystreams",
|
||||||
|
id: "https://example.com/notes/1",
|
||||||
|
type: "Note",
|
||||||
|
content: "This note has been deleted!",
|
||||||
|
summary: null,
|
||||||
|
inReplyTo: null,
|
||||||
|
published: "2021-01-01T00:00:00.000Z",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
expect(response.headers.get("content-type")).toBe("application/json");
|
||||||
|
|
||||||
|
const activity = await RawActivity.createQueryBuilder("activity")
|
||||||
|
// Where id is part of the jsonb column 'data'
|
||||||
|
.where("activity.data->>'id' = :id", {
|
||||||
|
id: "https://example.com/notes/1/activity",
|
||||||
|
})
|
||||||
|
.leftJoinAndSelect("activity.objects", "objects")
|
||||||
|
// Sort by most recent
|
||||||
|
.orderBy("activity.data->>'published'", "DESC")
|
||||||
|
.getOne();
|
||||||
|
|
||||||
|
expect(activity).not.toBeUndefined();
|
||||||
|
expect(activity?.data).toEqual({
|
||||||
|
"@context": "https://www.w3.org/ns/activitystreams",
|
||||||
|
type: "Delete",
|
||||||
|
id: "https://example.com/notes/1/activity",
|
||||||
|
actor: `${config.http.base_url}:${config.http.port}/@test`,
|
||||||
|
to: ["https://www.w3.org/ns/activitystreams#Public"],
|
||||||
|
cc: [],
|
||||||
|
published: "2021-01-03T00:00:00.000Z",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(activity?.objects).toHaveLength(0);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
|
|
|
||||||
|
|
@ -89,7 +89,7 @@ describe("POST /auth/login/", () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("POST /v1/oauth/token/", () => {
|
describe("POST /oauth/token/", () => {
|
||||||
test("should get an access token", async () => {
|
test("should get an access token", async () => {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
|
|
||||||
|
|
@ -101,7 +101,7 @@ describe("POST /v1/oauth/token/", () => {
|
||||||
formData.append("scope", "read write");
|
formData.append("scope", "read write");
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${config.http.base_url}:${config.http.port}/v1/oauth/token/`,
|
`${config.http.base_url}:${config.http.port}/oauth/token/`,
|
||||||
{
|
{
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: formData,
|
body: formData,
|
||||||
|
|
|
||||||
0
tests/test-utils.ts
Normal file
0
tests/test-utils.ts
Normal file
22
uno.config.ts
Normal file
22
uno.config.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
import {
|
||||||
|
defineConfig,
|
||||||
|
presetUno,
|
||||||
|
presetTypography,
|
||||||
|
presetWebFonts,
|
||||||
|
} from "unocss";
|
||||||
|
import { presetForms } from "@julr/unocss-preset-forms";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
presets: [
|
||||||
|
presetUno(),
|
||||||
|
presetTypography({
|
||||||
|
cssExtend: {
|
||||||
|
"h1,h2,h3,h4,h5.h6": {
|
||||||
|
"font-family": "'Poppins'",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
presetWebFonts(),
|
||||||
|
presetForms(),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
@ -8,10 +8,12 @@ export interface ConfigType {
|
||||||
password: string;
|
password: string;
|
||||||
database: string;
|
database: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
http: {
|
http: {
|
||||||
port: number;
|
port: number;
|
||||||
base_url: string;
|
base_url: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
validation: {
|
validation: {
|
||||||
max_displayname_size: number;
|
max_displayname_size: number;
|
||||||
max_bio_size: number;
|
max_bio_size: number;
|
||||||
|
|
@ -28,11 +30,98 @@ export interface ConfigType {
|
||||||
email_blacklist: string[];
|
email_blacklist: string[];
|
||||||
url_scheme_whitelist: string[];
|
url_scheme_whitelist: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
activitypub: {
|
||||||
|
use_tombstones: boolean;
|
||||||
|
};
|
||||||
[key: string]: unknown;
|
[key: string]: unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const configDefaults: ConfigType = {
|
||||||
|
http: {
|
||||||
|
port: 3000,
|
||||||
|
base_url: "http://0.0.0.0",
|
||||||
|
},
|
||||||
|
database: {
|
||||||
|
host: "localhost",
|
||||||
|
port: 5432,
|
||||||
|
username: "postgres",
|
||||||
|
password: "postgres",
|
||||||
|
database: "fediproject",
|
||||||
|
},
|
||||||
|
validation: {
|
||||||
|
max_displayname_size: 50,
|
||||||
|
max_bio_size: 6000,
|
||||||
|
max_note_size: 5000,
|
||||||
|
max_avatar_size: 5_000_000,
|
||||||
|
max_header_size: 5_000_000,
|
||||||
|
max_media_size: 40_000_000,
|
||||||
|
max_media_attachments: 4,
|
||||||
|
max_media_description_size: 1000,
|
||||||
|
max_username_size: 30,
|
||||||
|
|
||||||
|
username_blacklist: [
|
||||||
|
".well-known",
|
||||||
|
"~",
|
||||||
|
"about",
|
||||||
|
"activities",
|
||||||
|
"api",
|
||||||
|
"auth",
|
||||||
|
"dev",
|
||||||
|
"inbox",
|
||||||
|
"internal",
|
||||||
|
"main",
|
||||||
|
"media",
|
||||||
|
"nodeinfo",
|
||||||
|
"notice",
|
||||||
|
"oauth",
|
||||||
|
"objects",
|
||||||
|
"proxy",
|
||||||
|
"push",
|
||||||
|
"registration",
|
||||||
|
"relay",
|
||||||
|
"settings",
|
||||||
|
"status",
|
||||||
|
"tag",
|
||||||
|
"users",
|
||||||
|
"web",
|
||||||
|
"search",
|
||||||
|
"mfa",
|
||||||
|
],
|
||||||
|
|
||||||
|
blacklist_tempmail: false,
|
||||||
|
|
||||||
|
email_blacklist: [],
|
||||||
|
|
||||||
|
url_scheme_whitelist: [
|
||||||
|
"http",
|
||||||
|
"https",
|
||||||
|
"ftp",
|
||||||
|
"dat",
|
||||||
|
"dweb",
|
||||||
|
"gopher",
|
||||||
|
"hyper",
|
||||||
|
"ipfs",
|
||||||
|
"ipns",
|
||||||
|
"irc",
|
||||||
|
"xmpp",
|
||||||
|
"ircs",
|
||||||
|
"magnet",
|
||||||
|
"mailto",
|
||||||
|
"mumble",
|
||||||
|
"ssb",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
activitypub: {
|
||||||
|
use_tombstones: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export const getConfig = () => {
|
export const getConfig = () => {
|
||||||
return data as ConfigType;
|
return {
|
||||||
|
...configDefaults,
|
||||||
|
...(data as ConfigType),
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getHost = () => {
|
export const getHost = () => {
|
||||||
|
|
|
||||||
54
utils/request.ts
Normal file
54
utils/request.ts
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
/**
|
||||||
|
* Takes a request, and turns FormData or query parameters
|
||||||
|
* into a JSON object as would be returned by req.json()
|
||||||
|
* This is a translation layer that allows clients to use
|
||||||
|
* either FormData, query parameters, or JSON in the request
|
||||||
|
* @param request The request to parse
|
||||||
|
*/
|
||||||
|
export async function parseRequest<T>(request: Request): Promise<Partial<T>> {
|
||||||
|
const formData = await request.formData();
|
||||||
|
const query = new URL(request.url).searchParams;
|
||||||
|
|
||||||
|
// if request contains a JSON body
|
||||||
|
if (request.headers.get("Content-Type")?.includes("application/json")) {
|
||||||
|
return (await request.json()) as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If request contains FormData
|
||||||
|
if (request.headers.get("Content-Type")?.includes("multipart/form-data")) {
|
||||||
|
if ([...formData.entries()].length > 0) {
|
||||||
|
const data: Record<string, string | File> = {};
|
||||||
|
|
||||||
|
for (const [key, value] of formData.entries()) {
|
||||||
|
// If object, parse as JSON
|
||||||
|
try {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-base-to-string
|
||||||
|
data[key] = JSON.parse(value.toString());
|
||||||
|
} catch {
|
||||||
|
// If a file, set as a file
|
||||||
|
if (value instanceof File) {
|
||||||
|
data[key] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, set as a string
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-base-to-string
|
||||||
|
data[key] = value.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return data as T;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ([...query.entries()].length > 0) {
|
||||||
|
const data: Record<string, string> = {};
|
||||||
|
|
||||||
|
for (const [key, value] of query.entries()) {
|
||||||
|
data[key] = value.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
return data as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {};
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue