~vpzom/lotide

f443d25dbd6227e30b40bc8a75a926166b9f1c50 — Colin Reeder 7 months ago 6637cc7
Introduce site admins, allow them to edit site description
A migrations/20200911144521_admin/down.sql => migrations/20200911144521_admin/down.sql +3 -0
@@ 0,0 1,3 @@
BEGIN;
	ALTER TABLE person DROP COLUMN is_site_admin;
COMMIT;

A migrations/20200911144521_admin/up.sql => migrations/20200911144521_admin/up.sql +3 -0
@@ 0,0 1,3 @@
BEGIN;
	ALTER TABLE person ADD COLUMN is_site_admin BOOLEAN NOT NULL DEFAULT (FALSE);
COMMIT;

M openapi/openapi.json => openapi/openapi.json +24 -1
@@ 700,6 700,27 @@
						}
					}
				}
			},
			"patch": {
				"summary": "Modify instance settings",
				"requestBody": {
					"required": true,
					"content": {
						"application/json": {
							"schema": {
								"type": "object",
								"properties": {
									"description": {"type": "string"}
								}
							}
						}
					}
				},
				"responses": {
					"204": {
						"description": "Successfully modified."
					}
				}
			}
		},
		"/api/unstable/logins": {


@@ 756,9 777,11 @@
									"properties": {
										"user": {
											"type": "object",
											"required": ["id", "has_unread_notifications"],
											"required": ["id", "name", "is_site_admin", "has_unread_notifications"],
											"properties": {
												"id": {"type": "integer"},
												"name": {"type": "string"},
												"is_site_admin": {"type": "boolean"},
												"has_unread_notifications": {"type": "boolean"}
											}
										}

M res/lang/en.ftl => res/lang/en.ftl +1 -0
@@ 9,6 9,7 @@ no_such_community = No such community
no_such_local_user_by_name = No local user found by that name
no_such_post = No such post
no_such_user = No such user
not_admin = You are not a site admin
password_incorrect = Incorrect password
post_content_conflict = content_markdown and content_text are mutually exclusive
post_href_invalid = Specified URL is not valid

M src/main.rs => src/main.rs +19 -3
@@ 395,6 395,22 @@ pub async fn res_to_error(
    }
}

pub trait ReqParts {
    fn headers(&self) -> &hyper::HeaderMap<hyper::header::HeaderValue>;
}

impl<T> ReqParts for hyper::Request<T> {
    fn headers(&self) -> &hyper::HeaderMap<hyper::header::HeaderValue> {
        self.headers()
    }
}

impl ReqParts for http::request::Parts {
    fn headers(&self) -> &hyper::HeaderMap<hyper::header::HeaderValue> {
        &self.headers
    }
}

lazy_static::lazy_static! {
    static ref LANG_MAP: HashMap<unic_langid::LanguageIdentifier, fluent::FluentResource> = {
        let mut result = HashMap::new();


@@ 433,7 449,7 @@ impl Translator {
    }
}

pub fn get_lang_for_req(req: &hyper::Request<hyper::Body>) -> Translator {
pub fn get_lang_for_req(req: &impl ReqParts) -> Translator {
    let default = unic_langid::langid!("en");
    let languages = match req
        .headers()


@@ 471,7 487,7 @@ pub fn get_lang_for_req(req: &hyper::Request<hyper::Body>) -> Translator {
}

pub async fn authenticate(
    req: &hyper::Request<hyper::Body>,
    req: &impl ReqParts,
    db: &tokio_postgres::Client,
) -> Result<Option<UserLocalID>, Error> {
    use headers::Header;


@@ 504,7 520,7 @@ pub async fn authenticate(
}

pub async fn require_login(
    req: &hyper::Request<hyper::Body>,
    req: &impl ReqParts,
    db: &tokio_postgres::Client,
) -> Result<UserLocalID, Error> {
    authenticate(req, db).await?.ok_or_else(|| {

M src/routes/api/mod.rs => src/routes/api/mod.rs +53 -6
@@ 162,7 162,9 @@ pub fn route_api() -> crate::RouteNode<()> {
            .with_child("communities", communities::route_communities())
            .with_child(
                "instance",
                crate::RouteNode::new().with_handler_async("GET", route_unstable_instance_get),
                crate::RouteNode::new()
                    .with_handler_async("GET", route_unstable_instance_get)
                    .with_handler_async("PATCH", route_unstable_instance_patch),
            )
            .with_child(
                "misc",


@@ 367,13 369,15 @@ async fn route_unstable_logins_current_get(

    let user = crate::require_login(&req, &db).await?;

    let has_notifications: bool = {
        let row = db.query_one("SELECT EXISTS(SELECT 1 FROM notification WHERE to_user = $1 AND created_at > (SELECT last_checked_notifications FROM person WHERE id=$1))", &[&user]).await?;
        row.get(0)
    };
    let row = db.query_one("SELECT username, is_site_admin, EXISTS(SELECT 1 FROM notification WHERE to_user = person.id AND created_at > person.last_checked_notifications) FROM person WHERE id=$1", &[&user]).await?;
    let username: &str = row.get(0);
    let is_site_admin: bool = row.get(1);
    let has_notifications: bool = row.get(2);

    let body = serde_json::to_vec(
        &serde_json::json!({"user": {"id": user, "has_unread_notifications": has_notifications}}),
        &serde_json::json!({
            "user": {"id": user, "name": username, "is_site_admin": is_site_admin, "has_unread_notifications": has_notifications}
        }),
    )?
    .into();



@@ 465,6 469,49 @@ async fn route_unstable_instance_get(
        .body(serde_json::to_vec(&body)?.into())?)
}

async fn route_unstable_instance_patch(
    _: (),
    ctx: Arc<crate::RouteContext>,
    req: hyper::Request<hyper::Body>,
) -> Result<hyper::Response<hyper::Body>, crate::Error> {
    #[derive(Deserialize)]
    struct InstanceEditBody<'a> {
        description: Option<Cow<'a, str>>,
    }

    let lang = crate::get_lang_for_req(&req);

    let (req_parts, body) = req.into_parts();

    let body = hyper::body::to_bytes(body).await?;
    let body: InstanceEditBody = serde_json::from_slice(&body)?;

    let db = ctx.db_pool.get().await?;

    let user = crate::require_login(&req_parts, &db).await?;

    let is_site_admin: bool = {
        let row = db
            .query_one("SELECT is_site_admin FROM person WHERE id=$1", &[&user])
            .await?;
        row.get(0)
    };

    if is_site_admin {
        if let Some(description) = body.description {
            db.execute("UPDATE site SET description=$1", &[&description])
                .await?;
        }

        Ok(crate::empty_response())
    } else {
        Ok(crate::simple_response(
            hyper::StatusCode::FORBIDDEN,
            lang.tr("not_admin", None).into_owned(),
        ))
    }
}

async fn apply_comments_replies<'a, T>(
    comments: &mut Vec<(T, RespPostCommentInfo<'a>)>,
    include_your_for: Option<UserLocalID>,