✨ Añade pagetop-user, con autenticación y RBAC
Identidad de usuario, sesiones en base de datos, roles y permisos (RBAC) con "authenticated" implícito, contraseñas con Argon2id, tokens de un solo uso para verificación de email y restablecimiento de contraseña, y la UI de administración completa (usuarios, roles, permisos) sobre pagetop-admin. Incluye datos de demostración opcionales tras la feature `demo-data`.
This commit is contained in:
parent
043873a954
commit
def0513246
57 changed files with 7409 additions and 0 deletions
|
|
@ -0,0 +1,56 @@
|
|||
use pagetop_seaorm::migration::*;
|
||||
|
||||
pub struct Migration;
|
||||
|
||||
#[pagetop::async_trait]
|
||||
impl MigrationTrait for Migration {
|
||||
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.create_table(
|
||||
table_auto(Users::Table)
|
||||
.col(pk_auto(Users::Id))
|
||||
.col(string_len_uniq(Users::Username, 64))
|
||||
.col(string_len_uniq(Users::Email, 254))
|
||||
.col(timestamp_null(Users::EmailVerifiedAt))
|
||||
.col(string(Users::PasswordHash))
|
||||
// 0=Blocked, 1=Active, 2=Pending
|
||||
.col(small_integer(Users::Status).default(1))
|
||||
.col(string_len_null(Users::Language, 16))
|
||||
.col(string_len_null(Users::Timezone, 64))
|
||||
.col(string_len_null(Users::DisplayName, 128))
|
||||
.col(timestamp_null(Users::LastLoginAt))
|
||||
.col(timestamp_null(Users::LastAccessAt))
|
||||
.col(integer(Users::FailedLoginCount).default(0))
|
||||
.col(timestamp_null(Users::LockedUntil))
|
||||
// Acceso irrestricto al sistema, sin pasar por roles ni permisos.
|
||||
.col(boolean(Users::IsAdmin).default(false))
|
||||
.to_owned(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.drop_table(Table::drop().table(Users::Table).to_owned())
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(DeriveIden)]
|
||||
pub enum Users {
|
||||
Table,
|
||||
Id,
|
||||
Username,
|
||||
Email,
|
||||
EmailVerifiedAt,
|
||||
PasswordHash,
|
||||
Status,
|
||||
Language,
|
||||
Timezone,
|
||||
DisplayName,
|
||||
LastLoginAt,
|
||||
LastAccessAt,
|
||||
FailedLoginCount,
|
||||
LockedUntil,
|
||||
IsAdmin,
|
||||
}
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
use pagetop_seaorm::migration::*;
|
||||
|
||||
use sea_orm::{ConnectionTrait, DbBackend};
|
||||
|
||||
pub struct Migration;
|
||||
|
||||
#[pagetop::async_trait]
|
||||
impl MigrationTrait for Migration {
|
||||
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.create_table(
|
||||
table_auto(Roles::Table)
|
||||
.col(pk_auto(Roles::Id))
|
||||
.col(string_len_uniq(Roles::MachineName, 64))
|
||||
.col(string_len(Roles::Label, 128))
|
||||
.col(text_null(Roles::Description))
|
||||
.col(integer(Roles::Weight).default(0))
|
||||
// Los roles del sistema (anonymous, authenticated) no se borran.
|
||||
.col(boolean(Roles::Locked).default(false))
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Filas de sistema: anonymous (1), authenticated (2).
|
||||
let insert = Query::insert()
|
||||
.into_table(Roles::Table)
|
||||
.columns([
|
||||
Roles::Id,
|
||||
Roles::MachineName,
|
||||
Roles::Label,
|
||||
Roles::Weight,
|
||||
Roles::Locked,
|
||||
])
|
||||
.values_panic([
|
||||
1.into(),
|
||||
"anonymous".into(),
|
||||
"Anonymous".into(),
|
||||
0.into(),
|
||||
true.into(),
|
||||
])
|
||||
.values_panic([
|
||||
2.into(),
|
||||
"authenticated".into(),
|
||||
"Authenticated".into(),
|
||||
1.into(),
|
||||
true.into(),
|
||||
])
|
||||
.to_owned();
|
||||
|
||||
manager.exec_stmt(insert).await?;
|
||||
|
||||
// Los IDs anteriores se insertan explícitamente; en PostgreSQL la secuencia del `serial`
|
||||
// no avanza con inserciones explícitas, así que el próximo alta chocaría con estas filas.
|
||||
if manager.get_database_backend() == DbBackend::Postgres {
|
||||
manager
|
||||
.get_connection()
|
||||
.execute_unprepared(
|
||||
"SELECT setval(pg_get_serial_sequence('roles', 'id'), \
|
||||
(SELECT MAX(id) FROM roles))",
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.drop_table(Table::drop().table(Roles::Table).to_owned())
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(DeriveIden)]
|
||||
pub enum Roles {
|
||||
Table,
|
||||
Id,
|
||||
MachineName,
|
||||
Label,
|
||||
Description,
|
||||
Weight,
|
||||
Locked,
|
||||
}
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
use pagetop_seaorm::migration::*;
|
||||
|
||||
pub struct Migration;
|
||||
|
||||
#[pagetop::async_trait]
|
||||
impl MigrationTrait for Migration {
|
||||
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.create_table(
|
||||
Table::create()
|
||||
.table(UserRoles::Table)
|
||||
.if_not_exists()
|
||||
.col(integer(UserRoles::UserId))
|
||||
.col(integer(UserRoles::RoleId))
|
||||
.primary_key(
|
||||
Index::create()
|
||||
.col(UserRoles::UserId)
|
||||
.col(UserRoles::RoleId),
|
||||
)
|
||||
.foreign_key(
|
||||
ForeignKey::create()
|
||||
.from(UserRoles::Table, UserRoles::UserId)
|
||||
.to(
|
||||
super::m20260629_000001_create_users::Users::Table,
|
||||
super::m20260629_000001_create_users::Users::Id,
|
||||
)
|
||||
.on_delete(ForeignKeyAction::Cascade),
|
||||
)
|
||||
.foreign_key(
|
||||
ForeignKey::create()
|
||||
.from(UserRoles::Table, UserRoles::RoleId)
|
||||
.to(
|
||||
super::m20260629_000002_create_roles::Roles::Table,
|
||||
super::m20260629_000002_create_roles::Roles::Id,
|
||||
)
|
||||
.on_delete(ForeignKeyAction::Cascade),
|
||||
)
|
||||
.to_owned(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.drop_table(Table::drop().table(UserRoles::Table).to_owned())
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(DeriveIden)]
|
||||
pub enum UserRoles {
|
||||
Table,
|
||||
UserId,
|
||||
RoleId,
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
use pagetop_seaorm::migration::*;
|
||||
|
||||
pub struct Migration;
|
||||
|
||||
#[pagetop::async_trait]
|
||||
impl MigrationTrait for Migration {
|
||||
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.create_table(
|
||||
Table::create()
|
||||
.table(RolePermissions::Table)
|
||||
.if_not_exists()
|
||||
.col(integer(RolePermissions::RoleId))
|
||||
// Clave del permiso como string con namespace: "provider.action"
|
||||
.col(string_len(RolePermissions::PermissionKey, 190))
|
||||
.col(timestamp(RolePermissions::GrantedAt).default(Expr::current_timestamp()))
|
||||
.primary_key(
|
||||
Index::create()
|
||||
.col(RolePermissions::RoleId)
|
||||
.col(RolePermissions::PermissionKey),
|
||||
)
|
||||
.foreign_key(
|
||||
ForeignKey::create()
|
||||
.from(RolePermissions::Table, RolePermissions::RoleId)
|
||||
.to(
|
||||
super::m20260629_000002_create_roles::Roles::Table,
|
||||
super::m20260629_000002_create_roles::Roles::Id,
|
||||
)
|
||||
.on_delete(ForeignKeyAction::Cascade),
|
||||
)
|
||||
.to_owned(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.drop_table(Table::drop().table(RolePermissions::Table).to_owned())
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(DeriveIden)]
|
||||
pub enum RolePermissions {
|
||||
Table,
|
||||
RoleId,
|
||||
PermissionKey,
|
||||
GrantedAt,
|
||||
}
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
use pagetop_seaorm::migration::*;
|
||||
|
||||
pub struct Migration;
|
||||
|
||||
#[pagetop::async_trait]
|
||||
impl MigrationTrait for Migration {
|
||||
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.create_table(
|
||||
Table::create()
|
||||
.table(Sessions::Table)
|
||||
.if_not_exists()
|
||||
// sid: 64 chars hex (32 bytes aleatorios)
|
||||
.col(char_len(Sessions::Sid, 64).primary_key())
|
||||
.col(integer(Sessions::UserId))
|
||||
// Datos de sesión en JSON (flash messages, etc.)
|
||||
.col(text(Sessions::Data).default("{}"))
|
||||
.col(timestamp_null(Sessions::LastActivityAt))
|
||||
.col(timestamp(Sessions::ExpiresAt).default(Expr::current_timestamp()))
|
||||
.col(timestamp(Sessions::CreatedAt).default(Expr::current_timestamp()))
|
||||
.foreign_key(
|
||||
ForeignKey::create()
|
||||
.from(Sessions::Table, Sessions::UserId)
|
||||
.to(
|
||||
super::m20260629_000001_create_users::Users::Table,
|
||||
super::m20260629_000001_create_users::Users::Id,
|
||||
)
|
||||
.on_delete(ForeignKeyAction::Cascade),
|
||||
)
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
manager
|
||||
.create_index(
|
||||
Index::create()
|
||||
.name("idx_sessions_user_id")
|
||||
.table(Sessions::Table)
|
||||
.col(Sessions::UserId)
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
manager
|
||||
.create_index(
|
||||
Index::create()
|
||||
.name("idx_sessions_expires_at")
|
||||
.table(Sessions::Table)
|
||||
.col(Sessions::ExpiresAt)
|
||||
.to_owned(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.drop_table(Table::drop().table(Sessions::Table).to_owned())
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(DeriveIden)]
|
||||
pub enum Sessions {
|
||||
Table,
|
||||
Sid,
|
||||
UserId,
|
||||
Data,
|
||||
LastActivityAt,
|
||||
ExpiresAt,
|
||||
CreatedAt,
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
use pagetop_seaorm::migration::*;
|
||||
|
||||
pub struct Migration;
|
||||
|
||||
#[pagetop::async_trait]
|
||||
impl MigrationTrait for Migration {
|
||||
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.create_table(
|
||||
table_auto(UserTokens::Table)
|
||||
.col(pk_auto(UserTokens::Id))
|
||||
.col(integer(UserTokens::UserId))
|
||||
// "email_verification" | "password_reset"
|
||||
.col(string_len(UserTokens::Kind, 32))
|
||||
// SHA-256 del token plano (nunca se almacena el token en claro)
|
||||
.col(char_len_uniq(UserTokens::TokenHash, 64))
|
||||
.col(timestamp(UserTokens::ExpiresAt).default(Expr::current_timestamp()))
|
||||
.col(timestamp_null(UserTokens::ConsumedAt))
|
||||
.foreign_key(
|
||||
ForeignKey::create()
|
||||
.from(UserTokens::Table, UserTokens::UserId)
|
||||
.to(
|
||||
super::m20260629_000001_create_users::Users::Table,
|
||||
super::m20260629_000001_create_users::Users::Id,
|
||||
)
|
||||
.on_delete(ForeignKeyAction::Cascade),
|
||||
)
|
||||
.to_owned(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.drop_table(Table::drop().table(UserTokens::Table).to_owned())
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(DeriveIden)]
|
||||
pub enum UserTokens {
|
||||
Table,
|
||||
Id,
|
||||
UserId,
|
||||
Kind,
|
||||
TokenHash,
|
||||
ExpiresAt,
|
||||
ConsumedAt,
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue