Compare commits
2 commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7a04d1f7af | |||
| a2e035830c |
24 changed files with 1647 additions and 1644 deletions
9
.gitignore
vendored
9
.gitignore
vendored
|
|
@ -2,3 +2,12 @@ data/
|
||||||
.fsdb*
|
.fsdb*
|
||||||
public/files/*
|
public/files/*
|
||||||
.vscode/*
|
.vscode/*
|
||||||
|
|
||||||
|
|
||||||
|
# tim
|
||||||
|
ABOUT.md
|
||||||
|
AGENTS.md
|
||||||
|
API.md
|
||||||
|
ARCHITECTURE.md
|
||||||
|
CONTRIBUTING.md
|
||||||
|
REVIEW.md
|
||||||
68
ISSUES.md
Normal file
68
ISSUES.md
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
# Issues
|
||||||
|
|
||||||
|
|
||||||
|
## Creating Channels
|
||||||
|
* Creating a channel is weird from a UX perspective. You hit the plus arrow and a text field comes up, but there isn't any indication of how to submit it. Hitting Enter works, but not having a button is a weird UX choice.
|
||||||
|
* Hitting the plus icon again hides the text field but doesn't clear the value
|
||||||
|
* The plus button works as a visibility toggle but doesn't change from + to -. I would add a + button next to the Channels title that opens a mini form with a text field plus Create and Cancel buttons. You can have the Esc key close the dialog, but there should be clear buttons showing what does what.
|
||||||
|
|
||||||
|
## Chatting
|
||||||
|
|
||||||
|
* Attaching files doesn't really show what's happening; there should be a preview, whether link, image, or whatever.
|
||||||
|
* The chat bar should have a draggable area to drop files into.
|
||||||
|
* By default it seems 'react' gives a permission error? It also has a weird assortment of emoji's by default.
|
||||||
|
|
||||||
|
|
||||||
|
## Blurbs
|
||||||
|
|
||||||
|
* The + button acts as a hide/show toggle but doesn't change to - or whatever when it is shown.
|
||||||
|
* Hiding a blurb doesn't clear the fields.
|
||||||
|
* The character count seems to keep the previous count after posting a blurb.
|
||||||
|
* Need a filter and sort feature so you can see blurbs by topic or user.
|
||||||
|
* Need a subscription feature with notifications so you can see what a user is saying
|
||||||
|
* Need a read/unread feature so users can filter out read blurbs
|
||||||
|
* Is the goal to be able to respond to blurbs?
|
||||||
|
* Hashtags?
|
||||||
|
* Sharing and linking to Chats, Essays, and Forums
|
||||||
|
|
||||||
|
## Forum
|
||||||
|
|
||||||
|
* If this is supposed to be like a BBS forum should there be folders and/or topics?
|
||||||
|
* Need a way to respond
|
||||||
|
* Need a way to see unread posts
|
||||||
|
* Need a way to search, filter/subscribe and hide
|
||||||
|
* Do Forums need admins or owners who can block users, delete posts, etc?
|
||||||
|
* Sharing and linking to Chats, Essays, and Forums
|
||||||
|
|
||||||
|
## Essays
|
||||||
|
|
||||||
|
* Need a way to see unread posts
|
||||||
|
* Need a way to search, filter and hide
|
||||||
|
* Likes?
|
||||||
|
* Sharing and linking to Blurbs, Chats, and Forums
|
||||||
|
|
||||||
|
## Map
|
||||||
|
|
||||||
|
* Is the point to show where users are? If so users need to be able to set their location.
|
||||||
|
* Should users only be able to share their location with certain users, or everyone?
|
||||||
|
|
||||||
|
|
||||||
|
## General UI
|
||||||
|
|
||||||
|
* Log out button is very large and in a weird spot. Logging out is not a common activity so it shouldn't have such prominence.
|
||||||
|
* What is the purpose of the left-hand tray? Is it for eventually having access to multiple servers, or is it just a logo? It takes up real estate without providing a use. If the server feature isn't going to exist for a while, it should be removed and the logo placed elsewhere.
|
||||||
|
* The user's avatar is very large with some properties around it. This should probably follow the more common convention of being an icon in the top right, with the settings in a properties panel or page. Once set, the user isn't going to interact with the profile that much, so more room should be given to common operations.
|
||||||
|
* There isn't really a theme, or at least the look is very sparse. Setting just a color is fine, but it would be nice to have the ability to set a swatch or theme of different colors, like an IDE theme.
|
||||||
|
* Context menu icons are rough
|
||||||
|
* No server admin panel
|
||||||
|
* Need a way to admin users
|
||||||
|
* History of user actions
|
||||||
|
* Global search to find chats, blurbs, forums, or essays
|
||||||
|
|
||||||
|
## Accessibility
|
||||||
|
|
||||||
|
* Might need to look at the tabbing order and make sure everything is tabbable.
|
||||||
|
* Hotkeys for switching between areas
|
||||||
|
* Contrast is harsh for different colors in the theme. Colors for buttons, labels, and icons should be calculated to have adequate contrast with the primary color
|
||||||
|
* Light/dark theme
|
||||||
|
|
||||||
151
PERMISSIONS.md
Normal file
151
PERMISSIONS.md
Normal file
|
|
@ -0,0 +1,151 @@
|
||||||
|
# PERMISSIONS.md
|
||||||
|
|
||||||
|
Permission reference for `autonomous.contact`.
|
||||||
|
|
||||||
|
This document was built by crawling the codebase for:
|
||||||
|
|
||||||
|
- default permission assignment in [public/api/users/index.ts](public/api/users/index.ts)
|
||||||
|
- server-side permission checks in [public/api/](public/api)
|
||||||
|
- frontend permission gates in [public/tabs/](public/tabs) and related UI files
|
||||||
|
|
||||||
|
## Sources of truth
|
||||||
|
|
||||||
|
Current default permission sets are defined in [public/api/users/index.ts](public/api/users/index.ts#L14-L58).
|
||||||
|
|
||||||
|
- `DEFAULT_USER_PERMISSIONS` are assigned to normal users during signup.
|
||||||
|
- `DEFAULT_SUPERUSER_PERMISSIONS` are assigned to the first/bootstrap user.
|
||||||
|
|
||||||
|
## How permissions work
|
||||||
|
|
||||||
|
There are two layers of access control in this codebase:
|
||||||
|
|
||||||
|
1. **Global permission strings** stored on the user record.
|
||||||
|
2. **Object-level ACLs** stored inside resources, especially channel `permissions.read`, `permissions.write`, `permissions.events.read`, and `permissions.events.write`.
|
||||||
|
|
||||||
|
Important consequence: a user may have a global permission and still be blocked by a channel-level ACL.
|
||||||
|
|
||||||
|
## Generic permission families
|
||||||
|
|
||||||
|
These are not stored as standalone permissions, but the server checks for these prefixes:
|
||||||
|
|
||||||
|
- `events.create.*` via [public/api/events/index.ts](public/api/events/index.ts#L188) and [utils/prechecks.ts](utils/prechecks.ts#L47-L48)
|
||||||
|
- `events.write.*` via [public/api/events/:event_id/index.ts](public/api/events/:event_id/index.ts#L34-L35), [public/api/events/:event_id/index.ts](public/api/events/:event_id/index.ts#L117-L118), and [utils/prechecks.ts](utils/prechecks.ts#L50-L51)
|
||||||
|
|
||||||
|
## Permissions bible
|
||||||
|
|
||||||
|
| Permission | Default user | Bootstrap superuser | Purpose | Where checked | Notes |
|
||||||
|
| --- | --- | --- | --- | --- | --- |
|
||||||
|
| `channels.read` | yes | yes | Allows listing channels. | [public/api/channels/index.ts](public/api/channels/index.ts#L12) | Channel detail reads also require channel ACL membership; this permission gates the channel list endpoint only. |
|
||||||
|
| `channels.create` | no | yes | Allows creating channels. | [public/api/channels/index.ts](public/api/channels/index.ts#L31), [public/tabs/chat/channel_sidebar.html](public/tabs/chat/channel_sidebar.html#L189) | Fully wired. |
|
||||||
|
| `channels.delete` | no | yes | Intended to allow channel deletion. | Defined in [public/api/users/index.ts](public/api/users/index.ts#L51) | **No direct server-side check found.** Channel deletion currently uses `channel.permissions.write` membership instead of this string. See [public/api/channels/:channel_id/index.ts](public/api/channels/:channel_id/index.ts#L98-L100). |
|
||||||
|
| `channels.write` | no | yes | Intended to allow channel updates. | Defined in [public/api/users/index.ts](public/api/users/index.ts#L52) | **No direct server-side check found.** Channel updates currently use `channel.permissions.write` membership instead of this string. See [public/api/channels/:channel_id/index.ts](public/api/channels/:channel_id/index.ts#L45-L47). |
|
||||||
|
| `events.create.blurb` | yes | yes | Allows creating `blurb` events. | [utils/prechecks.ts](utils/prechecks.ts#L47-L48), [public/tabs/blurbs/new_blurb.html](public/tabs/blurbs/new_blurb.html#L28) | Matches server behavior. |
|
||||||
|
| `events.create.chat` | yes | yes | Allows creating `chat` events. | [utils/prechecks.ts](utils/prechecks.ts#L47-L48) | **UI mismatch:** chat composer is gated by `events.write.chat`, not `events.create.chat`. See [public/tabs/chat/chat.html](public/tabs/chat/chat.html#L151). |
|
||||||
|
| `events.create.essay` | yes | yes | Allows creating `essay` events. | [utils/prechecks.ts](utils/prechecks.ts#L47-L48), [public/tabs/essays/new_essay.html](public/tabs/essays/new_essay.html#L25) | Matches server behavior. |
|
||||||
|
| `events.create.post` | yes | yes | Allows creating `post` events. | [utils/prechecks.ts](utils/prechecks.ts#L47-L48), [public/tabs/forum/new_post.html](public/tabs/forum/new_post.html#L9) | Matches server behavior. |
|
||||||
|
| `events.create.presence` | yes | yes | Intended to allow creating `presence` events. | [utils/prechecks.ts](utils/prechecks.ts#L47-L48) | No dedicated UI or route-specific feature found beyond generic event creation. |
|
||||||
|
| `events.read.blurb` | yes | yes | Intended to allow viewing blurbs. | [public/tabs/blurbs/blurbs.html](public/tabs/blurbs/blurbs.html#L150) | **Frontend-only gate found.** No matching server-side event-read check by permission string was found. |
|
||||||
|
| `events.read.chat` | yes | yes | Intended to allow viewing chat. | [public/tabs/chat/chat.html](public/tabs/chat/chat.html#L23) | **Frontend-only gate found.** Server reads rely on authentication and channel ACLs, not this string. |
|
||||||
|
| `events.read.essay` | yes | yes | Intended to allow viewing essays. | [public/tabs/essays/essays.html](public/tabs/essays/essays.html#L126) | **Frontend-only gate found.** |
|
||||||
|
| `events.read.post` | yes | yes | Intended to allow viewing posts. | [public/tabs/forum/forum.html](public/tabs/forum/forum.html#L181) | **Frontend-only gate found.** |
|
||||||
|
| `events.read.presence` | yes | yes | Intended to allow viewing presence events. | Defined in [public/api/users/index.ts](public/api/users/index.ts#L27) | **No active check found** beyond the default assignment. |
|
||||||
|
| `events.write.blurb` | yes | yes | Allows updating/deleting `blurb` events. | [utils/prechecks.ts](utils/prechecks.ts#L50-L51) | Used by generic event update/delete endpoints. |
|
||||||
|
| `events.write.chat` | yes | yes | Allows updating/deleting `chat` events. | [utils/prechecks.ts](utils/prechecks.ts#L50-L51), [public/tabs/chat/chat.html](public/tabs/chat/chat.html#L151) | Also used by the chat composer UI, which appears to be stricter/different than server-side create rules. |
|
||||||
|
| `events.write.essay` | yes | yes | Allows updating/deleting `essay` events. | [utils/prechecks.ts](utils/prechecks.ts#L50-L51) | No dedicated UI gate found. |
|
||||||
|
| `events.write.post` | yes | yes | Allows updating/deleting `post` events. | [utils/prechecks.ts](utils/prechecks.ts#L50-L51) | No dedicated UI gate found. |
|
||||||
|
| `events.write.presence` | yes | yes | Intended to allow updating/deleting `presence` events. | [utils/prechecks.ts](utils/prechecks.ts#L50-L51) | No dedicated UI or route-specific usage found beyond generic event mutation logic. |
|
||||||
|
| `files.write.own` | yes | yes | Allows uploads only inside the current user's home path under `/files/users/<user-id>/...`. | [public/_pre.ts](public/_pre.ts#L12-L20) | Fully wired. |
|
||||||
|
| `files.write.all` | no | yes | Allows uploads anywhere under `/files/...`. | [public/_pre.ts](public/_pre.ts#L13-L20), [tests/11_file_uploads.test.ts](tests/11_file_uploads.test.ts#L165) | Fully wired. |
|
||||||
|
| `invites.create` | yes | yes | Allows creating invite codes. | [public/api/users/:user_id/invites/index.ts](public/api/users/:user_id/invites/index.ts#L73) | Fully wired. |
|
||||||
|
| `invites.read.own` | yes | yes | Allows reading invites for the path user when it is self. | [public/api/users/:user_id/invites/index.ts](public/api/users/:user_id/invites/index.ts#L17-L20) | Route behavior currently has filtering issues; see [REVIEW.md](REVIEW.md). |
|
||||||
|
| `invites.read.all` | no | yes | Allows reading invites across users. | [public/api/users/:user_id/invites/index.ts](public/api/users/:user_id/invites/index.ts#L18-L20) | Route behavior currently has filtering issues; see [REVIEW.md](REVIEW.md). |
|
||||||
|
| `self.read` | yes | yes | Allows reading the current authenticated user's own profile. | [public/api/users/me/index.ts](public/api/users/me/index.ts#L8), [public/api/users/:user_id/index.ts](public/api/users/:user_id/index.ts#L13-L17) | Fully wired. |
|
||||||
|
| `self.write` | yes | yes | Allows updating/deleting the current user's own profile. | [public/api/users/:user_id/index.ts](public/api/users/:user_id/index.ts#L44-L48), [public/api/users/:user_id/index.ts](public/api/users/:user_id/index.ts#L109-L113) | Fully wired. |
|
||||||
|
| `signups.read.own` | yes | yes | Allows reading signups for the path user when it is self. | [public/api/users/:user_id/signups/index.ts](public/api/users/:user_id/signups/index.ts#L13-L16) | Route behavior currently has filtering issues; see [REVIEW.md](REVIEW.md). |
|
||||||
|
| `signups.read.all` | no | yes | Allows reading signups across users. | [public/api/users/:user_id/signups/index.ts](public/api/users/:user_id/signups/index.ts#L14-L16) | Route behavior currently has filtering issues; see [REVIEW.md](REVIEW.md). |
|
||||||
|
| `users.read` | yes | yes | Allows reading/searching other users. | [public/api/users/index.ts](public/api/users/index.ts#L67), [public/api/users/:user_id/index.ts](public/api/users/:user_id/index.ts#L14-L17) | Fully wired. |
|
||||||
|
| `users.write` | no | yes | Allows updating/deleting other users and editing permissions. | [public/api/users/:user_id/index.ts](public/api/users/:user_id/index.ts#L45), [public/api/users/:user_id/index.ts](public/api/users/:user_id/index.ts#L81), [public/api/users/:user_id/index.ts](public/api/users/:user_id/index.ts#L110) | Fully wired. |
|
||||||
|
| `watches.create.own` | yes | yes | Allows creating watches for self. | [public/api/users/:user_id/watches/index.ts](public/api/users/:user_id/watches/index.ts#L80-L83) | Fully wired. |
|
||||||
|
| `watches.create.all` | no | no | Allows creating watches for other users. | [public/api/users/:user_id/watches/index.ts](public/api/users/:user_id/watches/index.ts#L81-L83) | **Referenced but not granted by either default permission set.** Must be assigned manually if needed. |
|
||||||
|
| `watches.read.own` | yes | yes | Allows reading watches for self. | [public/api/users/:user_id/watches/index.ts](public/api/users/:user_id/watches/index.ts#L15-L18) | Route behavior currently has filtering issues; see [REVIEW.md](REVIEW.md). |
|
||||||
|
| `watches.read.all` | no | yes | Allows reading watches across users. | [public/api/users/:user_id/watches/index.ts](public/api/users/:user_id/watches/index.ts#L16-L18) | Route behavior currently has filtering issues; see [REVIEW.md](REVIEW.md). |
|
||||||
|
| `watches.write.own` | yes | yes | Intended to allow updating/deleting own watches. | Defined in [public/api/users/index.ts](public/api/users/index.ts#L44) | **No direct permission check found.** Watch update/delete currently use ownership only. See [public/api/users/:user_id/watches/:watch_id/index.ts](public/api/users/:user_id/watches/:watch_id/index.ts#L20-L22) and [public/api/users/:user_id/watches/:watch_id/index.ts](public/api/users/:user_id/watches/:watch_id/index.ts#L73-L75). |
|
||||||
|
| `watches.write.all` | no | yes | Intended to allow updating/deleting others' watches. | Defined in [public/api/users/index.ts](public/api/users/index.ts#L58) | **No direct permission check found.** Watch update/delete currently use ownership only. |
|
||||||
|
|
||||||
|
## Summary: default permissions vs actual code
|
||||||
|
|
||||||
|
### Normal-user defaults that are clearly used
|
||||||
|
|
||||||
|
These are present in `DEFAULT_USER_PERMISSIONS` and have matching checks in code:
|
||||||
|
|
||||||
|
- `channels.read`
|
||||||
|
- `events.create.blurb`
|
||||||
|
- `events.create.chat`
|
||||||
|
- `events.create.essay`
|
||||||
|
- `events.create.post`
|
||||||
|
- `events.create.presence`
|
||||||
|
- `events.write.blurb`
|
||||||
|
- `events.write.chat`
|
||||||
|
- `events.write.essay`
|
||||||
|
- `events.write.post`
|
||||||
|
- `events.write.presence`
|
||||||
|
- `files.write.own`
|
||||||
|
- `invites.create`
|
||||||
|
- `invites.read.own`
|
||||||
|
- `self.read`
|
||||||
|
- `self.write`
|
||||||
|
- `signups.read.own`
|
||||||
|
- `users.read`
|
||||||
|
- `watches.create.own`
|
||||||
|
- `watches.read.own`
|
||||||
|
|
||||||
|
### Defaults that are only partially matched or frontend-only
|
||||||
|
|
||||||
|
- `events.read.blurb`
|
||||||
|
- `events.read.chat`
|
||||||
|
- `events.read.essay`
|
||||||
|
- `events.read.post`
|
||||||
|
- `events.read.presence`
|
||||||
|
- `watches.write.own`
|
||||||
|
|
||||||
|
### Bootstrap/superuser defaults that are present but not fully wired as permission strings
|
||||||
|
|
||||||
|
- `channels.delete`
|
||||||
|
- `channels.write`
|
||||||
|
- `watches.write.all`
|
||||||
|
|
||||||
|
### Referenced in code but not granted by default
|
||||||
|
|
||||||
|
- `watches.create.all`
|
||||||
|
|
||||||
|
## Biggest mismatches to know about
|
||||||
|
|
||||||
|
1. **Channel update/delete do not use `channels.write` or `channels.delete`.**
|
||||||
|
They use per-channel ACL membership instead. See [public/api/channels/:channel_id/index.ts](public/api/channels/:channel_id/index.ts).
|
||||||
|
|
||||||
|
2. **Watch update/delete do not use `watches.write.own` or `watches.write.all`.**
|
||||||
|
They use ownership checks only. See [public/api/users/:user_id/watches/:watch_id/index.ts](public/api/users/:user_id/watches/:watch_id/index.ts).
|
||||||
|
|
||||||
|
3. **`events.read.*` permissions are mostly UI gates, not server-enforced authorization.**
|
||||||
|
The event read endpoints do not generally check these strings.
|
||||||
|
|
||||||
|
4. **Chat creation UI uses `events.write.chat`, but the server requires `events.create.chat` for POST.**
|
||||||
|
This is the clearest create-vs-write mismatch in the current code.
|
||||||
|
|
||||||
|
5. **`watches.create.all` exists in code but is missing from both default sets.**
|
||||||
|
|
||||||
|
## Recommendation
|
||||||
|
|
||||||
|
If the goal is to make permissions predictable, the cleanest next step would be to choose one of these approaches:
|
||||||
|
|
||||||
|
1. remove unused permission strings from defaults, or
|
||||||
|
2. add explicit checks so every documented permission is actually authoritative
|
||||||
|
|
||||||
|
The most important cleanup targets are:
|
||||||
|
|
||||||
|
- `channels.write`
|
||||||
|
- `channels.delete`
|
||||||
|
- `watches.write.own`
|
||||||
|
- `watches.write.all`
|
||||||
|
- the `events.read.*` family
|
||||||
|
- the chat UI mismatch between `events.create.chat` and `events.write.chat`
|
||||||
|
|
@ -134,7 +134,7 @@ feature discussions.
|
||||||
|
|
||||||
3) Start the server:
|
3) Start the server:
|
||||||
|
|
||||||
`deno run task serve`
|
`deno run serve`
|
||||||
|
|
||||||
4) Navigate to http://localhost:8000
|
4) Navigate to http://localhost:8000
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -46,10 +46,10 @@
|
||||||
"@andyburke/lurid": "jsr:@andyburke/lurid@^0.2.0",
|
"@andyburke/lurid": "jsr:@andyburke/lurid@^0.2.0",
|
||||||
"@andyburke/serverus": "jsr:@andyburke/serverus@^0.16.0",
|
"@andyburke/serverus": "jsr:@andyburke/serverus@^0.16.0",
|
||||||
"@da/bcrypt": "jsr:@da/bcrypt@^1.0.1",
|
"@da/bcrypt": "jsr:@da/bcrypt@^1.0.1",
|
||||||
"@std/assert": "jsr:@std/assert@^1.0.19",
|
"@std/assert": "jsr:@std/assert@^1.0.17",
|
||||||
"@std/encoding": "jsr:@std/encoding@^1.0.10",
|
"@std/encoding": "jsr:@std/encoding@^1.0.10",
|
||||||
"@std/fs": "jsr:@std/fs@^1.0.23",
|
"@std/fs": "jsr:@std/fs@^1.0.22",
|
||||||
"@std/http": "jsr:@std/http@^1.0.25",
|
"@std/http": "jsr:@std/http@^1.0.23",
|
||||||
"@std/media-types": "jsr:@std/media-types@^1.1.0",
|
"@std/media-types": "jsr:@std/media-types@^1.1.0",
|
||||||
"@std/path": "jsr:@std/path@^1.1.4"
|
"@std/path": "jsr:@std/path@^1.1.4"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
63
deno.lock
generated
63
deno.lock
generated
|
|
@ -6,27 +6,28 @@
|
||||||
"jsr:@andyburke/serverus@0.16": "0.16.0",
|
"jsr:@andyburke/serverus@0.16": "0.16.0",
|
||||||
"jsr:@da/bcrypt@*": "1.0.1",
|
"jsr:@da/bcrypt@*": "1.0.1",
|
||||||
"jsr:@da/bcrypt@^1.0.1": "1.0.1",
|
"jsr:@da/bcrypt@^1.0.1": "1.0.1",
|
||||||
"jsr:@std/assert@^1.0.19": "1.0.19",
|
"jsr:@std/assert@^1.0.17": "1.0.17",
|
||||||
"jsr:@std/cli@^1.0.19": "1.0.28",
|
"jsr:@std/cli@^1.0.19": "1.0.25",
|
||||||
"jsr:@std/cli@^1.0.20": "1.0.28",
|
"jsr:@std/cli@^1.0.20": "1.0.25",
|
||||||
"jsr:@std/cli@^1.0.21": "1.0.28",
|
"jsr:@std/cli@^1.0.21": "1.0.25",
|
||||||
"jsr:@std/cli@^1.0.28": "1.0.28",
|
"jsr:@std/cli@^1.0.25": "1.0.25",
|
||||||
"jsr:@std/encoding@^1.0.10": "1.0.10",
|
"jsr:@std/encoding@^1.0.10": "1.0.10",
|
||||||
"jsr:@std/fmt@^1.0.6": "1.0.9",
|
"jsr:@std/fmt@^1.0.6": "1.0.8",
|
||||||
"jsr:@std/fmt@^1.0.9": "1.0.9",
|
"jsr:@std/fmt@^1.0.8": "1.0.8",
|
||||||
"jsr:@std/fs@^1.0.18": "1.0.23",
|
"jsr:@std/fs@^1.0.18": "1.0.22",
|
||||||
"jsr:@std/fs@^1.0.19": "1.0.23",
|
"jsr:@std/fs@^1.0.19": "1.0.22",
|
||||||
"jsr:@std/fs@^1.0.23": "1.0.23",
|
"jsr:@std/fs@^1.0.21": "1.0.22",
|
||||||
|
"jsr:@std/fs@^1.0.22": "1.0.22",
|
||||||
"jsr:@std/html@^1.0.5": "1.0.5",
|
"jsr:@std/html@^1.0.5": "1.0.5",
|
||||||
"jsr:@std/http@^1.0.20": "1.0.25",
|
"jsr:@std/http@^1.0.20": "1.0.23",
|
||||||
"jsr:@std/http@^1.0.25": "1.0.25",
|
"jsr:@std/http@^1.0.23": "1.0.23",
|
||||||
"jsr:@std/internal@^1.0.12": "1.0.12",
|
"jsr:@std/internal@^1.0.12": "1.0.12",
|
||||||
"jsr:@std/media-types@^1.1.0": "1.1.0",
|
"jsr:@std/media-types@^1.1.0": "1.1.0",
|
||||||
"jsr:@std/net@^1.0.6": "1.0.6",
|
"jsr:@std/net@^1.0.6": "1.0.6",
|
||||||
"jsr:@std/path@^1.1.0": "1.1.4",
|
"jsr:@std/path@^1.1.0": "1.1.4",
|
||||||
"jsr:@std/path@^1.1.1": "1.1.4",
|
"jsr:@std/path@^1.1.1": "1.1.4",
|
||||||
"jsr:@std/path@^1.1.4": "1.1.4",
|
"jsr:@std/path@^1.1.4": "1.1.4",
|
||||||
"jsr:@std/streams@^1.0.17": "1.0.17",
|
"jsr:@std/streams@^1.0.16": "1.0.16",
|
||||||
"npm:@types/node@*": "22.15.15"
|
"npm:@types/node@*": "22.15.15"
|
||||||
},
|
},
|
||||||
"jsr": {
|
"jsr": {
|
||||||
|
|
@ -58,23 +59,23 @@
|
||||||
"@da/bcrypt@1.0.1": {
|
"@da/bcrypt@1.0.1": {
|
||||||
"integrity": "d2172d3acbcff52e0465557a1a48b1ff1c92df08c90712dae5372255a8c45eb3"
|
"integrity": "d2172d3acbcff52e0465557a1a48b1ff1c92df08c90712dae5372255a8c45eb3"
|
||||||
},
|
},
|
||||||
"@std/assert@1.0.19": {
|
"@std/assert@1.0.17": {
|
||||||
"integrity": "eaada96ee120cb980bc47e040f82814d786fe8162ecc53c91d8df60b8755991e",
|
"integrity": "df5ebfffe77c03b3fa1401e11c762cc8f603d51021c56c4d15a8c7ab45e90dbe",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"jsr:@std/internal"
|
"jsr:@std/internal"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"@std/cli@1.0.28": {
|
"@std/cli@1.0.25": {
|
||||||
"integrity": "74ef9b976db59ca6b23a5283469c9072be6276853807a83ec6c7ce412135c70a"
|
"integrity": "1f85051b370c97a7a9dfc6ba626e7ed57a91bea8c081597276d1e78d929d8c91"
|
||||||
},
|
},
|
||||||
"@std/encoding@1.0.10": {
|
"@std/encoding@1.0.10": {
|
||||||
"integrity": "8783c6384a2d13abd5e9e87a7ae0520a30e9f56aeeaa3bdf910a3eaaf5c811a1"
|
"integrity": "8783c6384a2d13abd5e9e87a7ae0520a30e9f56aeeaa3bdf910a3eaaf5c811a1"
|
||||||
},
|
},
|
||||||
"@std/fmt@1.0.9": {
|
"@std/fmt@1.0.8": {
|
||||||
"integrity": "2487343e8899fb2be5d0e3d35013e54477ada198854e52dd05ed0422eddcabe0"
|
"integrity": "71e1fc498787e4434d213647a6e43e794af4fd393ef8f52062246e06f7e372b7"
|
||||||
},
|
},
|
||||||
"@std/fs@1.0.23": {
|
"@std/fs@1.0.22": {
|
||||||
"integrity": "3ecbae4ce4fee03b180fa710caff36bb5adb66631c46a6460aaad49515565a37",
|
"integrity": "de0f277a58a867147a8a01bc1b181d0dfa80bfddba8c9cf2bacd6747bcec9308",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"jsr:@std/internal",
|
"jsr:@std/internal",
|
||||||
"jsr:@std/path@^1.1.4"
|
"jsr:@std/path@^1.1.4"
|
||||||
|
|
@ -83,13 +84,13 @@
|
||||||
"@std/html@1.0.5": {
|
"@std/html@1.0.5": {
|
||||||
"integrity": "4e2d693f474cae8c16a920fa5e15a3b72267b94b84667f11a50c6dd1cb18d35e"
|
"integrity": "4e2d693f474cae8c16a920fa5e15a3b72267b94b84667f11a50c6dd1cb18d35e"
|
||||||
},
|
},
|
||||||
"@std/http@1.0.25": {
|
"@std/http@1.0.23": {
|
||||||
"integrity": "577b4252290af1097132812b339fffdd55fb0f4aeb98ff11bdbf67998aa17193",
|
"integrity": "6634e9e034c589bf35101c1b5ee5bbf052a5987abca20f903e58bdba85c80dee",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"jsr:@std/cli@^1.0.28",
|
"jsr:@std/cli@^1.0.25",
|
||||||
"jsr:@std/encoding",
|
"jsr:@std/encoding",
|
||||||
"jsr:@std/fmt@^1.0.9",
|
"jsr:@std/fmt@^1.0.8",
|
||||||
"jsr:@std/fs@^1.0.23",
|
"jsr:@std/fs@^1.0.21",
|
||||||
"jsr:@std/html",
|
"jsr:@std/html",
|
||||||
"jsr:@std/media-types",
|
"jsr:@std/media-types",
|
||||||
"jsr:@std/net",
|
"jsr:@std/net",
|
||||||
|
|
@ -112,8 +113,8 @@
|
||||||
"jsr:@std/internal"
|
"jsr:@std/internal"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"@std/streams@1.0.17": {
|
"@std/streams@1.0.16": {
|
||||||
"integrity": "7859f3d9deed83cf4b41f19223d4a67661b3d3819e9fc117698f493bf5992140"
|
"integrity": "85030627befb1767c60d4f65cb30fa2f94af1d6ee6e5b2515b76157a542e89c4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"npm": {
|
"npm": {
|
||||||
|
|
@ -136,10 +137,10 @@
|
||||||
"jsr:@andyburke/lurid@0.2",
|
"jsr:@andyburke/lurid@0.2",
|
||||||
"jsr:@andyburke/serverus@0.16",
|
"jsr:@andyburke/serverus@0.16",
|
||||||
"jsr:@da/bcrypt@^1.0.1",
|
"jsr:@da/bcrypt@^1.0.1",
|
||||||
"jsr:@std/assert@^1.0.19",
|
"jsr:@std/assert@^1.0.17",
|
||||||
"jsr:@std/encoding@^1.0.10",
|
"jsr:@std/encoding@^1.0.10",
|
||||||
"jsr:@std/fs@^1.0.23",
|
"jsr:@std/fs@^1.0.22",
|
||||||
"jsr:@std/http@^1.0.25",
|
"jsr:@std/http@^1.0.23",
|
||||||
"jsr:@std/media-types@^1.1.0",
|
"jsr:@std/media-types@^1.1.0",
|
||||||
"jsr:@std/path@^1.1.4"
|
"jsr:@std/path@^1.1.4"
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -122,7 +122,8 @@ export async function POST(req: Request, meta: Record<string, any>): Promise<Res
|
||||||
|
|
||||||
const session_result: SESSION_RESULT = await create_new_session({
|
const session_result: SESSION_RESULT = await create_new_session({
|
||||||
user,
|
user,
|
||||||
expires: body.session?.expires
|
expires: body.session?.expires,
|
||||||
|
request_url: req.url
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO: verify this redirect is relative?
|
// TODO: verify this redirect is relative?
|
||||||
|
|
@ -159,8 +160,17 @@ export type SESSION_RESULT = {
|
||||||
export type SESSION_INFO = {
|
export type SESSION_INFO = {
|
||||||
user: USER;
|
user: USER;
|
||||||
expires: string | undefined;
|
expires: string | undefined;
|
||||||
|
request_url?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function should_set_secure_cookies(request_url?: string): boolean {
|
||||||
|
if (!request_url) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new URL(request_url).protocol === 'https:';
|
||||||
|
}
|
||||||
|
|
||||||
// DELETE /api/auth - log out (delete session)
|
// DELETE /api/auth - log out (delete session)
|
||||||
PRECHECKS.DELETE = [get_session, get_user, require_user];
|
PRECHECKS.DELETE = [get_session, get_user, require_user];
|
||||||
const back_then = new Date(0).toUTCString();
|
const back_then = new Date(0).toUTCString();
|
||||||
|
|
@ -188,6 +198,7 @@ export async function create_new_session(session_settings: SESSION_INFO): Promis
|
||||||
const now = new Date().toISOString();
|
const now = new Date().toISOString();
|
||||||
const expires: string = session_settings.expires ??
|
const expires: string = session_settings.expires ??
|
||||||
new Date(new Date(now).valueOf() + DEFAULT_SESSION_TIME).toISOString();
|
new Date(new Date(now).valueOf() + DEFAULT_SESSION_TIME).toISOString();
|
||||||
|
const secure_attribute = should_set_secure_cookies(session_settings.request_url) ? '; Secure' : '';
|
||||||
|
|
||||||
crypto.getRandomValues(session_secret_buffer);
|
crypto.getRandomValues(session_secret_buffer);
|
||||||
|
|
||||||
|
|
@ -207,13 +218,13 @@ export async function create_new_session(session_settings: SESSION_INFO): Promis
|
||||||
const headers = new Headers();
|
const headers = new Headers();
|
||||||
|
|
||||||
const expires_in_utc = new Date(session.timestamps.expires).toUTCString();
|
const expires_in_utc = new Date(session.timestamps.expires).toUTCString();
|
||||||
headers.append('Set-Cookie', `${AUTHED_BEFORE_COOKIE_ID}=1; Path=/; Secure; Expires=${new Date(new Date(now).valueOf() + AUTHED_BEFORE_EXPIRATION).toUTCString()}`);
|
headers.append('Set-Cookie', `${AUTHED_BEFORE_COOKIE_ID}=1; Path=/${secure_attribute}; Expires=${new Date(new Date(now).valueOf() + AUTHED_BEFORE_EXPIRATION).toUTCString()}`);
|
||||||
headers.append('Set-Cookie', `${SESSION_ID_TOKEN}=${session.id}; Path=/; Secure; Expires=${expires_in_utc}`);
|
headers.append('Set-Cookie', `${SESSION_ID_TOKEN}=${session.id}; Path=/${secure_attribute}; Expires=${expires_in_utc}`);
|
||||||
headers.append(`x-${SESSION_ID_TOKEN}`, session.id);
|
headers.append(`x-${SESSION_ID_TOKEN}`, session.id);
|
||||||
|
|
||||||
// TODO: this wasn't really intended to be persisted in a cookie, but we are using it to
|
// TODO: this wasn't really intended to be persisted in a cookie, but we are using it to
|
||||||
// generate the TOTP for the call to /api/users/me
|
// generate the TOTP for the call to /api/users/me
|
||||||
headers.append('Set-Cookie', `${SESSION_SECRET_TOKEN}=${session.secret}; Path=/; Secure; Expires=${expires_in_utc}`);
|
headers.append('Set-Cookie', `${SESSION_SECRET_TOKEN}=${session.secret}; Path=/${secure_attribute}; Expires=${expires_in_utc}`);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
session,
|
session,
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ export async function GET(_req: Request, meta: Record<string, any>): Promise<Res
|
||||||
// POST /api/channels - Create a channel
|
// POST /api/channels - Create a channel
|
||||||
PRECHECKS.POST = [get_session, get_user, require_user, (_req: Request, meta: Record<string, any>): Response | undefined => {
|
PRECHECKS.POST = [get_session, get_user, require_user, (_req: Request, meta: Record<string, any>): Response | undefined => {
|
||||||
const can_create_channels = meta.user.permissions.includes('channels.create');
|
const can_create_channels = meta.user.permissions.includes('channels.create');
|
||||||
|
console.log('User permissions:', meta.user.permissions);
|
||||||
if (!can_create_channels) {
|
if (!can_create_channels) {
|
||||||
return CANNED_RESPONSES.permission_denied();
|
return CANNED_RESPONSES.permission_denied();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,13 @@ export async function GET(request: Request, meta: Record<string, any>): Promise<
|
||||||
event_id
|
event_id
|
||||||
} = /^.*\/events\/.*\/(?<event_type>.*?)\:(?<event_id>[A-Za-z-]+)\.json$/.exec(entry.path)?.groups ?? {};
|
} = /^.*\/events\/.*\/(?<event_type>.*?)\:(?<event_id>[A-Za-z-]+)\.json$/.exec(entry.path)?.groups ?? {};
|
||||||
|
|
||||||
|
console.dir({
|
||||||
|
entry,
|
||||||
|
event_type,
|
||||||
|
event_id,
|
||||||
|
query: meta.query
|
||||||
|
});
|
||||||
|
|
||||||
if (meta.query.after_id && event_id <= meta.query.after_id) {
|
if (meta.query.after_id && event_id <= meta.query.after_id) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -139,22 +139,6 @@ export async function POST(req: Request, meta: Record<string, any>): Promise<Res
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const password_verification_hash: string = body.password_verification_hash ?? (typeof body.password_verification === 'string'
|
|
||||||
? encodeBase64(
|
|
||||||
await crypto.subtle.digest('SHA-256', new TextEncoder().encode(body.password_verification))
|
|
||||||
)
|
|
||||||
: '');
|
|
||||||
if (password_verification_hash !== password_hash) {
|
|
||||||
return Response.json({
|
|
||||||
error: {
|
|
||||||
cause: 'invalid password verification hash',
|
|
||||||
message: 'Password and verification must be identical.'
|
|
||||||
}
|
|
||||||
}, {
|
|
||||||
status: 400
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const at_least_one_existing_user = (await USERS.all({
|
const at_least_one_existing_user = (await USERS.all({
|
||||||
limit: 1,
|
limit: 1,
|
||||||
offset: 0
|
offset: 0
|
||||||
|
|
@ -261,7 +245,8 @@ export async function POST(req: Request, meta: Record<string, any>): Promise<Res
|
||||||
|
|
||||||
const session_result: SESSION_RESULT = await create_new_session({
|
const session_result: SESSION_RESULT = await create_new_session({
|
||||||
user,
|
user,
|
||||||
expires: undefined
|
expires: undefined,
|
||||||
|
request_url: req.url
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO: verify this redirect is ok?
|
// TODO: verify this redirect is ok?
|
||||||
|
|
|
||||||
1377
public/base.css
1377
public/base.css
File diff suppressed because it is too large
Load diff
6
public/foo/index.html
Normal file
6
public/foo/index.html
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<body>
|
||||||
|
<h1>Hello World - foo</h1>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
1329
public/icons.css
1329
public/icons.css
File diff suppressed because it is too large
Load diff
|
|
@ -10,7 +10,6 @@
|
||||||
<link rel="icon" type="image/png" sizes="16x16" href="/icons/favicon-16x16.png" ></link>
|
<link rel="icon" type="image/png" sizes="16x16" href="/icons/favicon-16x16.png" ></link>
|
||||||
|
|
||||||
<link rel="stylesheet" href="/base.css"></link>
|
<link rel="stylesheet" href="/base.css"></link>
|
||||||
<link rel="stylesheet" href="/icons.css"></link>
|
|
||||||
<link rel="stylesheet" href="/files/custom.css"></link>
|
<link rel="stylesheet" href="/files/custom.css"></link>
|
||||||
|
|
||||||
<!-- inlining these to force them to be scoped for everything else -->
|
<!-- inlining these to force them to be scoped for everything else -->
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ function embed_vimeo(link_info) {
|
||||||
<iframe
|
<iframe
|
||||||
src="https://player.vimeo.com/video/${video_id}"
|
src="https://player.vimeo.com/video/${video_id}"
|
||||||
frameborder="0"
|
frameborder="0"
|
||||||
allow="fullscreen; picture-in-picture; clipboard-write; encrypted-media;"
|
allow="fullscreen; picture-in-picture; clipboard-write; encrypted-media; web-share"
|
||||||
referrerpolicy="strict-origin-when-cross-origin"
|
referrerpolicy="strict-origin-when-cross-origin"
|
||||||
title="Star Trek: Legacy"
|
title="Star Trek: Legacy"
|
||||||
loading="lazy"></iframe>
|
loading="lazy"></iframe>
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ function embed_youtube(link_info) {
|
||||||
<iframe
|
<iframe
|
||||||
src="https://www.youtube.com/embed/${video_id}"
|
src="https://www.youtube.com/embed/${video_id}"
|
||||||
title="YouTube video player"
|
title="YouTube video player"
|
||||||
allow="clipboard-write; encrypted-media; picture-in-picture;"
|
allow="clipboard-write; encrypted-media; picture-in-picture; web-share;"
|
||||||
referrerpolicy="strict-origin-when-cross-origin"
|
referrerpolicy="strict-origin-when-cross-origin"
|
||||||
allowfullscreen
|
allowfullscreen
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
|
|
|
||||||
|
|
@ -145,7 +145,7 @@
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
color: var(--text-primary);
|
color: var(--text);
|
||||||
letter-spacing: 0.5px;
|
letter-spacing: 0.5px;
|
||||||
transition:
|
transition:
|
||||||
background 0.25s,
|
background 0.25s,
|
||||||
|
|
@ -257,7 +257,7 @@
|
||||||
padding: 0.75rem;
|
padding: 0.75rem;
|
||||||
margin: 0 0.75rem 1rem 0;
|
margin: 0 0.75rem 1rem 0;
|
||||||
background: none;
|
background: none;
|
||||||
color: var(--text-primary);
|
color: var(--text);
|
||||||
border: 1px solid var(--border-highlight);
|
border: 1px solid var(--border-highlight);
|
||||||
border-radius: var(--border-radius);
|
border-radius: var(--border-radius);
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
|
|
@ -364,7 +364,7 @@
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
padding: 1rem;
|
padding: 0.75rem;
|
||||||
width: 6rem;
|
width: 6rem;
|
||||||
border-right: 1px solid var(--border-subtle);
|
border-right: 1px solid var(--border-subtle);
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
|
@ -421,7 +421,7 @@
|
||||||
#server-info {
|
#server-info {
|
||||||
position: relative;
|
position: relative;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
padding: 1rem 2rem;
|
padding: 0.75rem;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<div id="server-info">
|
<div id="server-info">
|
||||||
|
|
|
||||||
|
|
@ -17,9 +17,19 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
#auth-container {
|
#auth-container {
|
||||||
|
width: 95%;
|
||||||
|
max-height: calc(min(90vh,900px));
|
||||||
|
border-radius: 10px;
|
||||||
position: relative;
|
position: relative;
|
||||||
padding: 8em 4em;
|
background: var(--bg-lighter);
|
||||||
|
max-width: 40em;
|
||||||
|
margin: 0 auto;
|
||||||
|
border-radius: calc( var(--border-radius) * 2.5);
|
||||||
|
padding: 2em 1em;
|
||||||
overflow-y: scroll;
|
overflow-y: scroll;
|
||||||
|
|
||||||
|
transition: all 0.33s ease;
|
||||||
|
animation: zoomsettle 0.4s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
#signup-tab,
|
#signup-tab,
|
||||||
|
|
@ -98,10 +108,6 @@
|
||||||
<input id="signup-password" type="password" name="password" required />
|
<input id="signup-password" type="password" name="password" required />
|
||||||
<label class="placeholder" for="signup-password">password</label>
|
<label class="placeholder" for="signup-password">password</label>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<input id="signup-password-verification" type="password" name="password_verification" required />
|
|
||||||
<label class="placeholder" for="signup-password-verification">verify password</label>
|
|
||||||
</div>
|
|
||||||
<div>
|
<div>
|
||||||
<script>
|
<script>
|
||||||
APP.on( 'load', () => {
|
APP.on( 'load', () => {
|
||||||
|
|
|
||||||
|
|
@ -159,7 +159,7 @@
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
font-size: x-large;
|
font-size: x-large;
|
||||||
content: "#";
|
content: "#";
|
||||||
color: var(--text-primary);
|
color: var(--text);
|
||||||
}
|
}
|
||||||
|
|
||||||
#channel-list-container .channel-list > li.channel a {
|
#channel-list-container .channel-list > li.channel a {
|
||||||
|
|
|
||||||
|
|
@ -131,7 +131,7 @@
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0.1rem;
|
top: 0.1rem;
|
||||||
right: 0.1rem;
|
right: 0.1rem;
|
||||||
color: rgb(from var(--text-primary) r g b / 0.7);
|
color: rgb(from var(--text) r g b / 0.7);
|
||||||
border: none;
|
border: none;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -152,8 +152,7 @@
|
||||||
method="POST"
|
method="POST"
|
||||||
class="post-creation-form collapsible"
|
class="post-creation-form collapsible"
|
||||||
style="
|
style="
|
||||||
position: relative;
|
margin-top: 1rem
|
||||||
margin-top: 1rem;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
transition: all 0.5s;
|
transition: all 0.5s;
|
||||||
"
|
"
|
||||||
|
|
@ -222,7 +221,6 @@
|
||||||
reset-on-submit
|
reset-on-submit
|
||||||
focus-on-submit
|
focus-on-submit
|
||||||
enter-key-submits
|
enter-key-submits
|
||||||
accept-speech
|
|
||||||
></textarea>
|
></textarea>
|
||||||
|
|
||||||
<button id="chat-send" class="primary" aria-label="Send a message">
|
<button id="chat-send" class="primary" aria-label="Send a message">
|
||||||
|
|
|
||||||
|
|
@ -27,188 +27,10 @@ Deno.test({
|
||||||
const password_hash = encodeBase64(
|
const password_hash = encodeBase64(
|
||||||
await crypto.subtle.digest('SHA-256', new TextEncoder().encode(password))
|
await crypto.subtle.digest('SHA-256', new TextEncoder().encode(password))
|
||||||
);
|
);
|
||||||
const password_verification_hash = encodeBase64(
|
|
||||||
await crypto.subtle.digest('SHA-256', new TextEncoder().encode(password))
|
|
||||||
);
|
|
||||||
|
|
||||||
const info = await get_new_user(client, {
|
const info = await get_new_user(client, {
|
||||||
username,
|
username,
|
||||||
password_hash,
|
password_hash
|
||||||
password_verification_hash
|
|
||||||
});
|
|
||||||
|
|
||||||
asserts.assert(info);
|
|
||||||
asserts.assert(info.user);
|
|
||||||
asserts.assert(info.session);
|
|
||||||
asserts.assert(info.headers);
|
|
||||||
|
|
||||||
const user: USER = info.user;
|
|
||||||
|
|
||||||
asserts.assertEquals(user.username, username);
|
|
||||||
|
|
||||||
await delete_user(client, info);
|
|
||||||
} finally {
|
|
||||||
if (test_server_info) {
|
|
||||||
await test_server_info?.server?.stop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
Deno.test({
|
|
||||||
name: 'API - USERS - Create (fail on mismatched password verification hash)',
|
|
||||||
permissions: {
|
|
||||||
env: true,
|
|
||||||
read: true,
|
|
||||||
write: true,
|
|
||||||
net: true
|
|
||||||
},
|
|
||||||
fn: async () => {
|
|
||||||
let test_server_info: EPHEMERAL_SERVER | null = null;
|
|
||||||
try {
|
|
||||||
test_server_info = await get_ephemeral_listen_server();
|
|
||||||
const client: API_CLIENT = api({
|
|
||||||
prefix: '/api',
|
|
||||||
hostname: test_server_info.hostname,
|
|
||||||
port: test_server_info.port
|
|
||||||
});
|
|
||||||
|
|
||||||
const username = random_username();
|
|
||||||
const password = 'password';
|
|
||||||
const password_hash = encodeBase64(
|
|
||||||
await crypto.subtle.digest('SHA-256', new TextEncoder().encode(password))
|
|
||||||
);
|
|
||||||
const password_verification_hash = encodeBase64(
|
|
||||||
await crypto.subtle.digest('SHA-256', new TextEncoder().encode(password + '1'))
|
|
||||||
);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const info = await get_new_user(client, {
|
|
||||||
username,
|
|
||||||
password_hash,
|
|
||||||
password_verification_hash
|
|
||||||
});
|
|
||||||
|
|
||||||
asserts.fail('allowed user creation with mismatched password_verification_hash')
|
|
||||||
}
|
|
||||||
catch( error ) {
|
|
||||||
asserts.assert( error );
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
if (test_server_info) {
|
|
||||||
await test_server_info?.server?.stop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
Deno.test({
|
|
||||||
name: 'API - USERS - Create (mismatched password_verification)',
|
|
||||||
permissions: {
|
|
||||||
env: true,
|
|
||||||
read: true,
|
|
||||||
write: true,
|
|
||||||
net: true
|
|
||||||
},
|
|
||||||
fn: async () => {
|
|
||||||
let test_server_info: EPHEMERAL_SERVER | null = null;
|
|
||||||
try {
|
|
||||||
test_server_info = await get_ephemeral_listen_server();
|
|
||||||
const client: API_CLIENT = api({
|
|
||||||
prefix: '/api',
|
|
||||||
hostname: test_server_info.hostname,
|
|
||||||
port: test_server_info.port
|
|
||||||
});
|
|
||||||
|
|
||||||
const username = random_username();
|
|
||||||
const password = 'password';
|
|
||||||
|
|
||||||
try {
|
|
||||||
const info = await get_new_user(client, {
|
|
||||||
username,
|
|
||||||
password,
|
|
||||||
password_verification: password + '1'
|
|
||||||
});
|
|
||||||
|
|
||||||
asserts.fail( 'allowed account creation with mismatched password_verification' );
|
|
||||||
}
|
|
||||||
catch( error ) {
|
|
||||||
asserts.assert( error );
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
if (test_server_info) {
|
|
||||||
await test_server_info?.server?.stop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
Deno.test({
|
|
||||||
name: 'API - USERS - Create (auto-generate password in testing)',
|
|
||||||
permissions: {
|
|
||||||
env: true,
|
|
||||||
read: true,
|
|
||||||
write: true,
|
|
||||||
net: true
|
|
||||||
},
|
|
||||||
fn: async () => {
|
|
||||||
let test_server_info: EPHEMERAL_SERVER | null = null;
|
|
||||||
try {
|
|
||||||
test_server_info = await get_ephemeral_listen_server();
|
|
||||||
const client: API_CLIENT = api({
|
|
||||||
prefix: '/api',
|
|
||||||
hostname: test_server_info.hostname,
|
|
||||||
port: test_server_info.port
|
|
||||||
});
|
|
||||||
|
|
||||||
const username = random_username();
|
|
||||||
|
|
||||||
const info = await get_new_user(client, {
|
|
||||||
username,
|
|
||||||
});
|
|
||||||
|
|
||||||
asserts.assert(info);
|
|
||||||
asserts.assert(info.user);
|
|
||||||
asserts.assert(info.session);
|
|
||||||
asserts.assert(info.headers);
|
|
||||||
|
|
||||||
const user: USER = info.user;
|
|
||||||
|
|
||||||
asserts.assertEquals(user.username, username);
|
|
||||||
|
|
||||||
await delete_user(client, info);
|
|
||||||
} finally {
|
|
||||||
if (test_server_info) {
|
|
||||||
await test_server_info?.server?.stop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
Deno.test({
|
|
||||||
name: 'API - USERS - Create (auto-generate password_verification in testing)',
|
|
||||||
permissions: {
|
|
||||||
env: true,
|
|
||||||
read: true,
|
|
||||||
write: true,
|
|
||||||
net: true
|
|
||||||
},
|
|
||||||
fn: async () => {
|
|
||||||
let test_server_info: EPHEMERAL_SERVER | null = null;
|
|
||||||
try {
|
|
||||||
test_server_info = await get_ephemeral_listen_server();
|
|
||||||
const client: API_CLIENT = api({
|
|
||||||
prefix: '/api',
|
|
||||||
hostname: test_server_info.hostname,
|
|
||||||
port: test_server_info.port
|
|
||||||
});
|
|
||||||
|
|
||||||
const username = random_username();
|
|
||||||
const password = 'password';
|
|
||||||
|
|
||||||
const info = await get_new_user(client, {
|
|
||||||
username,
|
|
||||||
password
|
|
||||||
});
|
});
|
||||||
|
|
||||||
asserts.assert(info);
|
asserts.assert(info);
|
||||||
|
|
|
||||||
|
|
@ -119,18 +119,6 @@ export async function get_new_user(client: API_CLIENT, user_info?: Record<string
|
||||||
new_user_request_json.password = `${Math.round(Math.random() * 10)} - ${random_username()} ! ${random_username()}`;
|
new_user_request_json.password = `${Math.round(Math.random() * 10)} - ${random_username()} ! ${random_username()}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
|
||||||
!((typeof new_user_request_json.password_verification === 'string' && new_user_request_json.password_verification.length) ||
|
|
||||||
(typeof new_user_request_json.password_verification_hash === 'string' && new_user_request_json.password_verification_hash.length))
|
|
||||||
) {
|
|
||||||
if ( typeof new_user_request_json.password === 'string' ) {
|
|
||||||
new_user_request_json.password_verification = new_user_request_json.password;
|
|
||||||
}
|
|
||||||
else if ( typeof new_user_request_json.password_hash === 'string' ) {
|
|
||||||
new_user_request_json.password_verification_hash = new_user_request_json.password_hash;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (inviting_user_info) {
|
if (inviting_user_info) {
|
||||||
const invite_code = await client.fetch(`/users/${inviting_user_info.user?.id}/invites`, {
|
const invite_code = await client.fetch(`/users/${inviting_user_info.user?.id}/invites`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,10 @@ export function require_user(
|
||||||
meta: Record<string, any>
|
meta: Record<string, any>
|
||||||
): undefined | Response {
|
): undefined | Response {
|
||||||
if (!meta.user) {
|
if (!meta.user) {
|
||||||
|
console.dir({
|
||||||
|
require_user: true,
|
||||||
|
meta
|
||||||
|
});
|
||||||
return CANNED_RESPONSES.permission_denied();
|
return CANNED_RESPONSES.permission_denied();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue