From 75bc71eb39f0146f22be69ce9b7ab5cb0b6ec8d3 Mon Sep 17 00:00:00 2001 From: Duarte Date: Mon, 1 Jun 2026 09:08:37 +0100 Subject: [PATCH] =?UTF-8?q?Vers=C3=A3o=200.5=20antes=20do=20PWA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../00-Generic/US15-utilizador-username.md | 27 ++++++++++++++ src/app.d.ts | 1 + src/hooks.server.ts | 1 + src/lib/server/db/index.ts | 36 +++++++++++++++++++ src/lib/server/db/schema.ts | 1 + src/routes/admin/utilizadores/+page.server.ts | 3 +- src/routes/admin/utilizadores/+page.svelte | 8 +++-- .../admin/utilizadores/[id]/+page.server.ts | 8 +++-- .../admin/utilizadores/[id]/+page.svelte | 16 ++++++++- .../admin/utilizadores/novo/+page.server.ts | 9 ++++- .../admin/utilizadores/novo/+page.svelte | 16 ++++++++- src/routes/login/+page.svelte | 4 +-- 12 files changed, 120 insertions(+), 10 deletions(-) create mode 100644 docs/user-stories/00-Generic/US15-utilizador-username.md diff --git a/docs/user-stories/00-Generic/US15-utilizador-username.md b/docs/user-stories/00-Generic/US15-utilizador-username.md new file mode 100644 index 0000000..39972a9 --- /dev/null +++ b/docs/user-stories/00-Generic/US15-utilizador-username.md @@ -0,0 +1,27 @@ +# US15 - Identificação de Utilizador por Username + +**Como** utilizador do RefoodOne +**Quero** que a minha conta de acesso seja caracterizada por um username (para login) e um nome completo (para visualização no sistema) +**Para** simplificar o login no dia-a-dia e manter o meu nome legível na lista de utilizadores. + +## Descrição do Fluxo +1. Ao aceder à página de login, o voluntário insere o seu **Nome de Utilizador** (e.g. `joao`) em vez do e-mail ou nome completo. +2. Na lista de utilizadores na área de administração, o Administrador consegue visualizar em colunas separadas o **Nome de Utilizador** e o **Nome Completo**. +3. Ao criar ou editar um utilizador, o Administrador pode definir ambos os campos (**Nome Completo** e **Nome de Utilizador**). + +## Critérios de Aceitação + +### 1. Interface Gráfica (UI) +- **Ecrã de Login**: Campo de texto rotulado como "Nome de Utilizador" (ex: `refoodpdn`) em vez de "Utilizador / E-mail". +- **Ecrã de Listagem**: Coluna para o Nome Completo (Nome) e coluna para o Nome de Utilizador (Username). +- **Ecrã de Detalhe/Criação**: Dois campos de texto: + - **Nome Completo** (e.g. "João Silva") + - **Nome de Utilizador** (e.g. "joao") + +### 2. Comportamento e Regras de Negócio +- O `username` deve ser único na base de dados. +- Ao atualizar a base de dados, as contas já existentes devem ter o seu `username` atualizado de forma automática para o seu primeiro nome (a primeira palavra do campo nome em minúsculas). +- O login é efetuado através do `username`. + +### 3. Integração de Dados +- Alteração da tabela `users` para conter ambos os campos `name` e `username`. diff --git a/src/app.d.ts b/src/app.d.ts index 80abb84..cc1f3ae 100644 --- a/src/app.d.ts +++ b/src/app.d.ts @@ -6,6 +6,7 @@ declare global { interface Locals { user: { id: string; + name: string; username: string; role: string; } | null; diff --git a/src/hooks.server.ts b/src/hooks.server.ts index ccdc718..918f8f6 100644 --- a/src/hooks.server.ts +++ b/src/hooks.server.ts @@ -26,6 +26,7 @@ export const handle: Handle = async ({ event, resolve }) => { if (session.expiresAt > Date.now()) { event.locals.user = { id: user.id, + name: user.name, username: user.username, role: user.role }; diff --git a/src/lib/server/db/index.ts b/src/lib/server/db/index.ts index a0e999b..3f95b3d 100644 --- a/src/lib/server/db/index.ts +++ b/src/lib/server/db/index.ts @@ -8,6 +8,41 @@ if (!env.DATABASE_URL) throw new Error('DATABASE_URL is not set'); const client = new Database(env.DATABASE_URL); +// Run schema migration if 'name' column is missing +try { + const tableInfo = client.prepare("PRAGMA table_info(users)").all() as any[]; + const hasNameColumn = tableInfo.some((col) => col.name === 'name'); + if (!hasNameColumn) { + console.log("Migrating users table: adding 'name' column..."); + + // 1. Add nullable 'name' column + client.prepare("ALTER TABLE users ADD COLUMN name TEXT").run(); + + // 2. Set 'name' to the current 'username' (which represented the name before) + client.prepare("UPDATE users SET name = username").run(); + + // 3. Update 'username' to the lowercase first name + const allUsers = client.prepare("SELECT id, name FROM users").all() as any[]; + for (const u of allUsers) { + const fullName = u.name || ''; + const firstName = fullName.trim().split(/\s+/)[0].toLowerCase(); + + let uniqueUsername = firstName; + let counter = 1; + while (true) { + const exists = client.prepare("SELECT id FROM users WHERE username = ? AND id != ?").get(uniqueUsername, u.id); + if (!exists) break; + uniqueUsername = `${firstName}${counter}`; + counter++; + } + client.prepare("UPDATE users SET username = ? WHERE id = ?").run(uniqueUsername, u.id); + } + console.log("Database migration for users table completed."); + } +} catch (err) { + console.error("Migration failed:", err); +} + export const db = drizzle(client, { schema }); // Seed admin user if the users table is empty (US00) @@ -17,6 +52,7 @@ try { console.log('Seeding default admin user (refoodpdn)...'); const passwordHash = bcrypt.hashSync('rpdn!2512', 10); db.insert(schema.users).values({ + name: 'Administrador Refood', username: 'refoodpdn', passwordHash: passwordHash, role: 'admin' diff --git a/src/lib/server/db/schema.ts b/src/lib/server/db/schema.ts index ff81c55..f149157 100644 --- a/src/lib/server/db/schema.ts +++ b/src/lib/server/db/schema.ts @@ -2,6 +2,7 @@ import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core'; export const users = sqliteTable('users', { id: text('id').primaryKey().$defaultFn(() => crypto.randomUUID()), + name: text('name').notNull(), username: text('username').notNull().unique(), passwordHash: text('password_hash').notNull(), role: text('role').notNull(), // 'admin' | 'shift_manager' | 'volunteer' diff --git a/src/routes/admin/utilizadores/+page.server.ts b/src/routes/admin/utilizadores/+page.server.ts index df2cd50..54dd5f4 100644 --- a/src/routes/admin/utilizadores/+page.server.ts +++ b/src/routes/admin/utilizadores/+page.server.ts @@ -8,12 +8,13 @@ export const load: PageServerLoad = async () => { const list = db .select({ id: schema.users.id, + name: schema.users.name, username: schema.users.username, role: schema.users.role, createdAt: schema.users.createdAt }) .from(schema.users) - .orderBy(asc(schema.users.username)) + .orderBy(asc(schema.users.name)) .all(); return { diff --git a/src/routes/admin/utilizadores/+page.svelte b/src/routes/admin/utilizadores/+page.svelte index 126ca6c..32766b1 100644 --- a/src/routes/admin/utilizadores/+page.svelte +++ b/src/routes/admin/utilizadores/+page.svelte @@ -60,6 +60,7 @@ Nome + Username Role Ações @@ -67,7 +68,7 @@ {#if data.users.length === 0} - +

Nenhum utilizador encontrado.

@@ -75,7 +76,10 @@ {#each data.users as u} - {u.username} + {u.name} + + + @{u.username} diff --git a/src/routes/admin/utilizadores/[id]/+page.server.ts b/src/routes/admin/utilizadores/[id]/+page.server.ts index 7abed73..e4b7886 100644 --- a/src/routes/admin/utilizadores/[id]/+page.server.ts +++ b/src/routes/admin/utilizadores/[id]/+page.server.ts @@ -12,6 +12,7 @@ export const load: PageServerLoad = async ({ params }) => { const user = db .select({ id: schema.users.id, + name: schema.users.name, username: schema.users.username, role: schema.users.role, createdAt: schema.users.createdAt @@ -40,15 +41,16 @@ export const actions: Actions = { default: async ({ params, request }) => { const id = params.id; const data = await request.formData(); + const name = data.get('name')?.toString().trim(); const username = data.get('username')?.toString().trim(); const role = data.get('role')?.toString().trim(); const password = data.get('password')?.toString(); const confirmPassword = data.get('confirmPassword')?.toString(); - if (!username || !role) { + if (!name || !username || !role) { return fail(400, { success: false, - error: 'Nome de utilizador e Perfil são campos obrigatórios.' + error: 'Nome, Nome de utilizador e Perfil são campos obrigatórios.' }); } @@ -101,6 +103,7 @@ export const actions: Actions = { const passwordHash = bcrypt.hashSync(password!, 10); db.update(schema.users) .set({ + name, username, role, passwordHash @@ -110,6 +113,7 @@ export const actions: Actions = { } else { db.update(schema.users) .set({ + name, username, role }) diff --git a/src/routes/admin/utilizadores/[id]/+page.svelte b/src/routes/admin/utilizadores/[id]/+page.svelte index 73ceab0..dbcf9f7 100644 --- a/src/routes/admin/utilizadores/[id]/+page.svelte +++ b/src/routes/admin/utilizadores/[id]/+page.svelte @@ -38,6 +38,20 @@ }; }}>
+
+ + +
+
{ const data = await request.formData(); + const name = data.get('name')?.toString().trim(); const username = data.get('username')?.toString().trim(); const role = data.get('role')?.toString().trim(); const password = data.get('password')?.toString(); const confirmPassword = data.get('confirmPassword')?.toString(); - if (!username || !role || !password || !confirmPassword) { + if (!name || !username || !role || !password || !confirmPassword) { return fail(400, { success: false, + name, username, role, error: 'Todos os campos obrigatórios devem ser preenchidos.' @@ -25,6 +27,7 @@ export const actions: Actions = { if (role !== 'admin' && role !== 'shift_manager' && role !== 'volunteer') { return fail(400, { success: false, + name, username, role, error: 'Perfil selecionado é inválido.' @@ -34,6 +37,7 @@ export const actions: Actions = { if (password !== confirmPassword) { return fail(400, { success: false, + name, username, role, error: 'As palavras-passe introduzidas não coincidem.' @@ -43,6 +47,7 @@ export const actions: Actions = { if (password.length < 4) { return fail(400, { success: false, + name, username, role, error: 'A palavra-passe deve ter pelo menos 4 caracteres.' @@ -60,6 +65,7 @@ export const actions: Actions = { if (existing) { return fail(400, { success: false, + name, username, role, error: 'O nome de utilizador já está a ser utilizado.' @@ -70,6 +76,7 @@ export const actions: Actions = { const passwordHash = bcrypt.hashSync(password, 10); db.insert(schema.users) .values({ + name, username, role, passwordHash, diff --git a/src/routes/admin/utilizadores/novo/+page.svelte b/src/routes/admin/utilizadores/novo/+page.svelte index 19e2950..d0d1766 100644 --- a/src/routes/admin/utilizadores/novo/+page.svelte +++ b/src/routes/admin/utilizadores/novo/+page.svelte @@ -36,6 +36,20 @@ }; }}>
+
+ + +
+
- +