TutorLoom
The learning platform for companies, colleges and training institutes. Free and open source, with an Enterprise edition.
翻译准备中
尚未收集仓库信息。
尚未检测到更新。GitHub 发布版本后会显示在这里。
One link for your whole class's notes, slides & past papers. No accounts, no login — self-hosted Laravel + Vue.
翻译准备中
取自仓库 README
正在加载完整 README。
查看完整 README ↗A dead-simple materials board for students. No accounts, no login — anyone with the link can browse, download, and contribute course files across three sections: Notes, Slides, Past Papers.
Workspaces (multi-tenant). The site root (/) lets anyone create or
open a workspace — an isolated board with its own courses, owner secret,
and optional upload passphrase. Workspaces never see each other. Creating one
yields a one-time owner link (the only credential), the same capability-URL
idea as the per-file delete token.
Built with Laravel 13 + Inertia + Vue 3, Tailwind v4, SQLite.
/<workspace>?owner=SECRET to unlock a per-row
"Delete" on every file for the session; "Lock board" leaves owner mode on
the current device. The secret is per-workspace, stored only as a bcrypt
hash, and shown once at creation.TELEGRAM_BOT_TOKEN and
TELEGRAM_CHAT_ID to post a one-line notice on every upload. Outbound only,
sent after the response and failure-swallowed. Empty = disabled./operator (gated by OPERATOR_SECRET).
The operator can Remove any file (blocklisting its content hash) or
Dismiss false reports.?view=1); other types download. Gated to safe types so uploads can't be
served inline as an XSS vector.past_papers_2023.pdf); sort Newest / Oldest
/ A–Z.aria on
form errors).composer install
npm install
cp .env.example .env # if .env is missing
php artisan key:generate
touch database/database.sqlite
php artisan migrate
# No storage:link needed — uploads live on the private disk and stream
# through the app.
npm run build # or `npm run dev` for hot reload
php artisan serve
The site root (/) is the workspace landing: create a workspace (save the
one-time owner link) or open an existing one by name. Create and manage
courses in owner mode (/<workspace>?owner=SECRET): add, edit code/title
(the slug stays fixed so shared /c/<slug> links keep working), reorder, and
bulk-delete files. Course slugs are unique within a workspace.
Lose the owner link and the board is unrecoverable — unless the owner set a
recovery email (encrypted at rest, owner-mode only). Visit
/<workspace>/recover, enter that email, and a signed, single-use restore link
(valid for an hour) is emailed to the stored address. Opening it asks first;
pressing "Get my new owner link" rotates the owner secret, retires the old
link, and shows the new one once. Until then nothing changes, so a mail
scanner or link preview that opens the link can't take the board, and knowing
someone's recovery address is not enough to revoke their access. The link
is always built on APP_URL, never the request's Host header. Responses are identical whether the email matches or
not (no enumeration), rate-limited, and the feature is hidden when the mail
driver is log or array.
Hash::check. Both ways of presenting it — the unlock form and a
?owner= link — share one per-workspace rate limit, so a wrong guess
costs the same whichever door it comes through. Recovery rotates the
secret only when the mailed restore link is confirmed.download_token) and
the uploader's private delete capability (manage_token) are separate
columns. The page hands every visitor the former and never the latter.SecureHeaders middleware: nosniff,
SAMEORIGIN, Referrer-Policy, restrictive Permissions-Policy. Production
also adds HSTS and a locked-down CSP (default-src 'self', scripts by
per-request nonce, fonts self-hosted) — omitted in dev where Vite and plain
http:// would break under them.WorkspaceScope
global scope; workspace_id is never mass-assignable. Per-workspace session
keys (ws_owner_{id}, ws_upload_ok_{id}) so unlocking one never leaks into
another.OPERATOR_SECRET: timing-safe, rate-limited,
session-held (never in a URL); /operator 404s when unset.TRUSTED_PROXIES to the
proxy actually in front (Cloudflare's ranges, or 127.0.0.1 for a local
reverse proxy). Left empty, X-Forwarded-For is ignored and every client
behind a proxy shares the proxy's address; set to * — never do this — any
client could pick its own.mimetypes,
via magic bytes) as well as extension, so a renamed executable is rejected;
client filenames are sanitised before storage (no control chars / header
injection); and per-workspace byte and file-count caps bound abuse. Files are
not virus-scanned — treat downloads as untrusted (see the Terms page).Keep APP_KEY safe and never just replace it. Recovery emails are
encrypted with it: a new key without the old one makes every saved recovery
email unreadable (and signs everyone out). To rotate, put the old key in
APP_PREVIOUS_KEYS (comma-separated) alongside the new APP_KEY.
Back up two things together: the SQLite file (database/) and the uploads
(storage/app/private). Neither is useful without the other. A deploy that brings
migrations first copies the database to a backups folder beside it
(php artisan db:snapshot, newest 10 kept) and aborts the start if the copy
or a migration fails, so the previous container keeps serving. That copy sits
on the same disk: it saves you from a bad migration, not a lost server. Copy
both somewhere else as well.
The operator dashboard flags the commonest misconfigurations (mail off, Telegram off, an untrusted proxy, debug on in production) under "Fix in .env".
For production: set APP_DEBUG=false, SESSION_ENCRYPT=true,
SESSION_SECURE_COOKIE=true, TRUSTED_PROXIES, and serve over HTTPS. The
owner link is redeemed by a ?owner= GET that immediately redirects to the
clean URL, so the secret appears in exactly one request — but that request
does reach the web server's access log. If you keep those logs, strip or
hash the query string for lines containing owner=. Note that
SESSION_SECURE_COOKIE=true requires HTTPS — over plain http:// the
session cookie is dropped and every POST fails with a 419 (leave it unset in
local dev on http).
MIT — free to use, self-host, and modify.
The licence covers this code only. Any hosted instance is operated independently of it, and neither the licence nor this repository grants rights to a hosted instance's workspaces, boards, or uploaded files — those belong to whoever runs and uses that instance.