diff --git a/deepwell/src/api.rs b/deepwell/src/api.rs index 556f7580b1..5f5fd75434 100644 --- a/deepwell/src/api.rs +++ b/deepwell/src/api.rs @@ -189,6 +189,7 @@ async fn build_module(app_state: ServerState) -> anyhow::Result anyhow::Result, @@ -73,6 +74,27 @@ pub async fn page_get_direct( } } +pub async fn page_get_deleted( + ctx: &ServiceContext<'_>, + params: Params<'static>, +) -> Result> { + let GetPageSlug { site_id, slug } = params.parse()?; + + info!("Getting deleted page {slug} in site ID {site_id}"); + let get_deleted_page = PageService::get_deleted_by_slug(ctx, site_id, &slug) + .await? + .into_iter() + .map(|page| build_page_deleted_output(ctx, page)); + + let result = try_join_all(get_deleted_page) + .await? + .into_iter() + .flatten() + .collect(); + + Ok(result) +} + pub async fn page_get_score( ctx: &ServiceContext<'_>, params: Params<'static>, @@ -235,3 +257,32 @@ async fn build_page_output( layout, })) } + +async fn build_page_deleted_output( + ctx: &ServiceContext<'_>, + page: PageModel, +) -> Result> { + // Get page revision + let revision = + PageRevisionService::get_latest(ctx, page.site_id, page.page_id).await?; + + // Calculate score and determine layout + let rating = ScoreService::score(ctx, page.page_id).await?; + + // Build result struct + Ok(Some(GetDeletedPageOutput { + page_id: page.page_id, + page_created_at: page.created_at, + page_updated_at: page.updated_at, + page_deleted_at: page.deleted_at.expect("Page should be deleted"), + page_revision_count: revision.revision_number, + site_id: page.site_id, + discussion_thread_id: page.discussion_thread_id, + hidden_fields: revision.hidden, + title: revision.title, + alt_title: revision.alt_title, + slug: revision.slug, + tags: revision.tags, + rating, + })) +} diff --git a/deepwell/src/endpoints/view.rs b/deepwell/src/endpoints/view.rs index 8de2f57df5..734b2f0b90 100644 --- a/deepwell/src/endpoints/view.rs +++ b/deepwell/src/endpoints/view.rs @@ -20,7 +20,8 @@ use super::prelude::*; use crate::services::view::{ - GetPageView, GetPageViewOutput, GetUserView, GetUserViewOutput, + GetAdminView, GetAdminViewOutput, GetPageView, GetPageViewOutput, GetUserView, + GetUserViewOutput, }; /// Returns relevant context for rendering a page from a processed web request. @@ -40,3 +41,12 @@ pub async fn user_view( let input: GetUserView = params.parse()?; ViewService::user(ctx, input).await } + +/// Returns relevant context for rendering admin panel from a processed web request. +pub async fn admin_view( + ctx: &ServiceContext<'_>, + params: Params<'static>, +) -> Result { + let input: GetAdminView = params.parse()?; + ViewService::admin(ctx, input).await +} diff --git a/deepwell/src/services/page/service.rs b/deepwell/src/services/page/service.rs index 06fe873601..c55c9d3c77 100644 --- a/deepwell/src/services/page/service.rs +++ b/deepwell/src/services/page/service.rs @@ -565,6 +565,29 @@ impl PageService { Ok(page) } + /// Gets all deleted pages that match the provided slug. + pub async fn get_deleted_by_slug( + ctx: &ServiceContext<'_>, + site_id: i64, + slug: &str, + ) -> Result> { + let txn = ctx.transaction(); + let pages = { + Page::find() + .filter( + Condition::all() + .add(page::Column::Slug.eq(trim_default(slug))) + .add(page::Column::SiteId.eq(site_id)) + .add(page::Column::DeletedAt.is_not_null()), + ) + .order_by_desc(page::Column::CreatedAt) + .all(txn) + .await? + }; + + Ok(pages) + } + /// Get the layout associated with this page. /// /// If this page has a specific layout override, @@ -576,13 +599,10 @@ impl PageService { page_id: i64, ) -> Result { debug!("Getting page layout for site ID {site_id} page ID {page_id}"); - let page = Self::get(ctx, site_id, Reference::Id(page_id)).await?; + let page = Self::get_direct(ctx, page_id, true).await?; match page.layout { // Parse layout from string in page table - Some(layout) => match layout.parse() { - Ok(layout) => Ok(layout), - Err(_) => Err(Error::InvalidEnumValue), - }, + Some(layout) => layout.parse().map_err(|_| Error::InvalidEnumValue), // Fallback to site layout None => SiteService::get_layout(ctx, site_id).await, diff --git a/deepwell/src/services/page/structs.rs b/deepwell/src/services/page/structs.rs index ab4c165b50..0b3967f2e2 100644 --- a/deepwell/src/services/page/structs.rs +++ b/deepwell/src/services/page/structs.rs @@ -65,6 +65,12 @@ pub struct GetPageReferenceDetails<'a> { pub details: PageDetails, } +#[derive(Deserialize, Debug, Clone)] +pub struct GetPageSlug { + pub site_id: i64, + pub slug: String, +} + #[derive(Deserialize, Debug, Clone)] pub struct GetPageDirect { pub site_id: i64, @@ -113,6 +119,23 @@ pub struct GetPageOutput { pub layout: Layout, } +#[derive(Serialize, Debug, Clone)] +pub struct GetDeletedPageOutput { + pub page_id: i64, + pub page_created_at: OffsetDateTime, + pub page_updated_at: Option, + pub page_deleted_at: OffsetDateTime, + pub page_revision_count: i32, + pub site_id: i64, + pub discussion_thread_id: Option, + pub hidden_fields: Vec, + pub title: String, + pub alt_title: Option, + pub slug: String, + pub tags: Vec, + pub rating: ScoreValue, +} + #[derive(Serialize, Debug, Clone)] pub struct GetPageScoreOutput { pub page_id: i64, diff --git a/deepwell/src/services/relation/site_user.rs b/deepwell/src/services/relation/site_user.rs index f2b1bc2f31..188709ecc8 100644 --- a/deepwell/src/services/relation/site_user.rs +++ b/deepwell/src/services/relation/site_user.rs @@ -85,16 +85,7 @@ impl RelationService { } // Checks done, create - create_operation!( - ctx, - SiteMember, - Site, - site_id, - User, - user_id, - created_by, - &() - ) + create_operation!(ctx, SiteUser, Site, site_id, User, user_id, created_by, &()) } pub async fn get_site_user_id_for_site( diff --git a/deepwell/src/services/special_page/service.rs b/deepwell/src/services/special_page/service.rs index 648bb72c19..b5620788d1 100644 --- a/deepwell/src/services/special_page/service.rs +++ b/deepwell/src/services/special_page/service.rs @@ -73,6 +73,7 @@ impl SpecialPageService { SpecialPageType::Banned => { (vec![cow!(config.special_page_banned)], "wiki-page-banned") } + SpecialPageType::Unauthorized => (vec![], "admin-unauthorized"), }; // Look through each option to get the special page wikitext. @@ -131,10 +132,6 @@ impl SpecialPageService { page_info: &PageInfo<'_>, ) -> Result { debug!("Getting wikitext for special page, {} slugs", slugs.len()); - debug_assert!( - !slugs.is_empty(), - "No slugs to check for special page existence", - ); // Try all the pages listed. for slug in slugs { diff --git a/deepwell/src/services/special_page/structs.rs b/deepwell/src/services/special_page/structs.rs index 24f7753d18..0c089c56d8 100644 --- a/deepwell/src/services/special_page/structs.rs +++ b/deepwell/src/services/special_page/structs.rs @@ -27,6 +27,7 @@ pub enum SpecialPageType { Missing, Private, Banned, + Unauthorized, } #[derive(Serialize, Debug)] diff --git a/deepwell/src/services/view/service.rs b/deepwell/src/services/view/service.rs index 79419bb6ba..2ca953ef4c 100644 --- a/deepwell/src/services/view/service.rs +++ b/deepwell/src/services/view/service.rs @@ -358,6 +358,105 @@ impl ViewService { Ok(output) } + pub async fn admin( + ctx: &ServiceContext<'_>, + GetAdminView { + domain, + locales: locales_str, + session_token, + }: GetAdminView, + ) -> Result { + info!( + "Getting site view data for domain '{}', locales '{:?}'", + domain, locales_str, + ); + + // Parse all locales + let mut locales = parse_locales(&locales_str)?; + let config = ctx.config(); + + // Attempt to get a viewer helper structure, but if the site doesn't exist + // then return right away with the "no such site" response. + let viewer = match Self::get_viewer( + ctx, + &mut locales, + &domain, + session_token.ref_map(|s| s.as_str()), + ) + .await? + { + ViewerResult::FoundSite(viewer) => viewer, + ViewerResult::MissingSite(html) => { + return Ok(GetAdminViewOutput::SiteMissing { html }); + } + }; + + let page_info = PageInfo { + page: cow!(""), + category: cow_opt!(Some("admin")), + title: cow!(""), + alt_title: None, + site: cow!(viewer.site.slug), + score: ScoreValue::Integer(0), + tags: vec![], + language: if !locales.is_empty() { + Cow::Owned(locales[0].to_string()) + } else { + cow!(viewer.site.locale) + }, + }; + + let GetSpecialPageOutput { + wikitext: _, + render_output, + } = SpecialPageService::get( + ctx, + &viewer.site, + SpecialPageType::Unauthorized, + &locales, + config.default_page_layout, + page_info, + ) + .await?; + + let RenderOutput { + html_output: + HtmlOutput { + body: compiled_html, + .. + }, + .. + } = render_output; + + // Check user access to site settings + let user_permissions = match viewer.user_session { + Some(ref session) => session.user_permissions, + None => { + debug!("No user for session, disallow admin access"); + + return Ok(GetAdminViewOutput::AdminPermissions { + viewer, + html: compiled_html, + }); + } + }; + + // Determine whether to return the actual admin panel content + let output = if Self::can_access_admin(ctx, user_permissions).await? { + debug!("User has admin access, return data"); + GetAdminViewOutput::SiteFound { viewer } + } else { + warn!("User doesn't have admin access, returning permission page"); + + GetAdminViewOutput::AdminPermissions { + viewer, + html: compiled_html, + } + }; + + Ok(output) + } + /// Gets basic data and runs common logic for all web routes. /// /// All views seen by end users require a few translations before @@ -510,6 +609,16 @@ impl ViewService { Ok(true) } + async fn can_access_admin( + _ctx: &ServiceContext<'_>, + permissions: UserPermissions, + ) -> Result { + info!("Checking admin access: {permissions:?}"); + debug!("TODO: stub"); + // TODO perform permission checks + Ok(true) + } + fn should_redirect_site( ctx: &ServiceContext, site: &SiteModel, diff --git a/deepwell/src/services/view/structs.rs b/deepwell/src/services/view/structs.rs index af3c8e29a0..9f9acae146 100644 --- a/deepwell/src/services/view/structs.rs +++ b/deepwell/src/services/view/structs.rs @@ -115,6 +115,32 @@ pub enum GetUserViewOutput { }, } +#[derive(Deserialize, Debug, Clone)] +pub struct GetAdminView { + pub domain: String, + pub session_token: Option, + pub locales: Vec, +} + +#[derive(Serialize, Debug, Clone)] +#[serde(rename_all = "snake_case", tag = "type", content = "data")] +pub enum GetAdminViewOutput { + SiteFound { + #[serde(flatten)] + viewer: Viewer, + }, + + AdminPermissions { + #[serde(flatten)] + viewer: Viewer, + html: String, + }, + + SiteMissing { + html: String, + }, +} + #[derive(Debug, Clone)] pub enum ViewerResult { FoundSite(Viewer), diff --git a/framerail/src/lib/defaults.ts b/framerail/src/lib/defaults.ts index 4e2f16e1a9..50385539d3 100644 --- a/framerail/src/lib/defaults.ts +++ b/framerail/src/lib/defaults.ts @@ -1,6 +1,9 @@ const defaults = { fallbackLocale: "en", translateKeys: { + // Error + "error": {}, + // Footer "footer-powered-by": {}, "terms": {}, diff --git a/framerail/src/lib/popup/error.svelte b/framerail/src/lib/popup/error.svelte index faca6e4db6..753e277dea 100644 --- a/framerail/src/lib/popup/error.svelte +++ b/framerail/src/lib/popup/error.svelte @@ -1,12 +1,13 @@

UNTRANSLATED:Svelte Error

@@ -67,12 +111,12 @@ as soon as we can figure out prettier support for it. /> + +
+ {/each} + + +

+ + +{#if $page.error.view === "admin_permissions"} + UNTRANSLATED:Lacks permissions for page + {@html $page.error.html} +{:else if $page.error.view === "site_missing"} + UNTRANSLATED:No such site + {@html $page.error.html} +{:else} + UNTRANSLATED:Fallback error, something really went wrong +{/if} + + diff --git a/framerail/src/routes/[x+2d]/admin/+page.server.ts b/framerail/src/routes/[x+2d]/admin/+page.server.ts new file mode 100644 index 0000000000..763a9fa22f --- /dev/null +++ b/framerail/src/routes/[x+2d]/admin/+page.server.ts @@ -0,0 +1,5 @@ +import { loadAdminPage } from "$lib/server/load/admin" + +export async function load({ request, cookies }) { + return loadAdminPage(request, cookies) +} diff --git a/framerail/src/routes/[x+2d]/admin/+page.svelte b/framerail/src/routes/[x+2d]/admin/+page.svelte index 9962560590..80dd867c6d 100644 --- a/framerail/src/routes/[x+2d]/admin/+page.svelte +++ b/framerail/src/routes/[x+2d]/admin/+page.svelte @@ -1 +1,210 @@ -

UNTRANSLATED:TODO: admin panel route

+ + +

UNTRANSLATED:Admin panel route

+ + + +{#if isEdit} +
+ + + + + + + + + + + + +
+ + +
+
+{:else} +
+ {#if $page.data.site.name} +
+ {$page.data.internationalization?.["site-info.name"]} + {$page.data.site.name} +
+ {/if} + + {#if $page.data.site.slug} +
+ {$page.data.internationalization?.["site-info.slug"]} + {$page.data.site.slug} +
+ {/if} + + {#if $page.data.site.tagline} +
+ {$page.data.internationalization?.["site-info.tagline"]} + {$page.data.site.tagline} +
+ {/if} + + {#if $page.data.site.description} +
+ {$page.data.internationalization?.["site-info.description"]} + {$page.data.site.description} +
+ {/if} + + {#if $page.data.site.locale} +
+ {$page.data.internationalization?.["site-info.locale"]} + {$page.data.site.locale} +
+ {/if} + + {#if $page.data.site.layout} +
+ {$page.data.internationalization?.["site-info.layout"]} + {$page.data.internationalization?.[ + `wiki-page-layout.${$page.data.site.layout}` + ]} +
+ {/if} +
+ +
+ +
+{/if} + + diff --git a/framerail/src/routes/[x+2d]/admin/+server.ts b/framerail/src/routes/[x+2d]/admin/+server.ts new file mode 100644 index 0000000000..f2c90bf7b6 --- /dev/null +++ b/framerail/src/routes/[x+2d]/admin/+server.ts @@ -0,0 +1,54 @@ +import { authGetSession } from "$lib/server/auth/getSession" +import { siteUpdate } from "$lib/server/deepwell/admin.js" + +// Handling of server events from client + +export async function POST(event) { + let data = await event.request.formData() + + let sessionToken = event.cookies.get("wikijump_token") + let ipAddr = event.getClientAddress() + let userAgent = event.cookies.get("User-Agent") + + let session = await authGetSession(sessionToken) + + let action = data.get("action")?.toString().toLowerCase() + + let siteIdVal = data.get("site-id")?.toString() + let siteId = siteIdVal ? parseInt(siteIdVal) : null + + let res: object = {} + + try { + if (action === "edit") { + /** Edit site settings. */ + let name = data.get("name")?.toString() + let slug = data.get("slug")?.toString() + let tagline = data.get("tagline")?.toString() + let description = data.get("description")?.toString() + let locale = data.get("locale")?.toString() + let layout = data.get("layout")?.toString().trim() + + res = await siteUpdate( + siteId, + session?.user_id, + name, + slug, + tagline, + description, + locale, + layout + ) + } + + return new Response(JSON.stringify(res)) + } catch (error) { + return new Response( + JSON.stringify({ + message: error.message, + code: error.code, + data: error.data + }) + ) + } +} diff --git a/framerail/src/routes/[x+2d]/login/+page.svelte b/framerail/src/routes/[x+2d]/login/+page.svelte index 2cb149346d..b1a2e93c4e 100644 --- a/framerail/src/routes/[x+2d]/login/+page.svelte +++ b/framerail/src/routes/[x+2d]/login/+page.svelte @@ -20,7 +20,8 @@ } else { showErrorPopup.set({ state: true, - message: res.message + message: res.message, + data: res.data }) } } diff --git a/framerail/src/routes/[x+2d]/user/+page.svelte b/framerail/src/routes/[x+2d]/user/+page.svelte index c22bf8538a..0293782a5d 100644 --- a/framerail/src/routes/[x+2d]/user/+page.svelte +++ b/framerail/src/routes/[x+2d]/user/+page.svelte @@ -21,7 +21,8 @@ if (res?.message) { showErrorPopup.set({ state: true, - message: res.message + message: res.message, + data: res.data }) } else { isEdit = false diff --git a/locales/fluent/admin-panel/en.ftl b/locales/fluent/admin-panel/en.ftl new file mode 100644 index 0000000000..9b1e55a946 --- /dev/null +++ b/locales/fluent/admin-panel/en.ftl @@ -0,0 +1,11 @@ +site-info = + .name = Site name: + .slug = Site domain: + .tagline = Site tagline/subtitle: + .description = Site description: + .locale = Site locale: + .layout = Site layout: + +admin-unauthorized = + Unauthorized + + You do not have the permissions to access the admin panel. diff --git a/locales/fluent/admin-panel/zh_Hans.ftl b/locales/fluent/admin-panel/zh_Hans.ftl new file mode 100644 index 0000000000..0e0393ec50 --- /dev/null +++ b/locales/fluent/admin-panel/zh_Hans.ftl @@ -0,0 +1,11 @@ +site-info = + .name = 网站名称: + .slug = 网站域名: + .tagline = 网站副标题: + .description = 网站描述: + .locale = 网站语言: + .layout = 网站布局: + +admin-unauthorized = + 无权限 + + 您无权访问本网站的管理页面。 diff --git a/locales/fluent/base/en.ftl b/locales/fluent/base/en.ftl index 2ffdb15d21..a723616173 100644 --- a/locales/fluent/base/en.ftl +++ b/locales/fluent/base/en.ftl @@ -37,6 +37,7 @@ docs = Docs download = Download edit = Edit editor = Editor +error = Error footer = Page Footer general = General header = Page Header @@ -57,6 +58,7 @@ preview = Preview privacy = Privacy profile = Profile publish = Publish +restore = Restore reveal-sidebar = Reveal Sidebar save = Save security = Security diff --git a/locales/fluent/base/zh_Hans.ftl b/locales/fluent/base/zh_Hans.ftl index 181edbdd93..cb1923165e 100644 --- a/locales/fluent/base/zh_Hans.ftl +++ b/locales/fluent/base/zh_Hans.ftl @@ -36,6 +36,7 @@ docs = 文档 download = 下载 edit = 编辑 editor = 编辑器 +error = 错误 footer = 页脚 general = 通用 header = 页眉 @@ -56,6 +57,7 @@ preview = 预览 privacy = 隐私 profile = 个人简介 publish = 发布 +restore = 恢复 reveal-sidebar = 展开侧边栏 save = 保存 security = 安全 diff --git a/locales/fluent/wiki-page/en.ftl b/locales/fluent/wiki-page/en.ftl index e18508faad..87f1ac1b93 100644 --- a/locales/fluent/wiki-page/en.ftl +++ b/locales/fluent/wiki-page/en.ftl @@ -4,7 +4,7 @@ wiki-page-category = category: { $category } wiki-page-revision = revision: { $revision } -wiki-page-last-edit = last-edited: { $date } ({ $days -> +wiki-page-last-edit = last edited: { $date } ({ $days -> [0] today [1] yesterday *[other] { $days } days ago @@ -22,6 +22,13 @@ wiki-page-revision-comments = Comments wiki-page-revision-rollback = Revert +wiki-page-revision-type = Type + .create = Create + .regular = Edit + .move = Move + .delete = Delete + .undelete = Restore + ### Wiki Page Vote wiki-page-vote-set = Cast vote @@ -36,11 +43,14 @@ wiki-page-vote-score = Rating wiki-page-move-new-slug = New slug -wiki-page-layout-default = Default layout +wiki-page-layout = + .default = Default layout + .wikidot = Wikidot (Legacy) + .wikijump = Wikijump -wiki-page-layout-wikidot = Wikidot (Legacy) +wiki-page-restore = Select page to restore -wiki-page-layout-wikijump = Wikijump +wiki-page-deleted = Deleted at { $datetime } ### Special Page fallback strings diff --git a/locales/fluent/wiki-page/zh_Hans.ftl b/locales/fluent/wiki-page/zh_Hans.ftl index a55392366c..603bfcd6e3 100644 --- a/locales/fluent/wiki-page/zh_Hans.ftl +++ b/locales/fluent/wiki-page/zh_Hans.ftl @@ -4,7 +4,7 @@ wiki-page-category = 分类:{ $category } wiki-page-revision = 修改版本:{ $revision } -wiki-page-last-edit = 最后一次编辑:{ $date } ({ $days -> +wiki-page-last-edit = 最后编辑于: { $date } ({ $days -> [0] 今日 [1] 昨日 *[other] { $days } 日前 @@ -22,6 +22,13 @@ wiki-page-revision-comments = 编辑摘要 wiki-page-revision-rollback = 回滚 +wiki-page-revision-type = 类型 + .create = 创建 + .regular = 编辑 + .move = 移动 + .delete = 删除 + .undelete = 恢复 + ### 维基页面评分 wiki-page-vote-set = 进行评分 @@ -36,11 +43,14 @@ wiki-page-vote-score = 现时评分 wiki-page-move-new-slug = 新页面网址 -wiki-page-layout-default = 预设布局 +wiki-page-layout = + .default = 预设布局 + .wikidot = Wikidot(旧) + .wikijump = Wikijump -wiki-page-layout-wikidot = Wikidot(旧) +wiki-page-restore = 选择需恢复的页面 -wiki-page-layout-wikijump = Wikijump +wiki-page-deleted = 于{ $datetime }删除 ### 特殊页面