Flaxon 0.3.0 and Teloce-Py 0.3.0 are available on PyPI. Release instructions are maintained on this site; source links identify the framework snapshot. Check version and upgrade guidance.

Framework source · Markdown

09 | Login and project screens

Goal

Replace placeholders with working account forms and a project list.

What you are building

Login uses the shared API helper to load the anonymous session, submit register/login, and replace cached session state after rotation. Loading guards disable duplicate submissions and error messages remain visible. ProjectList requests only authorized records from the API and creates a project from a form. Internal project anchors use data-teloce-link. The server owns authorization; TypeScript types and disabled buttons do not enforce permissions.

Build this chapter

Stop the server. Work in project_manager. Follow each CREATE, whole-file replacement, or marked-block edit below in order.

Step 1: EDIT - replace the entire file - modules/auth/ui/pages/Login.html

Replace the placeholder with account forms. Use the session API and rotated CSRF token; do not read the HttpOnly cookie from JavaScript.

Select all existing contents of this file and replace them with the complete code below.

<template>
  <section class="narrow">
    <h1>Your account</h1>
    <p v-if="busy" role="status">Please wait…</p>
    <p v-if="message" role="alert">{{ message }}</p>
    <div v-if="user">
      <p>Signed in as {{ user.name }}.</p>
      <a href="/projects" data-teloce-link>Open projects</a>
      <button type="button" @click="logout" :disabled="busy">Sign out</button>
    </div>
    <form v-show="!user" @submit.prevent="submit">
      <label v-show="registering"
        >Name<input
          name="name"
          v-model="name"
          maxlength="80"
          :required="registering"
          autocomplete="name"
      /></label>
      <label
        >Email<input
          name="email"
          type="email"
          v-model="email"
          required
          autocomplete="username"
      /></label>
      <label
        >Password<input
          name="password"
          type="password"
          v-model="password"
          minlength="12"
          maxlength="128"
          required
          autocomplete="current-password"
      /></label>
      <button :disabled="busy">{{ registering ? "Create account" : "Sign in" }}</button>
      <button
        type="button"
        class="secondary"
        @click="registering = !registering"
        :disabled="busy"
      >
        {{ registering ? "Use an existing account" : "Register instead" }}
      </button>
    </form>
  </section>
</template>
<script lang="ts">
import { api, loadSession } from "../../../../ui/api.ts";
import type { Session } from "../../../../ui/types.ts";
export default {
  data() {
    return {
      user: null,
      registering: false,
      name: "",
      email: "",
      password: "",
      busy: false,
      message: "",
    };
  },
  async mounted() {
    try {
      this.user = (await loadSession()).user;
    } catch (error) {
      this.message = error.message;
    }
  },
  methods: {
    async submit() {
      if (this.busy) return;
      this.busy = true;
      this.message = "";
      try {
        const path = this.registering ? "/api/auth/register" : "/api/auth/login";
        const result = await api<Session>(path, "POST", {
          name: this.name,
          email: this.email,
          password: this.password,
        });
        this.user = result.user;
        this.password = "";
        window.__TELOCE_ROUTER__.navigate("/projects");
      } catch (error) {
        this.message = error.message;
      } finally {
        this.busy = false;
      }
    },
    async logout() {
      this.busy = true;
      try {
        await api("/api/auth/logout", "POST", {});
        this.user = null;
        this.message = "Signed out.";
      } catch (error) {
        this.message = error.message;
      } finally {
        this.busy = false;
      }
    },
  },
};
</script>
<style scoped>
* {
  box-sizing: border-box;
}
a {
  color: #1d4ed8;
}
h1 {
  font-size: clamp(1.8rem, 5vw, 3rem);
  line-height: 1.15;
}
h2 {
  font-size: 1.2rem;
}
p {
  line-height: 1.6;
}
[role="alert"] {
  color: #a21c2b;
  padding: 0.8rem;
  border-left: 4px solid;
}
:focus-visible {
  outline: 3px solid #f59e0b;
  outline-offset: 3px;
}
label {
  display: grid;
  gap: 0.4rem;
  margin: 0.9rem 0;
  font-weight: 600;
}
input,
select,
textarea,
button {
  font: inherit;
  border-radius: 7px;
  padding: 0.7rem;
  min-height: 44px;
}
input,
select,
textarea {
  width: 100%;
  border: 1px solid #abb9cf;
  background: white;
  color: #17243a;
}
button {
  display: inline-block;
  border: 0;
  background: #2563eb;
  color: white;
  padding: 0.7rem 1rem;
  cursor: pointer;
  margin: 0.3rem 0.5rem 0.3rem 0;
}
button:disabled {
  opacity: 0.55;
  cursor: wait;
}
.secondary {
  background: #e4eaf5;
  color: #17243a;
}
.danger {
  background: #b42332;
}
.narrow {
  max-width: 480px;
  margin: auto;
}
</style>

Say: "Replace the placeholder with account forms. Use the session API and rotated CSRF token; do not read the HttpOnly cookie from JavaScript."

Type: Follow the action above in modules/auth/ui/pages/Login.html; explain each new function or binding as you add it.

Viewers should see: The saved file matches this step. After all steps, run the chapter checks below and show the expected result.

Step 2: EDIT - replace the entire file - modules/projects/ui/pages/ProjectList.html

Replace the placeholder with the complete projects screen. Connect its form to the typed API helper; show loading, error and empty states. Keep all CSS inside style scoped.

Select all existing contents of this file and replace them with the complete code below.

<template>
  <section>
    <h1>Your projects</h1>
    <p>Choose a project to manage its tasks.</p>
    <p v-if="message" role="alert">
      {{ message }} <a href="/login" data-teloce-link>Account</a>
    </p>
    <p v-if="loading" role="status">Loading projects…</p>
    <form @submit.prevent="createProject" class="card">
      <h2>Create a project</h2>
      <label
        >Name<input name="project-name" v-model="name" required maxlength="120"
      /></label>
      <label
        >Description<textarea v-model="description" maxlength="1000"></textarea>
      </label>
      <button :disabled="busy">{{ busy ? "Saving…" : "Create project" }}</button>
    </form>
    <p v-if="!loading && projects.length === 0">
      No projects yet. Create your first one above.
    </p>
    <div class="cards">
      <a
        v-for="project in projects"
        :key="project.id"
        :href="'/projects/' + project.id"
        data-teloce-link
        class="card"
      >
        <h2>{{ project.name }}</h2>
        <p>{{ project.description }}</p>
      </a>
    </div>
  </section>
</template>
<script lang="ts">
import { api } from "../../../../ui/api.ts";
import type { Project } from "../../../../ui/types.ts";
export default {
  data() {
    return {
      projects: [],
      name: "",
      description: "",
      message: "",
      loading: true,
      busy: false,
    };
  },
  async mounted() {
    try {
      this.projects = await api<Project[]>("/api/projects/");
    } catch (error) {
      this.message = error.message;
    } finally {
      this.loading = false;
    }
  },
  methods: {
    async createProject() {
      if (this.busy) return;
      this.busy = true;
      this.message = "";
      try {
        const project = await api<Project>("/api/projects/", "POST", {
          name: this.name,
          description: this.description,
        });
        this.projects = [project, ...this.projects];
        this.name = "";
        this.description = "";
      } catch (error) {
        this.message = error.message;
      } finally {
        this.busy = false;
      }
    },
  },
};
</script>
<style scoped>
* {
  box-sizing: border-box;
}
a {
  color: #1d4ed8;
}
h1 {
  font-size: clamp(1.8rem, 5vw, 3rem);
  line-height: 1.15;
}
h2 {
  font-size: 1.2rem;
}
p {
  line-height: 1.6;
}
[role="alert"] {
  color: #a21c2b;
  padding: 0.8rem;
  border-left: 4px solid;
}
:focus-visible {
  outline: 3px solid #f59e0b;
  outline-offset: 3px;
}
.card {
  display: block;
  background: white;
  border: 1px solid #d8e0ed;
  border-radius: 12px;
  padding: 1.2rem;
  margin: 1rem 0;
  color: inherit;
  text-decoration: none;
}
label {
  display: grid;
  gap: 0.4rem;
  margin: 0.9rem 0;
  font-weight: 600;
}
input,
select,
textarea,
button {
  font: inherit;
  border-radius: 7px;
  padding: 0.7rem;
  min-height: 44px;
}
input,
select,
textarea {
  width: 100%;
  border: 1px solid #abb9cf;
  background: white;
  color: #17243a;
}
button {
  display: inline-block;
  border: 0;
  background: #2563eb;
  color: white;
  padding: 0.7rem 1rem;
  cursor: pointer;
  margin: 0.3rem 0.5rem 0.3rem 0;
}
button:disabled {
  opacity: 0.55;
  cursor: wait;
}
.secondary {
  background: #e4eaf5;
  color: #17243a;
}
.danger {
  background: #b42332;
}
.cards {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 240px), 1fr));
  gap: 1rem;
}
</style>

Say: "Replace the placeholder with the complete projects screen. Connect its form to the typed API helper; show loading, error and empty states. Keep all CSS inside style scoped."

Type: Follow the action above in modules/projects/ui/pages/ProjectList.html; explain each new function or binding as you add it.

Viewers should see: The saved file matches this step. After all steps, run the chapter checks below and show the expected result.

Run and check

python management.py runserver
# Open /login; register; create a project on /projects.
# Project details are completed in chapter 10.

Expected result

Register in /login, open /projects and create a project. Sign out and sign back in. The new project survives the restart.

Common errors

Always inspect response.ok. A resolved fetch promise can still represent a 400/403/500. Relative imports are resolved from each HTML component. Registration must refresh the cached CSRF token.

Short exercise

Show the validation message for an empty project name and make the same denied request directly to the API.

What to say and show

Say: "Replace placeholders with working account forms and a project list. The server remains responsible for persistence and authorization; the browser presents the result."

Show the expected result above, then point to the file responsible for it. Run the chapter check before the next lesson.

Save your checkpoint

git add .
git commit -m "Complete chapter 09: Login and project screens"