LAB 3 — Collaborative Writing Tool (Part 1): Accounts, Login, Projects, Shared Text

LAB 3 — Collaborative Writing Tool (Part 1)

Accounts, Login, Projects, Membership, Shared Document (No realtime yet)

Summary (what you’re building)

You are building the first version of a collaborative writing tool:

  • Users can register and log in
  • After login, a user can:
    • Create a writing project (they become the owner)
    • Join a project using a join code
    • See a list of projects they belong to
    • Open a project and view the shared document
    • Edit the document if the project is open
  • The owner can:
    • Close editing (project becomes read-only)
    • Reopen editing

Important: In Lab 3, if two people edit at the same time, the “last save wins.” (Realtime comes in Lab 4.)


Learning goals

By the end of this lab, you can:

  • Run a full-stack app on a remote Linux server using assigned ports
  • Use MariaDB/MySQL from Node/Express
  • Build a React app that uses React Router (first time!)
  • Use JWT authentication (token-based login)
  • Implement permissions: membership checks + owner-only actions

Server + Ports (Read This Carefully)

You will run everything on the Linux server:

  • Server IP: 10.192.145.179
  • Your instructor assigns you a base port between 4100–4200

Use this convention (recommended):

  • API port = your base port
  • Client port = base port + 1000

Example: base port 4123 → API on 4123, client on 5123.

You will access:

  • Client: http://10.192.145.179:5123
  • API: http://10.192.145.179:4123

If you can’t reach those ports from your laptop (fallback)

Use SSH port forwarding:

ssh -L 4123:localhost:4123 -L 5123:localhost:5123 yourUser@10.192.145.179

Then browse:

  • Client: http://localhost:5123
  • API: http://localhost:4123

Deliverables (what you submit)

  1. GitHub repo containing:
  • server/ Express backend
  • client/ React frontend
  • server/sql/001_init.sql schema
  • .env.example files for both server and client
  • README with setup + demo steps
  1. README answers:
  • Where are passwords stored and why is that safe?
  • How does the frontend prove the user is logged in?
  • How do you prevent non-owners from closing a project?
  1. Demo checklist in README (exact steps you can show a TA):
  • register → login → create project → join project as another user → edit text → close → confirm editing blocked → reopen → confirm editing allowed

Part 0 — Quick Start Checklist (so you don’t get lost)

You will do these big steps in order:

  1. Connect to the Linux server (VS Code Remote SSH recommended)
  2. Copy .env.example to .env for server and client
  3. Initialize your database schema
  4. Start backend (Express) on your API port
  5. Start frontend (Vite) on your client port
  6. Implement auth + routing + projects + document editing

Part 1 — Connect to the server (Remote SSH recommended)

1. Install VS Code Remote SSH

In VS Code:

  • Extensions → install Remote – SSH (Microsoft)

2. Connect

Command Palette:

  • Remote-SSH: Connect to Host…
  • Enter: yourUser@10.192.145.179

3. Open your lab folder on the server

Create a folder:

mkdir -p ~/comp318
cd ~/comp318

Clone your classroom repo there:

git clone <YOUR_REPO_URL>
cd collab-writing-labs

Part 2 — Install dependencies

Open two terminals in VS Code.

Server dependencies

cd server
npm install

Client dependencies

cd ../client
npm install

Part 3 — Configure ports + env files (required)

You will have two .env files:

  • server/.env
  • client/.env

Server .env

cd server
cp .env.example .env
nano .env

Fill in values using your ports. Example uses base port 4123:

PORT=4123
CORS_ORIGIN=http://10.192.145.179:5123

DB_HOST=localhost
DB_USER=<your_db_user>
DB_PASSWORD=<your_db_password>
DB_NAME=<your_db_name>

JWT_SECRET=<choose-a-long-random-string>

Client .env

cd ../client
cp .env.example .env
nano .env

Example:

VITE_API_BASE=http://10.192.145.179:4123
VITE_PORT=5123

Part 4 — Initialize the database schema

From server/:

npm run db:init

Verify:

mysql -u <your_db_user> -p -h localhost <your_db_name>
SHOW TABLES;

You should see tables like:

  • users
  • projects
  • project_members
  • project_documents

Part 5 — Start the backend and frontend (remote ports)

Backend: must listen on all interfaces

In server/src/index.js, your listen should be:

app.listen(port, "0.0.0.0", () => console.log(`API listening on ${port}`));

Start backend:

cd server
npm run dev

Test it:

curl http://localhost:4123/api/health

Frontend: must listen on all interfaces

In client/package.json, your dev script should use --host 0.0.0.0 and the env port:

"scripts": {
  "dev": "vite --host 0.0.0.0 --port $VITE_PORT"
}

Start frontend:

cd client
VITE_PORT=5123 npm run dev

Browse:

  • Direct: http://10.192.145.179:5123
  • Or forwarded: http://localhost:5123

Part 6 — React for the first time (mini tutorial you must use)

React mental model

  • React UI is made of components (functions that return JSX).
  • Components hold changing data using state (useState).
  • Components run code after rendering using effects (useEffect).

Example: controlled input (this is how forms work)

import { useState } from "react";

export default function ExampleForm() {
  const [email, setEmail] = useState("");

  return (
    <div>
      <input
        value={email}
        onChange={(e) => setEmail(e.target.value)}
        placeholder="Email"
      />
      <p>You typed: {email}</p>
    </div>
  );
}

Part 7 — React Router (first exposure: you must implement routing)

You must use URLs:

  • /login
  • /dashboard
  • /projects/:projectId

7.1 Install router

cd client
npm install react-router-dom

7.2 Wrap app in BrowserRouter

client/src/main.jsx (copy this):

import React from "react";
import ReactDOM from "react-dom/client";
import { BrowserRouter } from "react-router-dom";
import App from "./App.jsx";

ReactDOM.createRoot(document.getElementById("root")).render(
  <React.StrictMode>
    <BrowserRouter>
      <App />
    </BrowserRouter>
  </React.StrictMode>
);

7.3 Define routes in App.jsx

client/src/App.jsx (copy this):

import React from "react";
import { Routes, Route, Navigate } from "react-router-dom";

import LoginPage from "./pages/LoginPage.jsx";
import DashboardPage from "./pages/DashboardPage.jsx";
import ProjectPage from "./pages/ProjectPage.jsx";

function isLoggedIn() {
  return !!localStorage.getItem("token");
}

function RequireAuth({ children }) {
  if (!isLoggedIn()) return <Navigate to="/login" replace />;
  return children;
}

export default function App() {
  return (
    <Routes>
      <Route path="/login" element={<LoginPage />} />

      <Route
        path="/dashboard"
        element={
          <RequireAuth>
            <DashboardPage />
          </RequireAuth>
        }
      />

      <Route
        path="/projects/:projectId"
        element={
          <RequireAuth>
            <ProjectPage />
          </RequireAuth>
        }
      />

      <Route path="*" element={<Navigate to="/dashboard" replace />} />
    </Routes>
  );
}

7.4 Navigate after login

In a component:

import { useNavigate } from "react-router-dom";

const navigate = useNavigate();
navigate("/dashboard");

7.5 Read a URL parameter

In ProjectPage.jsx:

import { useParams } from "react-router-dom";
const { projectId } = useParams();

Part 8 — Backend requirements (implementation tasks)

You will implement these endpoints. All routes except health/auth require JWT.

Auth

  • POST /api/auth/register → store user with bcrypt password hash
  • POST /api/auth/login → verify password, return JWT token
  • GET /api/me → returns user info if token valid

Password hashing (required)

import bcrypt from "bcrypt";
const hash = await bcrypt.hash(password, 12);
const ok = await bcrypt.compare(password, user.password_hash);

JWT (required)

import jwt from "jsonwebtoken";
const token = jwt.sign({ userId: user.id, email: user.email }, process.env.JWT_SECRET, { expiresIn: "7d" });

Projects

  • GET /api/projects → list projects for current user
  • POST /api/projects → create project (owner is current user)
  • POST /api/projects/join → join by join code

Document

  • GET /api/projects/:projectId/document → returns project + document + isOwner
  • PUT /api/projects/:projectId/document → updates doc if project open

Owner controls

  • POST /api/projects/:projectId/close
  • POST /api/projects/:projectId/reopen

Required security checks

Every project route must enforce:

  1. Membership check for non-owner actions
  2. Owner check for close/reopen

Part 9 — Frontend requirements (implementation tasks)

You must build these pages:

LoginPage

  • A toggle: “Register” vs “Login”
  • Register calls /api/auth/register
  • Login calls /api/auth/login
  • On login success:
    • localStorage.setItem("token", token)
    • navigate to /dashboard
  • Show error messages from server

DashboardPage

  • Show:
    • Create project form
    • Join project form
    • List of my projects (link to /projects/:id)
  • Project list includes status open/closed

ProjectPage

  • Loads project doc with /api/projects/:projectId/document
  • Shows:
    • Project title
    • Status open/closed
    • Shared textarea
    • Save button
  • If closed:
    • disable textarea and Save button
  • If owner:
    • show Close/Reopen button

Part 10 — Troubleshooting (common failures)

“My API calls fail in browser”

  • Your server CORS_ORIGIN must exactly match your client URL.
    Example:
    • client: http://10.192.145.179:5123
    • server .env: CORS_ORIGIN=http://10.192.145.179:5123

“I can’t access the Vite page”

  • Make sure Vite is started with --host 0.0.0.0
  • If campus blocks ports, use SSH port forwarding fallback

Scroll to Top