Port 4ducky to Rust (Axum + SQLite)

Same design, layout, URLs and features as the PHP version: browse grid,
image view with AJAX comments, BBCode, likes, uploads (file, URL and
bookmarklet), quicktagger, profile, block list, inbox, invites, Hall of
Ducks, Truhe, abuse reports and the admin tools.

- Parameterized SQL, escaped templates (askama), argon2 passwords,
  server-side sessions, CSRF tokens on every write, strict script CSP
- SSRF-safe remote image fetching, magic-byte upload checks
- Self-hosted captcha replaces reCAPTCHA v1; analytics/social widgets removed
- Vanilla JS replaces jQuery and its plugins with the same DOM behaviour
- Reverse proxy support (frontend_path, trust_proxy, secure_cookies)
- Admin CLI: admin, settings, invites

See README.md for running it and the full list of changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-17 02:15:05 +02:00
co-authored by Claude Opus 5.5
parent 664d8c6117
commit d0f2d526df
56 changed files with 11133 additions and 0 deletions
+159
View File
@@ -0,0 +1,159 @@
{% include "header.html" %}
<div align="center">
<form action="/profile" enctype="multipart/form-data" method="post">
<input type="hidden" name="_csrf" value="{{ l.csrf }}"/>
<fieldset>
<legend>Change Profile</legend>
{%- if m.pass_short %}
<div class="warn">Your password must be at least 6 characters long!</div>
{%- endif %}
{%- if m.pass_not_equal %}
<div class="warn">Your both passwords are not equal!</div>
{%- endif %}
{%- if m.avatar_failed %}
<div class="warn">Your avatar Image could not be processed!</div>
{%- endif %}
{%- if m.color_failed %}
<div class="warn">Not a valid colorcode!</div>
{%- endif %}
<dl class="form">
<dt>Passwort:</dt>
<dd>
<input type="password" name="cpass" /> (leave empty, if you don't want to change it)
</dd>
<dt>(Repeat)</dt>
<dd>
<input type="password" name="cpass2" />
</dd>
{%- if !has_email %}
<dt>E-Mail:</dt>
<dd>
<input type="text" name="email" />
</dd>
{%- endif %}
<dt>Website:</dt>
<dd>
<input type="text" name="website" value="{{ website }}"/>
</dd>
<dt>Avatar:</dt>
<dd>
<input type="file" name="avatar" accept="image/jpeg,image/png,image/gif" style="color: #000; background-color: #fff;"/><br/>
</dd>
<dt>Color:</dt>
<dd>
<input class="color" maxlength="6" type="text" name="color" value="{{ color }}" style="width:100px;"/> (default:: 000000)
<br/>
</dd>
<dt>Style:</dt>
<dd>
<select name="stylecolor">
{%- if style == "white" %}<option value="white">White</option><option value="black">Black</option>{% else %}<option value="black">Black</option><option value="white">White</option>{% endif %}
</select>
</dd>
<dt>&nbsp;</dt>
<dd>
<input type="submit" name="save" class="button" value="Save" />
</dd>
</dl>
</fieldset>
</form>
<form action="/profile" enctype="multipart/form-data" method="post">
<input type="hidden" name="_csrf" value="{{ l.csrf }}"/>
<fieldset>
<legend>Block user</legend>
{%- if m.blocked %}
<div class="success">User blocked!</div>
{%- endif %}
{%- if m.block_failed %}
<div class="warn">Could not block user!</div>
{%- endif %}
{%- if m.block_already %}
<div class="warn">User already blocked!</div>
{%- endif %}
{%- if m.user_failed %}
<div class="warn">User not found!</div>
{%- endif %}
{%- if m.unblocked %}
<div class="success">User unblocked!</div>
{%- endif %}
{%- if m.unblock_failed %}
<div class="warn">Could not unblock user!</div>
{%- endif %}
{%- if m.not_blocked %}
<div class="warn">User not blocked!</div>
{%- endif %}
<dl class="form">
{%- for b in blocked %}
<div style="text-align:left" class="userInfo">
<img class="avatar" width="40" height="40" src="/{{ b.avatar }}"/>
<div class="name">
<strong>
<a href="/user/{{ b.name_enc }}">{{ b.name }}</a>
</strong>
</div>
<div class="info">
Images: <strong>{{ b.images }}</strong>
</div>
<div style="clear:both;"></div>
</div>
{%- else %}
<div class="success">No users blocked!</div>
{%- endfor %}
<dt>User:</dt>
<dd>
<input type="text" name="blockuser" />
</dd>
<dt>&nbsp;</dt>
<dd>
<input type="submit" name="block" class="button" value="Block" />
</dd>
<dt>User:</dt>
<dd>
{%- if !blocked.is_empty() %}
<select name="unblockuser">
<option value="select">Select user</option>
{%- for b in blocked %}
<option value="{{ b.name }}">{{ b.name }}</option>
{%- endfor %}
</select>
{%- else %}No user blocked!{% endif %}
</dd>
<dt>&nbsp;</dt>
<dd>
<input type="submit" name="unblock" class="button" value="Unblock" />
</dd>
</dl>
</fieldset>
</form>
</div>
{% include "footer.html" %}