diff --git a/src/web/pages/login.rs b/src/web/pages/login.rs index 143183a..d2fb44a 100644 --- a/src/web/pages/login.rs +++ b/src/web/pages/login.rs @@ -71,7 +71,11 @@ pub async fn page(Query(q): Query, req: Request) -> Result { e.preventDefault(); @@ -89,7 +93,12 @@ pub async fn page(Query(q): Query, req: Request) -> Result Result { - let u = User::authenticate(req.headers())? - .ok_or(RedirectViaError(Redirect::to("/login?re=/logs")))?; + let u = match User::authenticate(req.headers())? { + Some(u) => u, + None => return Ok(Redirect::to(&format!("/login?r={}", req.uri().path())).into_response()), + }; let mut conn = database::conn()?; let tx = conn.transaction()?; let logs = LogEntry::get_all(&tx)?; @@ -63,7 +65,7 @@ pub async fn page(req: Request) -> Result { } } } @else { - p class="text-center p-2" {"You must have permission to view this page."} + p class="text-center p-2" {"You must have permission to view logs."} } ), ) diff --git a/src/web/pages/persons.rs b/src/web/pages/persons.rs index 332816e..8a9cd7c 100644 --- a/src/web/pages/persons.rs +++ b/src/web/pages/persons.rs @@ -20,70 +20,69 @@ use crate::{ }; pub async fn page(req: Request) -> Result { - let u = User::authenticate(req.headers())?; + let u = match User::authenticate(req.headers())? { + Some(u) => u, + None => return Ok(Redirect::to(&format!("/login?r={}", req.uri().path())).into_response()), + }; let mut conn = database::conn()?; let tx = conn.transaction()?; Ok(base( "Persons | Mnemosyne", html!( - (nav(u.as_ref(), req.uri().path())) + (nav(Some(&u), req.uri().path())) - @if let Some(_) = u { - div class="mx-auto max-w-4xl px-2 my-4" { - p class="flex items-center gap-2" { - span class="text-neutral-500" {(PreEscaped(icons::CONTACT))} - span class="text-2xl font-semibold font-lora" {"Persons"} + div class="mx-auto max-w-4xl px-2 my-4" { + p class="flex items-center gap-2" { + span class="text-neutral-500" {(PreEscaped(icons::CONTACT))} + span class="text-2xl font-semibold font-lora" {"Persons"} + } + p class="text-neutral-500 text-sm font-light" { + @if let Ok(c) = Person::total_count(&tx) { + (c) " persons in total." + } @else { + "Could not get total person count." } - p class="text-neutral-500 text-sm font-light" { - @if let Ok(c) = Person::total_count(&tx) { - (c) " persons in total." - } @else { - "Could not get total person count." + } + } + @if let Ok(persons) = Person::get_all(&tx) { + div class="max-w-4xl mx-auto px-2 mt-4 flex flex-wrap gap-2" { + @for person in &persons { + div class="rounded px-4 py-2 bg-neutral-200/10 border border-neutral-200/15 flex items-center" { + span class="text-neutral-400 mr-1 scale-125" {"~"} + span class="text-sm" {(person.primary_name)} + div class="w-px h-2/3 my-auto mx-2 bg-neutral-200/15" {} + div class="text-xs flex items-center" { + ( + if let Ok(i) = person.get_in_quote_count(&tx) { + i.to_string() + } else { + "?".to_string() + } + ) span class="*:size-3 ml-1 text-neutral-400" {(PreEscaped(icons::SCROLL_TEXT))} + // div class="ml-2" {} + // "4" span class="*:size-3 ml-1 text-neutral-400" {(PreEscaped(icons::FILE_IMAGE))} + } } } } - @if let Ok(persons) = Person::get_all(&tx) { - div class="max-w-4xl mx-auto px-2 mt-4 flex flex-wrap gap-2" { - @for person in &persons { - div class="rounded px-4 py-2 bg-neutral-200/10 border border-neutral-200/15 flex items-center" { - span class="text-neutral-400 mr-1 scale-125" {"~"} - span class="text-sm" {(person.primary_name)} - div class="w-px h-2/3 my-auto mx-2 bg-neutral-200/15" {} - div class="text-xs flex items-center" { - ( - if let Ok(i) = person.get_in_quote_count(&tx) { - i.to_string() - } else { - "?".to_string() - } - ) span class="*:size-3 ml-1 text-neutral-400" {(PreEscaped(icons::SCROLL_TEXT))} - // div class="ml-2" {} - // "4" span class="*:size-3 ml-1 text-neutral-400" {(PreEscaped(icons::FILE_IMAGE))} - } - } + @if persons.is_empty() { + p class="text-center p-2" {"No persons yet."} + } + div class="mx-auto max-w-4xl mt-4 px-2" { + h3 class="font-lora font-semibold text-xl" {"Add new person"} + form action="/persons/create" method="post" { + label for="primary_name" class="text-neutral-500 font-light mt-2" {"Primary Name"} + div class="flex gap-2" { + input type="text" autocomplete="off" id="primary_name" name="primary_name" placeholder="e.g. Frank" + class="px-2 py-1 border border-neutral-200/25 bg-neutral-950/50 rounded"; + button type="submit" + class="px-4 py-1 border border-neutral-200/25 bg-neutral-200/5 rounded cursor-pointer hover:border-neutral-200/40" {"Submit"} } } - @if persons.is_empty() { - p class="text-center p-2" {"No persons yet."} - } - div class="mx-auto max-w-4xl mt-4 px-2" { - h3 class="font-lora font-semibold text-xl" {"Add new person"} - form action="/persons/create" method="post" { - label for="primary_name" class="text-neutral-500 font-light mt-2" {"Primary Name"} - div class="flex gap-2" { - input type="text" autocomplete="off" id="primary_name" name="primary_name" placeholder="e.g. Frank" - class="px-2 py-1 border border-neutral-200/25 bg-neutral-950/50 rounded"; - button type="submit" - class="px-4 py-1 border border-neutral-200/25 bg-neutral-200/5 rounded cursor-pointer hover:border-neutral-200/40" {"Submit"} - } - } - } - } @else { - p class="text-red-400 text-center" {"Failed to load persons."} } } @else { - p class="text-center p-2" {"You must be logged in to view this page."} + p class="text-red-400 text-center" {"Failed to load persons."} } ), ) diff --git a/src/web/pages/quotes.rs b/src/web/pages/quotes.rs index 3c6858c..58edfaa 100644 --- a/src/web/pages/quotes.rs +++ b/src/web/pages/quotes.rs @@ -1,6 +1,6 @@ use axum::{ extract::{Query, Request}, - response::{IntoResponse, Response}, + response::{IntoResponse, Redirect, Response}, }; use maud::{PreEscaped, html}; use serde::Deserialize; @@ -9,10 +9,7 @@ use crate::{ database, error::CompositeError, quotes::Quote, - users::{ - User, - auth::{UserAuthRequired, UserAuthenticate}, - }, + users::{User, auth::UserAuthenticate}, web::{ components::{nav::nav, quote::quote}, icons, @@ -31,7 +28,10 @@ pub async fn page( Query(query): Query, req: Request, ) -> Result { - let u = User::authenticate(req.headers())?.required()?; + let u = match User::authenticate(req.headers())? { + Some(u) => u, + None => return Ok(Redirect::to(&format!("/login?r={}", req.uri().path())).into_response()), + }; let conn = database::conn()?; let page = query.page.unwrap_or(1).max(1); diff --git a/src/web/pages/quotes/add.rs b/src/web/pages/quotes/add.rs index aa6a780..034a172 100644 --- a/src/web/pages/quotes/add.rs +++ b/src/web/pages/quotes/add.rs @@ -27,14 +27,17 @@ const LINE_ADD_RM_SCRIPT: &str = include_str!("line-add-rm.js"); const PREFILL_TIME_SCRIPT: &str = include_str!("prefill-time.js"); pub async fn page(req: Request) -> Result { - let u = User::authenticate(req.headers())?; + let u = match User::authenticate(req.headers())? { + Some(u) => u, + None => return Ok(Redirect::to(&format!("/login?r={}", req.uri().path())).into_response()), + }; let conn = database::conn()?; let names = Name::get_all(&conn)?; Ok(base( "Add Quote | Mnemosyne", html!( - (nav(u.as_ref(), req.uri().path())) + (nav(Some(&u), req.uri().path())) div class="max-w-4xl mx-auto px-2" { div class="my-4 flex justify-between" { diff --git a/src/web/pages/tags.rs b/src/web/pages/tags.rs index cc99aa5..54fe196 100644 --- a/src/web/pages/tags.rs +++ b/src/web/pages/tags.rs @@ -20,69 +20,68 @@ use crate::{ }; pub async fn page(req: Request) -> Result { - let u = User::authenticate(req.headers())?; + let u = match User::authenticate(req.headers())? { + Some(u) => u, + None => return Ok(Redirect::to(&format!("/login?r={}", req.uri().path())).into_response()), + }; let conn = database::conn()?; Ok(base( "Tags | Mnemosyne", html!( - (nav(u.as_ref(), req.uri().path())) + (nav(Some(&u), req.uri().path())) - @if let Some(_) = u { - div class="mx-auto max-w-4xl px-2 my-4" { - p class="flex items-center gap-2" { - span class="text-neutral-500" {(PreEscaped(icons::TAG))} - span class="text-2xl font-semibold font-lora" {"Tags"} + div class="mx-auto max-w-4xl px-2 my-4" { + p class="flex items-center gap-2" { + span class="text-neutral-500" {(PreEscaped(icons::TAG))} + span class="text-2xl font-semibold font-lora" {"Tags"} + } + p class="text-neutral-500 text-sm font-light" { + @if let Ok(c) = Tag::total_count(&conn) { + (c) " tags in total." + } @else { + "Could not get total tag count." } - p class="text-neutral-500 text-sm font-light" { - @if let Ok(c) = Tag::total_count(&conn) { - (c) " tags in total." - } @else { - "Could not get total tag count." + } + } + @if let Ok(tags) = Tag::get_all(&conn) { + div class="max-w-4xl mx-auto px-2 mt-4 flex flex-wrap gap-2" { + @for tag in &tags { + div class="rounded-full px-3 py-1 bg-neutral-200/10 border border-neutral-200/15 flex" { + span class="text-neutral-400 text-sm" {"#"} + span class="text-sm" {(tag.name)} + div class="w-px h-2/3 my-auto mx-2 bg-neutral-200/15" {} + div class="text-xs flex items-center" { + ( + if let Ok(i) = tag.get_tagged_quotes_count(&conn) { + i.to_string() + } else { + "?".to_string() + } + ) span class="*:size-3 ml-1 text-neutral-400" {(PreEscaped(icons::SCROLL_TEXT))} + // div class="ml-2" {} + // "0" span class="*:size-3 ml-1 text-neutral-400" {(PreEscaped(icons::FILE_IMAGE))} + } } } } - @if let Ok(tags) = Tag::get_all(&conn) { - div class="max-w-4xl mx-auto px-2 mt-4 flex flex-wrap gap-2" { - @for tag in &tags { - div class="rounded-full px-3 py-1 bg-neutral-200/10 border border-neutral-200/15 flex" { - span class="text-neutral-400 text-sm" {"#"} - span class="text-sm" {(tag.name)} - div class="w-px h-2/3 my-auto mx-2 bg-neutral-200/15" {} - div class="text-xs flex items-center" { - ( - if let Ok(i) = tag.get_tagged_quotes_count(&conn) { - i.to_string() - } else { - "?".to_string() - } - ) span class="*:size-3 ml-1 text-neutral-400" {(PreEscaped(icons::SCROLL_TEXT))} - // div class="ml-2" {} - // "0" span class="*:size-3 ml-1 text-neutral-400" {(PreEscaped(icons::FILE_IMAGE))} - } - } + @if tags.is_empty() { + p class="text-center p-2" {"No tags yet. How about making one?"} + } + div class="mx-auto max-w-4xl mt-4 px-2" { + h3 class="font-lora font-semibold text-xl" {"Add new tag"} + form action="/tags/create" method="post" { + label for="tagname" class="text-neutral-500 font-light mt-2" {"Tag Name"} + div class="flex gap-2" { + input type="text" autocomplete="off" id="tagname" name="tagname" placeholder="e.g. fashion" + class="px-2 py-1 border border-neutral-200/25 bg-neutral-950/50 rounded"; + button type="submit" + class="px-4 py-1 border border-neutral-200/25 bg-neutral-200/5 rounded cursor-pointer hover:border-neutral-200/40" {"Submit"} } } - @if tags.is_empty() { - p class="text-center p-2" {"No tags yet. How about making one?"} - } - div class="mx-auto max-w-4xl mt-4 px-2" { - h3 class="font-lora font-semibold text-xl" {"Add new tag"} - form action="/tags/create" method="post" { - label for="tagname" class="text-neutral-500 font-light mt-2" {"Tag Name"} - div class="flex gap-2" { - input type="text" autocomplete="off" id="tagname" name="tagname" placeholder="e.g. fashion" - class="px-2 py-1 border border-neutral-200/25 bg-neutral-950/50 rounded"; - button type="submit" - class="px-4 py-1 border border-neutral-200/25 bg-neutral-200/5 rounded cursor-pointer hover:border-neutral-200/40" {"Submit"} - } - } - } - } @else { - p class="text-red-400 text-center" {"Failed to load tags."} } } @else { - p class="text-center p-2" {"You must be logged in to view this page."} + p class="text-red-400 text-center" {"Failed to load tags."} } ), ) diff --git a/src/web/pages/users.rs b/src/web/pages/users.rs index 3afa995..4436611 100644 --- a/src/web/pages/users.rs +++ b/src/web/pages/users.rs @@ -1,6 +1,6 @@ use axum::{ extract::Request, - response::{IntoResponse, Response}, + response::{IntoResponse, Redirect, Response}, }; use maud::{PreEscaped, html}; @@ -22,49 +22,45 @@ pub mod create; pub mod profile; pub async fn page(req: Request) -> Result { - let u = User::authenticate(req.headers())?; - let conn = database::conn()?; - let us = match u.is_some() { - true => User::get_all(&conn), - false => Ok(vec![]), + let u = match User::authenticate(req.headers())? { + Some(u) => u, + None => return Ok(Redirect::to(&format!("/login?r={}", req.uri().path())).into_response()), }; + let conn = database::conn()?; + let us = User::get_all(&conn); Ok(base( "Users | Mnemosyne", html!( - (nav(u.as_ref(), req.uri().path())) + (nav(Some(&u), req.uri().path())) - @if let Some(u) = u { - div class="mx-auto max-w-4xl px-2 my-4" { - p class="flex items-center gap-2" { - span class="text-neutral-500" {(PreEscaped(icons::USERS))} - span class="text-2xl font-semibold font-lora" {"Users"} - } - p class="text-neutral-500 text-sm font-light" { - @if let Ok(v) = &us { - (v.len()) " users registered with Mnemosyne." - } @else { - "Could not fetch user count." - } - @if let Ok(true) = u.has_permission(&conn, Permission::ManuallyCreateUsers) { - " " - a href="/users/create" class="text-blue-500 hover:text-blue-400 hover:underline" { - "Create a new user" - } - } - } + div class="mx-auto max-w-4xl px-2 my-4" { + p class="flex items-center gap-2" { + span class="text-neutral-500" {(PreEscaped(icons::USERS))} + span class="text-2xl font-semibold font-lora" {"Users"} } - div class="mx-auto max-w-4xl flex flex-wrap gap-4" { - @if let Ok(vec) = &us { - @for user in vec { - (user_miniprofile(user)) - } + p class="text-neutral-500 text-sm font-light" { + @if let Ok(v) = &us { + (v.len()) " users registered with Mnemosyne." } @else { - p class="text-center py-4 text-light text-red-500" {"Failed to load users."} + "Could not fetch user count." + } + @if let Ok(true) = u.has_permission(&conn, Permission::ManuallyCreateUsers) { + " " + a href="/users/create" class="text-blue-500 hover:text-blue-400 hover:underline" { + "Create a new user" + } } } - } @else { - p class="text-center p-2" {"You must be logged in to view this page."} + } + div class="mx-auto max-w-4xl flex flex-wrap gap-4" { + @if let Ok(vec) = &us { + @for user in vec { + (user_miniprofile(user)) + } + } @else { + p class="text-center py-4 text-light text-red-500" {"Failed to load users."} + } } ), ) diff --git a/src/web/pages/users/create.rs b/src/web/pages/users/create.rs index 68d530b..116f0c6 100644 --- a/src/web/pages/users/create.rs +++ b/src/web/pages/users/create.rs @@ -21,42 +21,41 @@ use crate::{ }; pub async fn page(req: Request) -> Result { - let u = User::authenticate(req.headers())?; + let u = match User::authenticate(req.headers())? { + Some(u) => u, + None => return Ok(Redirect::to(&format!("/login?r={}", req.uri().path())).into_response()), + }; let conn = database::conn()?; Ok(base( "Create User | Mnemosyne", html!( - (nav(u.as_ref(), req.uri().path())) + (nav(Some(&u), req.uri().path())) - @if let Some(u) = u { - div class="mx-auto max-w-4xl px-2 my-4" { - p class="flex items-center gap-2" { - span class="text-neutral-500" {(PreEscaped(icons::USER_PLUS))} - span class="text-2xl font-semibold font-lora" {"Create a new user"} - } + div class="mx-auto max-w-4xl px-2 my-4" { + p class="flex items-center gap-2" { + span class="text-neutral-500" {(PreEscaped(icons::USER_PLUS))} + span class="text-2xl font-semibold font-lora" {"Create a new user"} } - @if let Ok(true) = u.has_permission(&conn, Permission::ManuallyCreateUsers) { - div class="mx-auto max-w-4xl px-2 mt-4" { - form action="/users/create-form" method="post" class="flex flex-col" { - label for="handle" class="font-light text-neutral-500" {"Handle"} - div class="flex w-64 items-center border border-neutral-200/25 rounded bg-neutral-950/50" { - span class="pl-2 text-neutral-500 select-none" {"@"} - input id="handle" name="handle" type="text" autocomplete="off" - class="w-fit pl-0.5 pr-1 py-1 outline-none"; - } - label for="password" class="font-light text-neutral-500 mt-4" {"Password"} br; - input id="password" name="password" type="password" autocomplete="off" - class="px-2 w-64 py-1 border border-neutral-200/25 bg-neutral-950/50 rounded"; - input type="submit" value="Create" - class="px-4 mt-4 w-64 py-1 border border-neutral-200/25 bg-neutral-200/5 rounded cursor-pointer hover:border-neutral-200/40"; + } + @if let Ok(true) = u.has_permission(&conn, Permission::ManuallyCreateUsers) { + div class="mx-auto max-w-4xl px-2 mt-4" { + form action="/users/create-form" method="post" class="flex flex-col" { + label for="handle" class="font-light text-neutral-500" {"Handle"} + div class="flex w-64 items-center border border-neutral-200/25 rounded bg-neutral-950/50" { + span class="pl-2 text-neutral-500 select-none" {"@"} + input id="handle" name="handle" type="text" autocomplete="off" + class="w-fit pl-0.5 pr-1 py-1 outline-none"; } + label for="password" class="font-light text-neutral-500 mt-4" {"Password"} br; + input id="password" name="password" type="password" autocomplete="off" + class="px-2 w-64 py-1 border border-neutral-200/25 bg-neutral-950/50 rounded"; + input type="submit" value="Create" + class="px-4 mt-4 w-64 py-1 border border-neutral-200/25 bg-neutral-200/5 rounded cursor-pointer hover:border-neutral-200/40"; } - } @else { - p class="text-center p-2" {"You must have permission to view this page."} } } @else { - p class="text-center p-2" {"You must be logged in to view this page."} + p class="text-center p-2" {"You must have permission to view this page."} } ), ) diff --git a/src/web/pages/users/profile.rs b/src/web/pages/users/profile.rs index 4bbebe0..1ebc59d 100644 --- a/src/web/pages/users/profile.rs +++ b/src/web/pages/users/profile.rs @@ -22,7 +22,7 @@ use crate::{ pub async fn page(Path(id): Path, req: Request) -> Result { let u = match User::authenticate(req.headers())? { Some(u) => u, - None => return Ok(Redirect::to("/users").into_response()), + None => return Ok(Redirect::to(&format!("/login?r={}", req.uri().path())).into_response()), }; let mut conn = database::conn()?; let tx = conn.transaction()?; diff --git a/src/web/pages/usersettings.rs b/src/web/pages/usersettings.rs index ba5e584..4bcd140 100644 --- a/src/web/pages/usersettings.rs +++ b/src/web/pages/usersettings.rs @@ -20,53 +20,52 @@ use crate::{ }; pub async fn page(req: Request) -> Result { - let u = User::authenticate(req.headers())?; + let u = match User::authenticate(req.headers())? { + Some(u) => u, + None => return Ok(Redirect::to(&format!("/login?r={}", req.uri().path())).into_response()), + }; Ok(base( "User Settings | Mnemosyne", html!( - (nav(u.as_ref(), req.uri().path())) + (nav(Some(&u), req.uri().path())) - @if let Some(u) = u { - div class="max-w-4xl mx-auto px-2" { - div class="mx-auto max-w-4xl my-4" { - p class="flex items-center gap-2" { - span class="text-neutral-500" {(PreEscaped(icons::SERVER))} - span class="text-2xl font-semibold font-lora" {"Your User Settings"} - } - p class="text-neutral-500 text-sm font-light" { - // "Hi, " (u.handle) "!" " " "This is your user settings page." br; - "Looking for Mnemosyne settings?" " " - a class="text-blue-500 hover:text-blue-400 hover:underline" href="/mnemosyne-settings" {"Here."} - } + div class="max-w-4xl mx-auto px-2" { + div class="mx-auto max-w-4xl my-4" { + p class="flex items-center gap-2" { + span class="text-neutral-500" {(PreEscaped(icons::SERVER))} + span class="text-2xl font-semibold font-lora" {"Your User Settings"} } - - label for="handle" class="font-light text-neutral-500" {"Handle"} - form action="/user-settings/handle" method="post" class="flex gap-2" { - div class="flex items-center border border-neutral-200/25 rounded bg-neutral-950/50" { - span class="pl-2 text-neutral-500 select-none" {"@"} - input id="handle" name="handle" type="text" autocomplete="off" value={(u.handle)} - class="w-full bg-transparent pl-0.5 pr-1 py-1 outline-none"; - } - button type="submit" class="px-4 py-1 border border-neutral-200/25 bg-neutral-200/5 rounded cursor-pointer hover:border-neutral-200/40" { - "Save" - } - } - hr class="mt-6 mb-4 border-neutral-600"; - p class="flex items-center gap-1" { - span class="text-neutral-500 scale-[.8]" {(PreEscaped(icons::USER_KEY))} - span class="text-lg font-semibold font-lora" {"Change Password"} - } - label for="password" class="font-light text-neutral-500" {"New password"} - form action="/user-settings/passwd" method="post" class="flex gap-2" { - input id="password" name="password" type="password" autocomplete="off" class="px-2 py-1 border border-neutral-200/25 bg-neutral-950/50 rounded"; - button type="submit" class="px-4 py-1 border border-neutral-200/25 bg-neutral-200/5 rounded cursor-pointer hover:border-neutral-200/40" { - "Submit" - } + p class="text-neutral-500 text-sm font-light" { + // "Hi, " (u.handle) "!" " " "This is your user settings page." br; + "Looking for Mnemosyne settings?" " " + a class="text-blue-500 hover:text-blue-400 hover:underline" href="/mnemosyne-settings" {"Here."} + } + } + + label for="handle" class="font-light text-neutral-500" {"Handle"} + form action="/user-settings/handle" method="post" class="flex gap-2" { + div class="flex items-center border border-neutral-200/25 rounded bg-neutral-950/50" { + span class="pl-2 text-neutral-500 select-none" {"@"} + input id="handle" name="handle" type="text" autocomplete="off" value={(u.handle)} + class="w-full bg-transparent pl-0.5 pr-1 py-1 outline-none"; + } + button type="submit" class="px-4 py-1 border border-neutral-200/25 bg-neutral-200/5 rounded cursor-pointer hover:border-neutral-200/40" { + "Save" + } + } + hr class="mt-6 mb-4 border-neutral-600"; + p class="flex items-center gap-1" { + span class="text-neutral-500 scale-[.8]" {(PreEscaped(icons::USER_KEY))} + span class="text-lg font-semibold font-lora" {"Change Password"} + } + label for="password" class="font-light text-neutral-500" {"New password"} + form action="/user-settings/passwd" method="post" class="flex gap-2" { + input id="password" name="password" type="password" autocomplete="off" class="px-2 py-1 border border-neutral-200/25 bg-neutral-950/50 rounded"; + button type="submit" class="px-4 py-1 border border-neutral-200/25 bg-neutral-200/5 rounded cursor-pointer hover:border-neutral-200/40" { + "Submit" } } - } @else { - p class="text-center p-2" {"You must be logged in to view this page."} } ), )