Hub: accounts, IMVU linking by tagline, proxy sign-in, presence, console
CI / test (push) Failing after 42s

People sign up on the website and link IMVU accounts by putting a code in the account's tagline, which the
hub reads from IMVU's public API (the most recent proof wins; link caps per person). IMVU's age verification
or Access Pass make an account adult. T5DE's proxy signs in with a device code and keeps a WebSocket open,
reporting the IMVU account in use and the community products it wears; /online shows those who opted in.
Moderators get a console: who's online, people, link caps, bans, roles, and the audit log. Tests run the
website and API against an embedded Postgres and a fake IMVU API.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
dhkatzandClaude Opus 5.5 committed 2026-10-29 03:45:52 -07:00
1 parent 54e569bea3
commit b9dc3f0ec1
36 files changed
+3548 -5

No files matched your search

+82
View File
@@ -0,0 +1,82 @@
{{define "title"}}Account{{end}}
{{define "content"}}
<h1>Your account</h1>
<p class="lead">Signed in as {{.Person.Username}}{{if .Person.IsModerator}} <span class="tag gold">{{.Person.Role}}</span>{{end}}</p>
{{if .Error}}<div class="error">{{.Error}}</div>{{end}}
<div class="panel">
<h2>IMVU accounts <span class="count">{{len .Links}} of {{.Person.LinkCap}}</span></h2>
{{range .Links}}
<div class="account-row">
<div class="avatar">{{slice .Username 0 1 | upper}}</div>
<div class="account-main">
<div class="account-name">{{.Username}}</div>
<div class="tags">
{{if .Online}}<span class="tag online">online in T5DE</span>{{end}}
{{if .AgeVerified}}<span class="tag adult">age verified</span>{{else if .AccessPass}}<span class="tag adult">Access Pass</span>{{else}}<span class="tag">not age verified</span>{{end}}
<span class="tag">checked {{ago .CheckedAt}}</span>
</div>
{{if .Outfit}}
<div class="small muted">Wearing:</div>
<div class="outfit">{{range .Outfit}}<span class="item">{{.Name}}</span>{{end}}</div>
{{end}}
<div class="settings">
<form method="post" action="/account/links/{{.IMVUID}}/settings">
<input type="hidden" name="csrf" value="{{$.CSRF}}">
<label class="check"><input type="checkbox" name="badge" {{if .ShowBadge}}checked{{end}}> Show T5DE badge</label>
<label class="check"><input type="checkbox" name="presence" {{if .ShowPresence}}checked{{end}}> Show when I'm online in T5DE</label>
<button class="secondary">Save</button>
</form>
</div>
</div>
<div class="row-actions">
<form method="post" action="/account/links/{{.IMVUID}}/recheck"><input type="hidden" name="csrf" value="{{$.CSRF}}"><button class="secondary" title="Check age verification with IMVU again">Check again</button></form>
<form method="post" action="/account/links/{{.IMVUID}}/unlink"><input type="hidden" name="csrf" value="{{$.CSRF}}"><button class="secondary">Unlink</button></form>
</div>
</div>
{{else}}
<p class="empty">No IMVU accounts linked yet.</p>
{{end}}
</div>
{{range .Challenges}}
<div class="panel" id="link-{{.IMVUID}}">
<h2>Link {{.Username}}</h2>
<p>To prove {{.Username}} is yours:</p>
<ol class="steps">
<li>Put this code anywhere in {{.Username}}'s IMVU tagline (on your avatar card in the client, or your profile):<br>
<span class="code">{{.Code}}</span></li>
<li>Press Check. You can take the code out again once it's linked.</li>
</ol>
<div class="actions">
<form method="post" action="/account/link/{{.IMVUID}}/check"><input type="hidden" name="csrf" value="{{$.CSRF}}"><button>Check</button></form>
<form method="post" action="/account/link/{{.IMVUID}}/cancel"><input type="hidden" name="csrf" value="{{$.CSRF}}"><button class="secondary">Cancel</button></form>
<span class="muted small">The code works for a day.</span>
</div>
</div>
{{end}}
{{if .CanLink}}
<div class="panel">
<h2>Link an IMVU account</h2>
<form method="post" action="/account/link" class="inline">
<input type="hidden" name="csrf" value="{{.CSRF}}">
<input type="text" name="username" placeholder="IMVU username" required maxlength="64">
<button>Next</button>
</form>
<p class="muted small">Other people can't see which IMVU accounts are yours. Age-verified IMVU accounts (or with Access Pass)
can see adult community products; each account is checked on its own.</p>
</div>
{{else}}
<div class="panel"><p class="muted">You've linked as many IMVU accounts as you can ({{.Person.LinkCap}}). Ask a moderator if you need more.</p></div>
{{end}}
<div class="panel">
<h2>T5DE on your computers</h2>
<p class="muted">{{if .Signins}}Signed in on {{.Signins}} computer{{if ne .Signins 1}}s{{end}}.{{else}}Not signed in on any computer yet.{{end}}
To sign T5DE in, it shows a code to enter at <a href="/device">/device</a>.</p>
{{if .Signins}}
<form method="post" action="/account/proxies/signout"><input type="hidden" name="csrf" value="{{.CSRF}}"><button class="secondary">Sign out everywhere</button></form>
{{end}}
</div>
{{end}}
+45
View File
@@ -0,0 +1,45 @@
{{define "title"}}Console{{end}}
{{define "content"}}
<h1>Console</h1>
<p class="lead">Moderation. <a href="/console/people">People</a></p>
<div class="grid">
<div class="panel"><h2>People</h2><div class="stat">{{.Counts.People}}</div><p class="muted small">accounts</p></div>
<div class="panel"><h2>IMVU accounts</h2><div class="stat">{{.Counts.Links}}</div><p class="muted small">linked</p></div>
<div class="panel"><h2>Computers</h2><div class="stat">{{.Counts.Signins}}</div><p class="muted small">signed in</p></div>
</div>
<div class="panel" hx-get="/console/online" hx-trigger="every 5s" hx-swap="innerHTML">
{{template "console-online" .}}
</div>
<div class="panel">
<h2>Audit log</h2>
<table>
<tr><th>When</th><th>Who</th><th>What</th><th>Target</th><th>Detail</th></tr>
{{range .Audit}}
<tr><td class="muted">{{date .At}}</td><td>{{if .Actor}}{{.Actor}}{{else}}<span class="muted">hub</span>{{end}}</td><td>{{.Action}}</td><td>{{.Target}}</td>
<td class="muted small">{{range $k, $v := .Detail}}{{$k}}: {{$v}} {{end}}</td></tr>
{{else}}
<tr><td colspan="5" class="empty">Nothing yet.</td></tr>
{{end}}
</table>
</div>
{{end}}
{{define "console-online"}}
<h2>Online now <span class="count">{{len .Online}} IMVU accounts, {{.Connections}} connections</span></h2>
{{range .Online}}
<div class="account-row">
<div class="avatar">{{slice .Link.Username 0 1 | upper}}</div>
<div class="account-main">
<div class="account-name">{{.Link.Username}} <a class="small" href="/console/people/{{.Online.PersonID}}">person</a></div>
<div class="tags">
<span class="tag online">since {{ago .Online.Since}}</span>
{{if .Hidden}}<span class="tag">presence hidden</span>{{end}}
{{if .Link.Adult}}<span class="tag adult">adult</span>{{end}}
</div>
{{if .Online.Outfit}}<div class="outfit">{{range .Online.Outfit}}<span class="item">{{.Name}}</span>{{end}}</div>{{end}}
</div>
</div>
{{else}}
<p class="empty">Nobody.</p>
{{end}}
{{end}}
@@ -0,0 +1,25 @@
{{define "title"}}People{{end}}
{{define "content"}}
<h1>People</h1>
<p class="lead"><a href="/console">Console</a></p>
<div class="panel">
<form method="get" action="/console/people" class="inline">
<input type="search" name="q" value="{{.Search}}" placeholder="T5DE or IMVU username">
<button class="secondary">Search</button>
</form>
</div>
<div class="panel">
<table>
<tr><th>Username</th><th>Role</th><th>Link cap</th><th>Joined</th><th></th></tr>
{{range .People}}
<tr>
<td><a href="/console/people/{{.ID}}">{{.Username}}</a></td>
<td>{{.Role}}</td><td>{{.LinkCap}}</td><td class="muted">{{date .CreatedAt}}</td>
<td>{{if .Banned}}<span class="tag warn">banned</span>{{end}}</td>
</tr>
{{else}}
<tr><td colspan="5" class="empty">Nobody found.</td></tr>
{{end}}
</table>
</div>
{{end}}
@@ -0,0 +1,61 @@
{{define "title"}}{{.Subject.Username}}{{end}}
{{define "content"}}
<h1>{{.Subject.Username}}</h1>
<p class="lead"><a href="/console/people">People</a> · {{.Subject.Role}} · joined {{date .Subject.CreatedAt}}
{{if .Subject.Banned}}<span class="tag warn">banned: {{.Subject.BanReason}}</span>{{end}}</p>
<div class="panel">
<h2>IMVU accounts <span class="count">{{len .Links}} of {{.Subject.LinkCap}}</span></h2>
<p class="muted small">Only moderators see which IMVU accounts belong to one person.</p>
<table>
<tr><th>IMVU account</th><th>ID</th><th>Linked</th><th>Age</th><th>Shows</th></tr>
{{range .Links}}
<tr><td>{{.Username}}</td><td class="muted">{{.IMVUID}}</td><td class="muted">{{date .LinkedAt}}</td>
<td>{{if .AgeVerified}}verified{{else if .AccessPass}}Access Pass{{else}}<span class="muted">no</span>{{end}}</td>
<td class="small">{{if .ShowBadge}}badge {{end}}{{if .ShowPresence}}presence{{end}}</td></tr>
{{else}}
<tr><td colspan="5" class="empty">None.</td></tr>
{{end}}
</table>
<p class="muted small">Signed in on {{.Signins}} computer{{if ne .Signins 1}}s{{end}}.</p>
</div>
<div class="grid">
<div class="panel">
<h2>Link cap</h2>
<form method="post" action="/console/people/{{.Subject.ID}}/cap" class="inline">
<input type="hidden" name="csrf" value="{{.CSRF}}">
<input type="number" name="cap" value="{{.Subject.LinkCap}}" min="0" max="1000">
<button>Set</button>
</form>
<p class="muted small">How many IMVU accounts they can link (normally 10).</p>
</div>
<div class="panel">
<h2>Ban</h2>
{{if .Subject.Banned}}
<form method="post" action="/console/people/{{.Subject.ID}}/unban"><input type="hidden" name="csrf" value="{{.CSRF}}"><button class="secondary">Unban</button></form>
{{else}}
<form method="post" action="/console/people/{{.Subject.ID}}/ban">
<input type="hidden" name="csrf" value="{{.CSRF}}">
<input type="text" name="reason" placeholder="Reason (they see it)" required maxlength="500">
<div class="actions"><button class="danger">Ban</button></div>
</form>
<p class="muted small">Covers all their IMVU accounts, and signs them out everywhere.</p>
{{end}}
</div>
{{if .Person.IsAdmin}}
<div class="panel">
<h2>Role</h2>
<form method="post" action="/console/people/{{.Subject.ID}}/role" class="inline">
<input type="hidden" name="csrf" value="{{.CSRF}}">
<select name="role">
<option value="user"{{if eq .Subject.Role "user"}} selected{{end}}>User</option>
<option value="moderator"{{if eq .Subject.Role "moderator"}} selected{{end}}>Moderator</option>
<option value="admin"{{if eq .Subject.Role "admin"}} selected{{end}}>Admin</option>
</select>
<button>Set</button>
</form>
</div>
{{end}}
</div>
{{end}}
+20
View File
@@ -0,0 +1,20 @@
{{define "title"}}Sign T5DE in{{end}}
{{define "content"}}
<div class="panel narrow">
{{if .Approved}}
<h1>T5DE is signed in</h1>
<p class="muted">You can close this page and go back to T5DE.</p>
{{else}}
<h1>Sign T5DE in</h1>
<p class="muted">Enter the code T5DE shows, to sign it in as {{.Person.Username}}.</p>
{{if .Error}}<div class="error">{{.Error}}</div>{{end}}
<form method="post" action="/device">
<input type="hidden" name="csrf" value="{{.CSRF}}">
<label for="code">Code</label>
<input type="text" id="code" name="code" value="{{.Code}}" placeholder="XXXX-XXXX" required autofocus autocomplete="off">
<div class="actions"><button>Sign in</button></div>
</form>
<p class="muted small">Only enter a code you got from T5DE yourself.</p>
{{end}}
</div>
{{end}}
+8
View File
@@ -0,0 +1,8 @@
{{define "title"}}Sorry{{end}}
{{define "content"}}
<div class="panel narrow">
<h1>Sorry</h1>
<p>{{.Message}}</p>
<div class="actions"><a class="button secondary" href="/">Home</a></div>
</div>
{{end}}
+33
View File
@@ -0,0 +1,33 @@
{{define "title"}}Home{{end}}
{{define "content"}}
<div class="hero">
<h1>T5DE Hub</h1>
<p class="lead">Your T5DE account: link your IMVU accounts, see who's online in T5DE, and soon, community products.</p>
{{if not .Person}}
<div class="actions">
<a class="button" href="/signup">Create account</a>
<a class="button secondary" href="/signin">Sign in</a>
</div>
{{end}}
</div>
<div class="grid">
<div class="panel">
<h2>Online in T5DE</h2>
<div class="stat">{{.OnlineCount}}</div>
<p class="muted small">people showing that they're online. <a href="/online">See who</a></p>
</div>
{{if .Person}}
<div class="panel">
<h2>Your IMVU accounts</h2>
<div class="stat">{{len .Links}}</div>
<p class="muted small">linked. <a href="/account">Manage</a></p>
</div>
{{else}}
<div class="panel">
<h2>One account, all your avatars</h2>
<p class="muted small">Link any of your IMVU accounts by putting a code in its tagline. Your accounts stay private:
nobody else can see they're yours.</p>
</div>
{{end}}
</div>
{{end}}
+40
View File
@@ -0,0 +1,40 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{template "title" .}} · T5DE Hub</title>
<meta name="htmx-config" content='{"includeIndicatorStyles": false}'>
<link rel="icon" href="/static/favicon.svg" type="image/svg+xml">
<link rel="stylesheet" href="/static/style.css">
<script src="/static/htmx.min.js" defer></script>
</head>
<body hx-headers='{"X-CSRF-Token": "{{.CSRF}}"}'>
<header class="top">
<div class="wrap">
<a class="brand" href="/"><span class="logo">T5DE</span> Hub</a>
<nav>
<a href="/online"{{if eq .Page "online"}} class="here"{{end}}>Online</a>
{{if .Person}}
<a href="/account"{{if eq .Page "account"}} class="here"{{end}}>Account</a>
{{if .Person.IsModerator}}<a href="/console"{{if eq .Page "console"}} class="here"{{end}}>Console</a>{{end}}
<span class="right">
<span class="muted small">{{.Person.Username}}</span>
<form method="post" action="/signout"><input type="hidden" name="csrf" value="{{.CSRF}}"><button class="secondary">Sign out</button></form>
</span>
{{else}}
<span class="right">
<a href="/signin">Sign in</a>
<a class="button" href="/signup">Create account</a>
</span>
{{end}}
</nav>
</div>
</header>
<main class="wrap">
{{if .Flash}}<div class="flash">{{.Flash}}</div>{{end}}
{{template "content" .}}
</main>
<footer class="wrap muted">T5DE is unofficial: not made by or affiliated with IMVU.</footer>
</body>
</html>
+27
View File
@@ -0,0 +1,27 @@
{{define "title"}}Online{{end}}
{{define "content"}}
<h1>Online in T5DE</h1>
<p class="lead">IMVU accounts in a T5DE client right now, that chose to show it.</p>
<div class="panel" hx-get="/online/list" hx-trigger="every 5s" hx-swap="innerHTML">
{{template "online-list" .}}
</div>
{{end}}
{{define "online-list"}}
{{range .Online}}
<div class="account-row">
<div class="avatar">{{slice .Link.Username 0 1 | upper}}</div>
<div class="account-main">
<div class="account-name">{{.Link.Username}}</div>
<div class="tags">
{{if .Link.ShowBadge}}<span class="tag gold">T5DE</span>{{end}}
<span class="tag online">online {{ago .Online.Since}}</span>
{{if .Hidden}}<span class="tag warn">hidden: moderators only</span>{{end}}
</div>
{{if .Online.Outfit}}<div class="outfit">{{range .Online.Outfit}}<span class="item">{{.Name}}</span>{{end}}</div>{{end}}
</div>
</div>
{{else}}
<p class="empty">Nobody is showing that they're online right now.</p>
{{end}}
{{end}}
+19
View File
@@ -0,0 +1,19 @@
{{define "title"}}Sign in{{end}}
{{define "content"}}
<div class="panel narrow">
<h1>Sign in</h1>
<p class="muted">to your T5DE account.</p>
{{if .Error}}<div class="error">{{.Error}}</div>{{end}}
<form method="post" action="/signin">
<input type="hidden" name="next" value="{{.Next}}">
<label for="username">Username</label>
<input type="text" id="username" name="username" value="{{.Username}}" autocomplete="username" required autofocus>
<label for="password">Password</label>
<input type="password" id="password" name="password" autocomplete="current-password" required>
<div class="actions">
<button>Sign in</button>
<span class="muted small">No account? <a href="/signup">Create one</a></span>
</div>
</form>
</div>
{{end}}
+22
View File
@@ -0,0 +1,22 @@
{{define "title"}}Create account{{end}}
{{define "content"}}
<div class="panel narrow">
<h1>Create account</h1>
<p class="muted">One T5DE account for all your IMVU accounts.</p>
{{if .Error}}<div class="error">{{.Error}}</div>{{end}}
<form method="post" action="/signup">
<label for="username">Username</label>
<input type="text" id="username" name="username" value="{{.Username}}" autocomplete="username" required autofocus
pattern="[A-Za-z0-9_]{3,32}" title="3 to 32 letters, digits or underscores">
<label for="password">Password</label>
<input type="password" id="password" name="password" autocomplete="new-password" required minlength="10">
<label for="password2">Password again</label>
<input type="password" id="password2" name="password2" autocomplete="new-password" required minlength="10">
<p class="muted small">Not your IMVU password: use a new one.</p>
<div class="actions">
<button>Create account</button>
<span class="muted small">Have one? <a href="/signin">Sign in</a></span>
</div>
</form>
</div>
{{end}}