Compare commits

...
Sign in to create a new pull request.

2 commits
dev ... dev

Author SHA1 Message Date
7a04d1f7af spelling grammar 2026-03-09 14:36:05 -04:00
a2e035830c First draft of UX issues 2026-03-09 14:32:35 -04:00
7 changed files with 247 additions and 7 deletions

9
.gitignore vendored
View file

@ -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
View 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
View 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`

View file

@ -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

View file

@ -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,

View file

@ -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();
} }

View file

@ -245,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?