<?xml version="1.0"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
	<id>https://tech.uvoo.io/index.php?action=history&amp;feed=atom&amp;title=Vuetify_login</id>
	<title>Vuetify login - Revision history</title>
	<link rel="self" type="application/atom+xml" href="https://tech.uvoo.io/index.php?action=history&amp;feed=atom&amp;title=Vuetify_login"/>
	<link rel="alternate" type="text/html" href="https://tech.uvoo.io/index.php?title=Vuetify_login&amp;action=history"/>
	<updated>2026-04-04T16:04:29Z</updated>
	<subtitle>Revision history for this page on the wiki</subtitle>
	<generator>MediaWiki 1.35.2</generator>
	<entry>
		<id>https://tech.uvoo.io/index.php?title=Vuetify_login&amp;diff=5621&amp;oldid=prev</id>
		<title>Busk: Created page with &quot;Awesome — here’s a clean, working pattern to add username/password login → JWT → protected CRUD to the Vue + Vuetify + vue-postgrest app.  It includes:  SQL to create...&quot;</title>
		<link rel="alternate" type="text/html" href="https://tech.uvoo.io/index.php?title=Vuetify_login&amp;diff=5621&amp;oldid=prev"/>
		<updated>2025-08-10T10:47:13Z</updated>

		<summary type="html">&lt;p&gt;Created page with &amp;quot;Awesome — here’s a clean, working pattern to add username/password login → JWT → protected CRUD to the Vue + Vuetify + vue-postgrest app.  It includes:  SQL to create...&amp;quot;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;New page&lt;/b&gt;&lt;/p&gt;&lt;div&gt;Awesome — here’s a clean, working pattern to add username/password login → JWT → protected CRUD to the Vue + Vuetify + vue-postgrest app.&lt;br /&gt;
&lt;br /&gt;
It includes:&lt;br /&gt;
&lt;br /&gt;
SQL to create a users table, bcrypt hashing, and an api.login RPC that returns a JWT&lt;br /&gt;
&lt;br /&gt;
A Vue auth store (tiny, no Pinia needed) that persists the token&lt;br /&gt;
&lt;br /&gt;
A router guard that sends unauth’d users to /login&lt;br /&gt;
&lt;br /&gt;
A Login.vue page that POSTs to /rpc/login, saves the token, and redirects&lt;br /&gt;
&lt;br /&gt;
Minor changes to the TodosCrud.vue calls so every request carries Authorization: Bearer &amp;lt;jwt&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
1) Postgres / PostgREST: users + login RPC&lt;br /&gt;
&lt;br /&gt;
-- Enable needed extensions&lt;br /&gt;
CREATE EXTENSION IF NOT EXISTS pgcrypto;  -- for crypt() / gen_salt()&lt;br /&gt;
CREATE EXTENSION IF NOT EXISTS pgjwt;     -- for sign()&lt;br /&gt;
&lt;br /&gt;
-- Users table (bcrypt hashed password)&lt;br /&gt;
CREATE TABLE public.users (&lt;br /&gt;
  id serial PRIMARY KEY,&lt;br /&gt;
  username text UNIQUE NOT NULL,&lt;br /&gt;
  pass_hash text NOT NULL,     -- bcrypt&lt;br /&gt;
  role text NOT NULL DEFAULT 'anon', -- app role in the JWT (e.g., 'web', 'anon')&lt;br /&gt;
  created_at timestamptz NOT NULL DEFAULT now()&lt;br /&gt;
);&lt;br /&gt;
&lt;br /&gt;
-- Demo user: username 'demo', password 'demo123'&lt;br /&gt;
INSERT INTO public.users (username, pass_hash, role)&lt;br /&gt;
VALUES (&lt;br /&gt;
  'demo',&lt;br /&gt;
  crypt('demo123', gen_salt('bf')),&lt;br /&gt;
  'web'  -- whatever role your PostgREST uses for authenticated requests&lt;br /&gt;
);&lt;br /&gt;
&lt;br /&gt;
-- Example todos table (CRUD role grants)&lt;br /&gt;
CREATE TABLE public.todos(&lt;br /&gt;
  id serial PRIMARY KEY,&lt;br /&gt;
  title text NOT NULL,&lt;br /&gt;
  done boolean NOT NULL DEFAULT false,&lt;br /&gt;
  created_at timestamptz NOT NULL DEFAULT now()&lt;br /&gt;
);&lt;br /&gt;
&lt;br /&gt;
-- Grant access to web role (and maybe read to anon if you want)&lt;br /&gt;
GRANT SELECT, INSERT, UPDATE, DELETE ON public.todos TO web;&lt;br /&gt;
-- If your PostgREST anon role is `anon`, keep this locked down for anon:&lt;br /&gt;
REVOKE ALL ON public.todos FROM anon;&lt;br /&gt;
&lt;br /&gt;
-- JWT login RPC (returns { token: &amp;lt;jwt&amp;gt; })&lt;br /&gt;
-- NOTE: set your secret to exactly what PostgREST uses as PGRST_JWT_SECRET&lt;br /&gt;
CREATE OR REPLACE FUNCTION api.login(username text, password text)&lt;br /&gt;
RETURNS TABLE (token text) LANGUAGE plpgsql SECURITY DEFINER AS $$&lt;br /&gt;
DECLARE&lt;br /&gt;
  _user public.users%ROWTYPE;&lt;br /&gt;
  _claims json;&lt;br /&gt;
  _secret text := current_setting('app.jwt_secret', true); -- we'll set this GUC in postgrest&lt;br /&gt;
BEGIN&lt;br /&gt;
  SELECT * INTO _user FROM public.users WHERE users.username = login.username;&lt;br /&gt;
  IF NOT FOUND THEN&lt;br /&gt;
    RAISE EXCEPTION 'invalid_user_or_password' USING ERRCODE = '28P01';&lt;br /&gt;
  END IF;&lt;br /&gt;
&lt;br /&gt;
  IF crypt(password, _user.pass_hash) &amp;lt;&amp;gt; _user.pass_hash THEN&lt;br /&gt;
    RAISE EXCEPTION 'invalid_user_or_password' USING ERRCODE = '28P01';&lt;br /&gt;
  END IF;&lt;br /&gt;
&lt;br /&gt;
  -- JWT claims — include PostgREST-required 'role'&lt;br /&gt;
  _claims := json_build_object(&lt;br /&gt;
    'sub', _user.id,&lt;br /&gt;
    'role', _user.role,&lt;br /&gt;
    'username', _user.username,&lt;br /&gt;
    'iat', extract(epoch from now())::int,&lt;br /&gt;
    'exp', (extract(epoch from now())::int + 60*60*8) -- 8h&lt;br /&gt;
  );&lt;br /&gt;
&lt;br /&gt;
  IF coalesce(_secret,'') = '' THEN&lt;br /&gt;
    RAISE EXCEPTION 'jwt_secret_not_configured';&lt;br /&gt;
  END IF;&lt;br /&gt;
&lt;br /&gt;
  token := sign(_claims, _secret);&lt;br /&gt;
  RETURN NEXT;&lt;br /&gt;
END;&lt;br /&gt;
$$;&lt;br /&gt;
&lt;br /&gt;
-- PostgREST config notes (postgrest.conf):&lt;br /&gt;
--   db-anon-role = &amp;quot;anon&amp;quot;&lt;br /&gt;
--   db-pre-request = &amp;quot;app.settings&amp;quot;   (optional)&lt;br /&gt;
--   app.settings.jwt_secret = &amp;quot;&amp;lt;your-long-secret&amp;gt;&amp;quot;&lt;br /&gt;
--   (or set environment: PGRST_JWT_SECRET to the same value)&lt;br /&gt;
&lt;br /&gt;
-- Finally, expose RPC to anon so they can log in&lt;br /&gt;
GRANT EXECUTE ON FUNCTION api.login(text, text) TO anon;&lt;br /&gt;
&lt;br /&gt;
&amp;gt; Make sure PostgREST uses the same secret as the function (app.settings.jwt_secret or PGRST_JWT_SECRET). The function reads it via current_setting('app.jwt_secret', true).&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
2) Tiny auth helper (token store)&lt;br /&gt;
&lt;br /&gt;
// src/auth.ts&lt;br /&gt;
import { ref, computed } from 'vue'&lt;br /&gt;
&lt;br /&gt;
const LS_KEY = 'jwt'&lt;br /&gt;
const _token = ref&amp;lt;string | null&amp;gt;(localStorage.getItem(LS_KEY))&lt;br /&gt;
&lt;br /&gt;
export const isAuthed = computed(() =&amp;gt; !!_token.value)&lt;br /&gt;
export const token = computed({&lt;br /&gt;
  get: () =&amp;gt; _token.value,&lt;br /&gt;
  set: (t: string | null) =&amp;gt; {&lt;br /&gt;
    _token.value = t&lt;br /&gt;
    if (t) localStorage.setItem(LS_KEY, t)&lt;br /&gt;
    else localStorage.removeItem(LS_KEY)&lt;br /&gt;
  }&lt;br /&gt;
})&lt;br /&gt;
&lt;br /&gt;
export function authHeaders() {&lt;br /&gt;
  return token.value ? { Authorization: `Bearer ${token.value}` } : {}&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
3) Router with auth guard&lt;br /&gt;
&lt;br /&gt;
// src/router.ts&lt;br /&gt;
import { createRouter, createWebHistory } from 'vue-router'&lt;br /&gt;
import { isAuthed } from './auth'&lt;br /&gt;
import TodosCrud from './components/TodosCrud.vue'&lt;br /&gt;
import Login from './views/Login.vue'&lt;br /&gt;
&lt;br /&gt;
const routes = [&lt;br /&gt;
  { path: '/login', name: 'login', component: Login, meta: { public: true } },&lt;br /&gt;
  { path: '/', name: 'home', component: TodosCrud }, // protected&lt;br /&gt;
]&lt;br /&gt;
&lt;br /&gt;
export const router = createRouter({&lt;br /&gt;
  history: createWebHistory(),&lt;br /&gt;
  routes,&lt;br /&gt;
})&lt;br /&gt;
&lt;br /&gt;
router.beforeEach((to) =&amp;gt; {&lt;br /&gt;
  if (to.meta.public) return true&lt;br /&gt;
  if (!isAuthed.value) return { name: 'login', query: { redirect: to.fullPath } }&lt;br /&gt;
  return true&lt;br /&gt;
})&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
4) main.ts (Vuetify + vue-postgrest + Router)&lt;br /&gt;
&lt;br /&gt;
// src/main.ts&lt;br /&gt;
import { createApp } from 'vue'&lt;br /&gt;
import App from './App.vue'&lt;br /&gt;
&lt;br /&gt;
import 'vuetify/styles'&lt;br /&gt;
import { createVuetify } from 'vuetify'&lt;br /&gt;
import { aliases, mdi } from 'vuetify/iconsets/mdi'&lt;br /&gt;
import '@mdi/font/css/materialdesignicons.css'&lt;br /&gt;
const vuetify = createVuetify({ icons: { defaultSet: 'mdi', aliases, sets: { mdi } } })&lt;br /&gt;
&lt;br /&gt;
import VuePostgrest from 'vue-postgrest'&lt;br /&gt;
import { router } from './router'&lt;br /&gt;
&lt;br /&gt;
const app = createApp(App)&lt;br /&gt;
&lt;br /&gt;
app.use(VuePostgrest, {&lt;br /&gt;
  apiRoot: 'http://localhost:3000/', // your PostgREST base URL&lt;br /&gt;
  // We’ll pass per-request headers from components so we can update after login.&lt;br /&gt;
})&lt;br /&gt;
&lt;br /&gt;
app.use(vuetify)&lt;br /&gt;
app.use(router)&lt;br /&gt;
app.mount('#app')&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
5) Login page (POST /rpc/login, save token, redirect)&lt;br /&gt;
&lt;br /&gt;
&amp;lt;!-- src/views/Login.vue --&amp;gt;&lt;br /&gt;
&amp;lt;template&amp;gt;&lt;br /&gt;
  &amp;lt;v-container class=&amp;quot;d-flex justify-center align-center&amp;quot; style=&amp;quot;min-height: 70vh&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;v-card width=&amp;quot;420&amp;quot;&amp;gt;&lt;br /&gt;
      &amp;lt;v-card-title class=&amp;quot;text-h6&amp;quot;&amp;gt;Sign in&amp;lt;/v-card-title&amp;gt;&lt;br /&gt;
      &amp;lt;v-card-text&amp;gt;&lt;br /&gt;
        &amp;lt;v-form @submit.prevent=&amp;quot;submit&amp;quot;&amp;gt;&lt;br /&gt;
          &amp;lt;v-text-field&lt;br /&gt;
            v-model=&amp;quot;username&amp;quot;&lt;br /&gt;
            label=&amp;quot;Username&amp;quot;&lt;br /&gt;
            autocomplete=&amp;quot;username&amp;quot;&lt;br /&gt;
            required&lt;br /&gt;
          /&amp;gt;&lt;br /&gt;
          &amp;lt;v-text-field&lt;br /&gt;
            v-model=&amp;quot;password&amp;quot;&lt;br /&gt;
            label=&amp;quot;Password&amp;quot;&lt;br /&gt;
            type=&amp;quot;password&amp;quot;&lt;br /&gt;
            autocomplete=&amp;quot;current-password&amp;quot;&lt;br /&gt;
            required&lt;br /&gt;
          /&amp;gt;&lt;br /&gt;
          &amp;lt;v-alert v-if=&amp;quot;error&amp;quot; type=&amp;quot;error&amp;quot; variant=&amp;quot;tonal&amp;quot; class=&amp;quot;mt-2&amp;quot;&amp;gt;{{ error }}&amp;lt;/v-alert&amp;gt;&lt;br /&gt;
          &amp;lt;div class=&amp;quot;d-flex justify-end mt-4&amp;quot;&amp;gt;&lt;br /&gt;
            &amp;lt;v-btn type=&amp;quot;submit&amp;quot; :loading=&amp;quot;loading&amp;quot; color=&amp;quot;primary&amp;quot;&amp;gt;Login&amp;lt;/v-btn&amp;gt;&lt;br /&gt;
          &amp;lt;/div&amp;gt;&lt;br /&gt;
        &amp;lt;/v-form&amp;gt;&lt;br /&gt;
      &amp;lt;/v-card-text&amp;gt;&lt;br /&gt;
    &amp;lt;/v-card&amp;gt;&lt;br /&gt;
  &amp;lt;/v-container&amp;gt;&lt;br /&gt;
&amp;lt;/template&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;script setup lang=&amp;quot;ts&amp;quot;&amp;gt;&lt;br /&gt;
import { ref } from 'vue'&lt;br /&gt;
import { token } from '@/auth'&lt;br /&gt;
import { useRouter, useRoute } from 'vue-router'&lt;br /&gt;
&lt;br /&gt;
const router = useRouter()&lt;br /&gt;
const route = useRoute()&lt;br /&gt;
&lt;br /&gt;
const username = ref('')&lt;br /&gt;
const password = ref('')&lt;br /&gt;
const loading = ref(false)&lt;br /&gt;
const error = ref('')&lt;br /&gt;
&lt;br /&gt;
async function submit() {&lt;br /&gt;
  loading.value = true&lt;br /&gt;
  error.value = ''&lt;br /&gt;
  try {&lt;br /&gt;
    const res = await fetch('http://localhost:3000/rpc/login', {&lt;br /&gt;
      method: 'POST',&lt;br /&gt;
      headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },&lt;br /&gt;
      body: JSON.stringify({ username: username.value, password: password.value }),&lt;br /&gt;
    })&lt;br /&gt;
    if (!res.ok) {&lt;br /&gt;
      const t = await res.text()&lt;br /&gt;
      throw new Error(t || `Login failed (${res.status})`)&lt;br /&gt;
    }&lt;br /&gt;
    const json = await res.json() as { token: string }[] | { token: string }&lt;br /&gt;
    // PostgREST returns rows; handle both shapes safely&lt;br /&gt;
    const tok = Array.isArray(json) ? json[0]?.token : (json as any)?.token&lt;br /&gt;
    if (!tok) throw new Error('No token in response')&lt;br /&gt;
    token.value = tok&lt;br /&gt;
&lt;br /&gt;
    const to = (route.query.redirect as string) || '/'&lt;br /&gt;
    router.replace(to)&lt;br /&gt;
  } catch (e: any) {&lt;br /&gt;
    error.value = e?.message || 'Login failed'&lt;br /&gt;
  } finally {&lt;br /&gt;
    loading.value = false&lt;br /&gt;
  }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/script&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
6) Add a Logout button (optional)&lt;br /&gt;
&lt;br /&gt;
In App.vue (or a small TopBar), add:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;!-- src/App.vue --&amp;gt;&lt;br /&gt;
&amp;lt;template&amp;gt;&lt;br /&gt;
  &amp;lt;v-app&amp;gt;&lt;br /&gt;
    &amp;lt;v-app-bar flat&amp;gt;&lt;br /&gt;
      &amp;lt;v-toolbar-title&amp;gt;Todos&amp;lt;/v-toolbar-title&amp;gt;&lt;br /&gt;
      &amp;lt;v-spacer /&amp;gt;&lt;br /&gt;
      &amp;lt;v-btn v-if=&amp;quot;isAuthed&amp;quot; variant=&amp;quot;text&amp;quot; @click=&amp;quot;logout&amp;quot;&amp;gt;Logout&amp;lt;/v-btn&amp;gt;&lt;br /&gt;
    &amp;lt;/v-app-bar&amp;gt;&lt;br /&gt;
    &amp;lt;v-main&amp;gt;&lt;br /&gt;
      &amp;lt;router-view /&amp;gt;&lt;br /&gt;
    &amp;lt;/v-main&amp;gt;&lt;br /&gt;
  &amp;lt;/v-app&amp;gt;&lt;br /&gt;
&amp;lt;/template&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;script setup lang=&amp;quot;ts&amp;quot;&amp;gt;&lt;br /&gt;
import { isAuthed, token } from './auth'&lt;br /&gt;
import { useRouter } from 'vue-router'&lt;br /&gt;
const router = useRouter()&lt;br /&gt;
function logout() {&lt;br /&gt;
  token.value = null&lt;br /&gt;
  router.replace('/login')&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/script&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
7) Update TodosCrud.vue to send Authorization on each request&lt;br /&gt;
&lt;br /&gt;
We’ll reuse the same component you already had, but add headers: authHeaders() wherever we call PostgREST:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;script lang=&amp;quot;ts&amp;quot;&amp;gt;&lt;br /&gt;
import { defineComponent } from 'vue'&lt;br /&gt;
import { pg } from 'vue-postgrest'&lt;br /&gt;
+import { authHeaders } from '@/auth'&lt;br /&gt;
&lt;br /&gt;
export default defineComponent({&lt;br /&gt;
  name: 'TodosCrud',&lt;br /&gt;
  mixins: [pg],&lt;br /&gt;
  data() {&lt;br /&gt;
    return {&lt;br /&gt;
      pgConfig: {&lt;br /&gt;
        route: 'todos',&lt;br /&gt;
        query: { select: ['id','title','done','created_at'], order: { id: 'asc' } },&lt;br /&gt;
        count: 'exact',&lt;br /&gt;
      },&lt;br /&gt;
      // ...rest unchanged&lt;br /&gt;
    }&lt;br /&gt;
  },&lt;br /&gt;
  methods: {&lt;br /&gt;
    refresh() {&lt;br /&gt;
-     (this as any).pg.$get()&lt;br /&gt;
+     (this as any).pg.$get({ headers: authHeaders() })&lt;br /&gt;
    },&lt;br /&gt;
    openCreate() {&lt;br /&gt;
-     this.dialog.model = (this as any).pg.$new({ title: '', done: false })&lt;br /&gt;
+     this.dialog.model = (this as any).pg.$new({ title: '', done: false })&lt;br /&gt;
      this.dialog.mode = 'create'&lt;br /&gt;
      this.errorMessage = ''&lt;br /&gt;
      this.dialog.open = true&lt;br /&gt;
    },&lt;br /&gt;
    openEdit(item: any) {&lt;br /&gt;
      this.dialog.model = (this as any).pg.$new({&lt;br /&gt;
        id: item.id, title: item.title, done: item.done&lt;br /&gt;
      })&lt;br /&gt;
      this.dialog.mode = 'edit'&lt;br /&gt;
      this.errorMessage = ''&lt;br /&gt;
      this.dialog.open = true&lt;br /&gt;
    },&lt;br /&gt;
    async submitDialog() {&lt;br /&gt;
      this.saving = true&lt;br /&gt;
      this.errorMessage = ''&lt;br /&gt;
      try {&lt;br /&gt;
        if (this.dialog.mode === 'create') {&lt;br /&gt;
-         await this.dialog.model.$post({ return: 'minimal' })&lt;br /&gt;
+         await this.dialog.model.$post({ return: 'minimal', headers: authHeaders() })&lt;br /&gt;
        } else {&lt;br /&gt;
-         await this.dialog.model.$patch()&lt;br /&gt;
+         await this.dialog.model.$patch({ headers: authHeaders() })&lt;br /&gt;
        }&lt;br /&gt;
        this.closeDialog()&lt;br /&gt;
        this.refresh()&lt;br /&gt;
      } catch (e: any) {&lt;br /&gt;
        this.errorMessage = e?.message || 'Save failed'&lt;br /&gt;
      } finally {&lt;br /&gt;
        this.saving = false&lt;br /&gt;
      }&lt;br /&gt;
    },&lt;br /&gt;
    async saveInline(item: any) {&lt;br /&gt;
      try {&lt;br /&gt;
-       await item.$patch({ columns: ['done'] })&lt;br /&gt;
+       await item.$patch({ columns: ['done'], headers: authHeaders() })&lt;br /&gt;
      } catch (e) {&lt;br /&gt;
        item.done = !item.done&lt;br /&gt;
      }&lt;br /&gt;
    },&lt;br /&gt;
    async doDelete() {&lt;br /&gt;
      if (!this.confirm.item) return&lt;br /&gt;
      this.saving = true&lt;br /&gt;
      try {&lt;br /&gt;
-       await this.confirm.item.$delete()&lt;br /&gt;
+       await this.confirm.item.$delete({ headers: authHeaders() })&lt;br /&gt;
        this.confirm.open = false&lt;br /&gt;
        this.refresh()&lt;br /&gt;
      } finally {&lt;br /&gt;
        this.saving = false&lt;br /&gt;
      }&lt;br /&gt;
    },&lt;br /&gt;
  },&lt;br /&gt;
  mounted() {&lt;br /&gt;
    // initial load with headers&lt;br /&gt;
    this.refresh()&lt;br /&gt;
  }&lt;br /&gt;
})&lt;br /&gt;
&amp;lt;/script&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;gt; This keeps it explicit and guarantees every request carries the latest token. If vue-postgrest in your version supports a global headers setter, you can centralize this, but the above is bulletproof.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
---&lt;br /&gt;
&lt;br /&gt;
That’s it&lt;br /&gt;
&lt;br /&gt;
Go to /login, sign in as demo / demo123&lt;br /&gt;
&lt;br /&gt;
You’ll be redirected to /, where all todos requests now include your JWT&lt;br /&gt;
&lt;br /&gt;
Use Add / Edit / Delete / Toggle Done; all calls are authorized&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
If you want, I can also wire up server-side pagination in the Vuetify v-data-table that drives limit/offset in pg.$get({ range: { limit, offset }}) and uses pg.$range.totalCount for the total row count.&lt;/div&gt;</summary>
		<author><name>Busk</name></author>
	</entry>
</feed>