Skip to content

Repository files navigation

SlipNote

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.

Features

  • Anonymous upload — pick a section, optional title and your name, attach one or several files at once. PDF / Word / PowerPoint / image, up to 25 MB each. Files that would exceed the workspace cap are skipped while the rest save. Identical files already on the board (matched by content hash) are skipped as duplicates.
  • Uploader delete — a single-file upload gets a secret token; the success banner shows a one-time "Undo upload" link, good only for that page view. Holding the token is the only credential. Batch uploads (multiple files at once) don't get this link yet.
  • Owner mode — visit /<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.
  • Optional upload passphrase — each workspace can gate uploads behind its own passphrase (entered once per session). Empty = open (the default).
  • Optional Telegram notifications — set 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.
  • Reporting & operator moderation — every file has a Report button; reports surface on an operator dashboard at /operator (gated by OPERATOR_SECRET). The operator can Remove any file (blocklisting its content hash) or Dismiss false reports.
  • Anonymous download — files grouped by section with file-type icons, addressed by an unguessable per-file token (not a sequential id).
  • Download a whole section — one streamed zip per section (Notes / Slides / Past Papers), the exam-week "give me all the past papers" case.
  • In-browser preview — click a file's title to open PDFs and images inline (?view=1); other types download. Gated to safe types so uploads can't be served inline as an XSS vector.
  • QR code — every board link has a scannable QR with a downloadable PNG for slides and handouts.
  • Live search + sort — multi-word, separator-tolerant search over title and filename (so "past paper" finds past_papers_2023.pdf); sort Newest / Oldest / A–Z.
  • Storage meter — owners see how much of the workspace cap is used before uploads start bouncing.
  • Dark mode, mobile-optimised, and accessible (WCAG AA contrast, aria on form errors).

Requirements

  • PHP 8.3+, Composer
  • Node 18+ / npm
  • SQLite (default; no DB server needed)

Setup

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

Workspaces & courses

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.

Owner recovery (opt-in)

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 rotates the owner secret, retires the old link, and shows the new one once. Until it is opened nothing changes — so knowing someone's recovery address is not enough to revoke their access. 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.

Security

  • Owner secret stored only as a bcrypt hash; checks are timing-safe via 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 used.
  • Two tokens per file. The public download address (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.
  • Recovery email encrypted at rest; recovery responses don't reveal whether an email matched (no enumeration).
  • HTTP security headers via 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.
  • Workspace isolation enforced at the query layer by the 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 kill-switch gated by OPERATOR_SECRET: timing-safe, rate-limited, session-held (never in a URL); /operator 404s when unset.
  • Abuse throttling — per-IP limits on uploads, workspace creation, abuse reports, section ZIPs and operator login; a fail-closed host-disk check; and a content-hash blocklist refusing re-upload of operator-removed files. Per-IP means the IP the app believes, so set 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.
  • Upload hardening — files are validated by real content type (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).

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).

License

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.

About

One link for your whole class's notes, slides & past papers. No accounts, no login — self-hosted Laravel + Vue.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Contributors

Languages