S3+S4: user param auth, static file serving, Docker deploy config

- replace hardcoded benji with ?user= query param
- add GET /api/users and GET /api/me?user= endpoints
- serve frontend static files via tower-http ServeDir
- add multi-stage Dockerfile (Rust + Vite → single image)
- add docker-compose.yml + Caddyfile for apes.unslope.com
- frontend: getCurrentUsername() from URL param → localStorage
- sidebar shows current user

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-29 19:18:06 +02:00
parent 0b6244390e
commit f8420496b2
8 changed files with 156 additions and 10 deletions

View File

@@ -1,7 +1,7 @@
import { useCallback, useEffect, useRef, useState } from "react";
import type { Channel } from "@/types/Channel";
import type { Message } from "@/types/Message";
import { getChannels, getMessages } from "@/api";
import { getChannels, getMessages, getCurrentUsername } from "@/api";
import { ChannelSidebar } from "@/components/ChannelSidebar";
import { MessageItem } from "@/components/MessageItem";
import { ComposeBox } from "@/components/ComposeBox";

View File

@@ -1,22 +1,46 @@
import type { Channel } from "./types/Channel";
import type { Message } from "./types/Message";
import type { User } from "./types/User";
import type { CreateChannel } from "./types/CreateChannel";
import type { PostMessage } from "./types/PostMessage";
const BASE = "/api";
// Get current user from URL param or localStorage
export function getCurrentUsername(): string {
const url = new URL(window.location.href);
const param = url.searchParams.get("user");
if (param) {
localStorage.setItem("colony_user", param);
return param;
}
return localStorage.getItem("colony_user") || "benji";
}
function userQuery(): string {
return `user=${encodeURIComponent(getCurrentUsername())}`;
}
async function json<T>(res: Response): Promise<T> {
if (!res.ok) throw new Error(`${res.status}: ${await res.text()}`);
return res.json();
}
export async function getMe(): Promise<User> {
return json(await fetch(`${BASE}/me?${userQuery()}`));
}
export async function getUsers(): Promise<User[]> {
return json(await fetch(`${BASE}/users`));
}
export async function getChannels(): Promise<Channel[]> {
return json(await fetch(`${BASE}/channels`));
}
export async function createChannel(body: CreateChannel): Promise<Channel> {
return json(
await fetch(`${BASE}/channels`, {
await fetch(`${BASE}/channels?${userQuery()}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
@@ -26,10 +50,10 @@ export async function createChannel(body: CreateChannel): Promise<Channel> {
export async function getMessages(
channelId: string,
params?: { since?: string; type?: string; user_id?: string },
params?: { after_seq?: number; type?: string; user_id?: string },
): Promise<Message[]> {
const query = new URLSearchParams();
if (params?.since) query.set("since", params.since);
if (params?.after_seq) query.set("after_seq", String(params.after_seq));
if (params?.type) query.set("type", params.type);
if (params?.user_id) query.set("user_id", params.user_id);
const qs = query.toString();
@@ -43,7 +67,7 @@ export async function postMessage(
body: PostMessage,
): Promise<Message> {
return json(
await fetch(`${BASE}/channels/${channelId}/messages`, {
await fetch(`${BASE}/channels/${channelId}/messages?${userQuery()}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),

View File

@@ -1,6 +1,6 @@
import { useState } from "react";
import type { Channel } from "@/types/Channel";
import { createChannel } from "@/api";
import { createChannel, getCurrentUsername } from "@/api";
interface Props {
channels: Channel[];
@@ -61,6 +61,12 @@ export function ChannelSidebar({
))}
</div>
{/* Current user */}
<div className="px-3 py-2 border-t border-sidebar-border text-[11px]">
<span className="text-muted-foreground">logged in as </span>
<span className="font-bold text-foreground">{getCurrentUsername()}</span>
</div>
{/* New channel input */}
<div className="p-2 border-t border-sidebar-border">
<input