add: full journals api

add: full notes api
This commit is contained in:
trisua 2025-06-18 21:00:07 -04:00
parent 102ea0ee35
commit 42421bd906
11 changed files with 476 additions and 12 deletions

View file

@ -1019,6 +1019,18 @@
("data-turbo" "false") ("data-turbo" "false")
(icon (text "rabbit")) (icon (text "rabbit"))
(str (text "general:link.reference"))) (str (text "general:link.reference")))
(a
("href" "{{ config.policies.terms_of_service }}")
("class" "button")
(icon (text "heart-handshake"))
(text "Terms of service"))
(a
("href" "{{ config.policies.privacy }}")
("class" "button")
(icon (text "cookie"))
(text "Privacy policy"))
(b ("class" "title") (str (text "general:label.account"))) (b ("class" "title") (str (text "general:label.account")))
(button (button
("onclick" "trigger('me::switch_account')") ("onclick" "trigger('me::switch_account')")

View file

@ -0,0 +1,153 @@
use axum::{
response::IntoResponse,
extract::{Json, Path},
Extension,
};
use axum_extra::extract::CookieJar;
use crate::{
get_user_from_token,
routes::api::v1::{UpdateJournalView, CreateJournal, UpdateJournalTitle},
State,
};
use tetratto_core::model::{
journals::{Journal, JournalPrivacyPermission},
oauth,
permissions::FinePermission,
ApiReturn, Error,
};
pub async fn get_request(
jar: CookieJar,
Path(id): Path<usize>,
Extension(data): Extension<State>,
) -> impl IntoResponse {
let data = &(data.read().await).0;
let user = match get_user_from_token!(jar, data, oauth::AppScope::UserReadJournals) {
Some(ua) => ua,
None => return Json(Error::NotAllowed.into()),
};
let journal = match data.get_journal_by_id(id).await {
Ok(x) => x,
Err(e) => return Json(e.into()),
};
if journal.privacy == JournalPrivacyPermission::Private
&& user.id != journal.owner
&& !user.permissions.contains(FinePermission::MANAGE_JOURNALS)
{
return Json(Error::NotAllowed.into());
}
Json(ApiReturn {
ok: true,
message: "Success".to_string(),
payload: Some(journal),
})
}
pub async fn list_request(jar: CookieJar, Extension(data): Extension<State>) -> impl IntoResponse {
let data = &(data.read().await).0;
let user = match get_user_from_token!(jar, data, oauth::AppScope::UserReadJournals) {
Some(ua) => ua,
None => return Json(Error::NotAllowed.into()),
};
match data.get_journals_by_user(user.id).await {
Ok(x) => Json(ApiReturn {
ok: true,
message: "Success".to_string(),
payload: Some(x),
}),
Err(e) => Json(e.into()),
}
}
pub async fn create_request(
jar: CookieJar,
Extension(data): Extension<State>,
Json(props): Json<CreateJournal>,
) -> impl IntoResponse {
let data = &(data.read().await).0;
let user = match get_user_from_token!(jar, data, oauth::AppScope::UserCreateJournals) {
Some(ua) => ua,
None => return Json(Error::NotAllowed.into()),
};
match data
.create_journal(Journal::new(user.id, props.title))
.await
{
Ok(x) => Json(ApiReturn {
ok: true,
message: "Journal created".to_string(),
payload: Some(x),
}),
Err(e) => Json(e.into()),
}
}
pub async fn update_title_request(
jar: CookieJar,
Path(id): Path<usize>,
Extension(data): Extension<State>,
Json(props): Json<UpdateJournalTitle>,
) -> impl IntoResponse {
let data = &(data.read().await).0;
let user = match get_user_from_token!(jar, data, oauth::AppScope::UserManageJournals) {
Some(ua) => ua,
None => return Json(Error::NotAllowed.into()),
};
match data.update_journal_title(id, &user, &props.title).await {
Ok(_) => Json(ApiReturn {
ok: true,
message: "Journal updated".to_string(),
payload: (),
}),
Err(e) => Json(e.into()),
}
}
pub async fn update_privacy_request(
jar: CookieJar,
Path(id): Path<usize>,
Extension(data): Extension<State>,
Json(props): Json<UpdateJournalView>,
) -> impl IntoResponse {
let data = &(data.read().await).0;
let user = match get_user_from_token!(jar, data, oauth::AppScope::UserManageJournals) {
Some(ua) => ua,
None => return Json(Error::NotAllowed.into()),
};
match data.update_journal_privacy(id, &user, props.view).await {
Ok(_) => Json(ApiReturn {
ok: true,
message: "Journal updated".to_string(),
payload: (),
}),
Err(e) => Json(e.into()),
}
}
pub async fn delete_request(
jar: CookieJar,
Path(id): Path<usize>,
Extension(data): Extension<State>,
) -> impl IntoResponse {
let data = &(data.read().await).0;
let user = match get_user_from_token!(jar, data, oauth::AppScope::UserManageJournals) {
Some(ua) => ua,
None => return Json(Error::NotAllowed.into()),
};
match data.delete_journal(id, &user).await {
Ok(_) => Json(ApiReturn {
ok: true,
message: "Journal deleted".to_string(),
payload: (),
}),
Err(e) => Json(e.into()),
}
}

View file

@ -2,6 +2,8 @@ pub mod apps;
pub mod auth; pub mod auth;
pub mod channels; pub mod channels;
pub mod communities; pub mod communities;
pub mod journals;
pub mod notes;
pub mod notifications; pub mod notifications;
pub mod reactions; pub mod reactions;
pub mod reports; pub mod reports;
@ -22,6 +24,7 @@ use tetratto_core::model::{
PollOption, PostContext, PollOption, PostContext,
}, },
communities_permissions::CommunityPermission, communities_permissions::CommunityPermission,
journals::JournalPrivacyPermission,
oauth::AppScope, oauth::AppScope,
permissions::FinePermission, permissions::FinePermission,
reactions::AssetType, reactions::AssetType,
@ -530,7 +533,9 @@ pub fn routes() -> Router {
delete(communities::emojis::delete_request), delete(communities::emojis::delete_request),
) )
// stacks // stacks
.route("/stacks", get(stacks::list_request))
.route("/stacks", post(stacks::create_request)) .route("/stacks", post(stacks::create_request))
.route("/stacks/{id}", get(stacks::get_request))
.route("/stacks/{id}/name", post(stacks::update_name_request)) .route("/stacks/{id}/name", post(stacks::update_name_request))
.route("/stacks/{id}/privacy", post(stacks::update_privacy_request)) .route("/stacks/{id}/privacy", post(stacks::update_privacy_request))
.route("/stacks/{id}/mode", post(stacks::update_mode_request)) .route("/stacks/{id}/mode", post(stacks::update_mode_request))
@ -541,6 +546,23 @@ pub fn routes() -> Router {
.route("/stacks/{id}/block", post(stacks::block_request)) .route("/stacks/{id}/block", post(stacks::block_request))
.route("/stacks/{id}/block", delete(stacks::unblock_request)) .route("/stacks/{id}/block", delete(stacks::unblock_request))
.route("/stacks/{id}", delete(stacks::delete_request)) .route("/stacks/{id}", delete(stacks::delete_request))
// journals
.route("/journals", get(journals::list_request))
.route("/journals", post(journals::create_request))
.route("/journals/{id}", get(journals::get_request))
.route("/journals/{id}", delete(journals::delete_request))
.route("/journals/{id}/title", post(journals::update_title_request))
.route(
"/journals/{id}/privacy",
post(journals::update_privacy_request),
)
// notes
.route("/notes", post(notes::create_request))
.route("/notes/{id}", get(notes::get_request))
.route("/notes/{id}", delete(notes::delete_request))
.route("/notes/{id}/title", post(notes::update_title_request))
.route("/notes/{id}/content", post(notes::update_content_request))
.route("/notes/from_journal/{id}", get(notes::list_request))
// uploads // uploads
.route("/uploads/{id}", get(uploads::get_request)) .route("/uploads/{id}", get(uploads::get_request))
.route("/uploads/{id}", delete(uploads::delete_request)) .route("/uploads/{id}", delete(uploads::delete_request))
@ -846,3 +868,35 @@ pub struct CreateGrant {
pub struct RefreshGrantToken { pub struct RefreshGrantToken {
pub verifier: String, pub verifier: String,
} }
#[derive(Deserialize)]
pub struct CreateJournal {
pub title: String,
}
#[derive(Deserialize)]
pub struct CreateNote {
pub title: String,
pub content: String,
pub journal: String,
}
#[derive(Deserialize)]
pub struct UpdateJournalTitle {
pub title: String,
}
#[derive(Deserialize)]
pub struct UpdateJournalView {
pub view: JournalPrivacyPermission,
}
#[derive(Deserialize)]
pub struct UpdateNoteTitle {
pub title: String,
}
#[derive(Deserialize)]
pub struct UpdateNoteContent {
pub content: String,
}

View file

@ -0,0 +1,182 @@
use axum::{
response::IntoResponse,
extract::{Json, Path},
Extension,
};
use axum_extra::extract::CookieJar;
use crate::{
get_user_from_token,
routes::api::v1::{CreateNote, UpdateNoteContent, UpdateNoteTitle},
State,
};
use tetratto_core::model::{
journals::{JournalPrivacyPermission, Note},
oauth,
permissions::FinePermission,
ApiReturn, Error,
};
pub async fn get_request(
jar: CookieJar,
Path(id): Path<usize>,
Extension(data): Extension<State>,
) -> impl IntoResponse {
let data = &(data.read().await).0;
let user = match get_user_from_token!(jar, data, oauth::AppScope::UserReadJournals) {
Some(ua) => ua,
None => return Json(Error::NotAllowed.into()),
};
let note = match data.get_note_by_id(id).await {
Ok(x) => x,
Err(e) => return Json(e.into()),
};
let journal = match data.get_journal_by_id(note.id).await {
Ok(x) => x,
Err(e) => return Json(e.into()),
};
if journal.privacy == JournalPrivacyPermission::Private
&& user.id != journal.owner
&& !user.permissions.contains(FinePermission::MANAGE_JOURNALS)
{
return Json(Error::NotAllowed.into());
}
Json(ApiReturn {
ok: true,
message: "Success".to_string(),
payload: Some(note),
})
}
pub async fn list_request(
jar: CookieJar,
Path(id): Path<usize>,
Extension(data): Extension<State>,
) -> impl IntoResponse {
let data = &(data.read().await).0;
let user = match get_user_from_token!(jar, data, oauth::AppScope::UserReadJournals) {
Some(ua) => ua,
None => return Json(Error::NotAllowed.into()),
};
let journal = match data.get_journal_by_id(id).await {
Ok(x) => x,
Err(e) => return Json(e.into()),
};
if journal.privacy == JournalPrivacyPermission::Private
&& user.id != journal.owner
&& !user.permissions.contains(FinePermission::MANAGE_JOURNALS)
{
return Json(Error::NotAllowed.into());
}
match data.get_notes_by_journal(id).await {
Ok(x) => Json(ApiReturn {
ok: true,
message: "Success".to_string(),
payload: Some(x),
}),
Err(e) => Json(e.into()),
}
}
pub async fn create_request(
jar: CookieJar,
Extension(data): Extension<State>,
Json(props): Json<CreateNote>,
) -> impl IntoResponse {
let data = &(data.read().await).0;
let user = match get_user_from_token!(jar, data, oauth::AppScope::UserCreateNotes) {
Some(ua) => ua,
None => return Json(Error::NotAllowed.into()),
};
match data
.create_note(Note::new(
user.id,
props.title,
match props.journal.parse() {
Ok(x) => x,
Err(_) => return Json(Error::Unknown.into()),
},
props.content,
))
.await
{
Ok(x) => Json(ApiReturn {
ok: true,
message: "Note created".to_string(),
payload: Some(x),
}),
Err(e) => Json(e.into()),
}
}
pub async fn update_title_request(
jar: CookieJar,
Path(id): Path<usize>,
Extension(data): Extension<State>,
Json(props): Json<UpdateNoteTitle>,
) -> impl IntoResponse {
let data = &(data.read().await).0;
let user = match get_user_from_token!(jar, data, oauth::AppScope::UserManageNotes) {
Some(ua) => ua,
None => return Json(Error::NotAllowed.into()),
};
match data.update_note_title(id, &user, &props.title).await {
Ok(_) => Json(ApiReturn {
ok: true,
message: "Note updated".to_string(),
payload: (),
}),
Err(e) => Json(e.into()),
}
}
pub async fn update_content_request(
jar: CookieJar,
Path(id): Path<usize>,
Extension(data): Extension<State>,
Json(props): Json<UpdateNoteContent>,
) -> impl IntoResponse {
let data = &(data.read().await).0;
let user = match get_user_from_token!(jar, data, oauth::AppScope::UserManageNotes) {
Some(ua) => ua,
None => return Json(Error::NotAllowed.into()),
};
match data.update_note_content(id, &user, &props.content).await {
Ok(_) => Json(ApiReturn {
ok: true,
message: "Note updated".to_string(),
payload: (),
}),
Err(e) => Json(e.into()),
}
}
pub async fn delete_request(
jar: CookieJar,
Path(id): Path<usize>,
Extension(data): Extension<State>,
) -> impl IntoResponse {
let data = &(data.read().await).0;
let user = match get_user_from_token!(jar, data, oauth::AppScope::UserManageNotes) {
Some(ua) => ua,
None => return Json(Error::NotAllowed.into()),
};
match data.delete_note(id, &user).await {
Ok(_) => Json(ApiReturn {
ok: true,
message: "Note deleted".to_string(),
payload: (),
}),
Err(e) => Json(e.into()),
}
}

View file

@ -19,6 +19,54 @@ use super::{
UpdateStackSort, UpdateStackSort,
}; };
pub async fn get_request(
jar: CookieJar,
Path(id): Path<usize>,
Extension(data): Extension<State>,
) -> impl IntoResponse {
let data = &(data.read().await).0;
let user = match get_user_from_token!(jar, data, oauth::AppScope::UserReadStacks) {
Some(ua) => ua,
None => return Json(Error::NotAllowed.into()),
};
let stack = match data.get_stack_by_id(id).await {
Ok(x) => x,
Err(e) => return Json(e.into()),
};
if stack.privacy == StackPrivacy::Private
&& user.id != stack.owner
&& ((stack.mode != StackMode::Circle) | stack.users.contains(&user.id))
&& !user.permissions.check(FinePermission::MANAGE_STACKS)
{
return Json(Error::NotAllowed.into());
}
Json(ApiReturn {
ok: true,
message: "Success".to_string(),
payload: Some(stack),
})
}
pub async fn list_request(jar: CookieJar, Extension(data): Extension<State>) -> impl IntoResponse {
let data = &(data.read().await).0;
let user = match get_user_from_token!(jar, data, oauth::AppScope::UserReadStacks) {
Some(ua) => ua,
None => return Json(Error::NotAllowed.into()),
};
match data.get_stacks_by_user(user.id).await {
Ok(x) => Json(ApiReturn {
ok: true,
message: "Success".to_string(),
payload: Some(x),
}),
Err(e) => Json(e.into()),
}
}
pub async fn create_request( pub async fn create_request(
jar: CookieJar, jar: CookieJar,
Extension(data): Extension<State>, Extension(data): Extension<State>,

View file

@ -1,7 +1,7 @@
CREATE TABLE IF NOT EXISTS channels ( CREATE TABLE IF NOT EXISTS journals (
id BIGINT NOT NULL PRIMARY KEY, id BIGINT NOT NULL PRIMARY KEY,
created BIGINT NOT NULL, created BIGINT NOT NULL,
owner BIGINT NOT NULL, owner BIGINT NOT NULL,
title TEXT NOT NULL, title TEXT NOT NULL,
view TEXT NOT NULL privacy TEXT NOT NULL
) )

View file

@ -1,4 +1,4 @@
CREATE TABLE IF NOT EXISTS channels ( CREATE TABLE IF NOT EXISTS notes (
id BIGINT NOT NULL PRIMARY KEY, id BIGINT NOT NULL PRIMARY KEY,
created BIGINT NOT NULL, created BIGINT NOT NULL,
owner BIGINT NOT NULL, owner BIGINT NOT NULL,

View file

@ -3,7 +3,7 @@ use crate::{
model::{ model::{
auth::User, auth::User,
permissions::FinePermission, permissions::FinePermission,
journals::{Journal, JournalViewPermission}, journals::{Journal, JournalPrivacyPermission},
Error, Result, Error, Result,
}, },
}; };
@ -18,7 +18,7 @@ impl DataManager {
created: get!(x->1(i64)) as usize, created: get!(x->1(i64)) as usize,
owner: get!(x->2(i64)) as usize, owner: get!(x->2(i64)) as usize,
title: get!(x->3(String)), title: get!(x->3(String)),
view: serde_json::from_str(&get!(x->4(String))).unwrap(), privacy: serde_json::from_str(&get!(x->4(String))).unwrap(),
} }
} }
@ -36,7 +36,7 @@ impl DataManager {
let res = query_rows!( let res = query_rows!(
&conn, &conn,
"SELECT * FROM journals WHERE owner = $1 ORDER BY name ASC", "SELECT * FROM journals WHERE owner = $1 ORDER BY title ASC",
&[&(id as i64)], &[&(id as i64)],
|x| { Self::get_journal_from_row(x) } |x| { Self::get_journal_from_row(x) }
); );
@ -89,7 +89,7 @@ impl DataManager {
&(data.created as i64), &(data.created as i64),
&(data.owner as i64), &(data.owner as i64),
&data.title, &data.title,
&serde_json::to_string(&data.view).unwrap(), &serde_json::to_string(&data.privacy).unwrap(),
] ]
); );
@ -137,5 +137,5 @@ impl DataManager {
} }
auto_method!(update_journal_title(&str)@get_journal_by_id:MANAGE_JOURNALS -> "UPDATE journals SET title = $1 WHERE id = $2" --cache-key-tmpl="atto.journal:{}"); auto_method!(update_journal_title(&str)@get_journal_by_id:MANAGE_JOURNALS -> "UPDATE journals SET title = $1 WHERE id = $2" --cache-key-tmpl="atto.journal:{}");
auto_method!(update_journal_view(JournalViewPermission)@get_journal_by_id:MANAGE_JOURNALS -> "UPDATE journals SET privacy = $1 WHERE id = $2" --serde --cache-key-tmpl="atto.journal:{}"); auto_method!(update_journal_privacy(JournalPrivacyPermission)@get_journal_by_id:MANAGE_JOURNALS -> "UPDATE journals SET privacy = $1 WHERE id = $2" --serde --cache-key-tmpl="atto.journal:{}");
} }

View file

@ -121,4 +121,5 @@ impl DataManager {
} }
auto_method!(update_note_title(&str)@get_note_by_id:MANAGE_NOTES -> "UPDATE notes SET title = $1 WHERE id = $2" --cache-key-tmpl="atto.note:{}"); auto_method!(update_note_title(&str)@get_note_by_id:MANAGE_NOTES -> "UPDATE notes SET title = $1 WHERE id = $2" --cache-key-tmpl="atto.note:{}");
auto_method!(update_note_content(&str)@get_note_by_id:MANAGE_NOTES -> "UPDATE notes SET content = $1 WHERE id = $2" --cache-key-tmpl="atto.note:{}");
} }

View file

@ -2,14 +2,14 @@ use serde::{Serialize, Deserialize};
use tetratto_shared::{snow::Snowflake, unix_epoch_timestamp}; use tetratto_shared::{snow::Snowflake, unix_epoch_timestamp};
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
pub enum JournalViewPermission { pub enum JournalPrivacyPermission {
/// Can be accessed by anyone via link. /// Can be accessed by anyone via link.
Public, Public,
/// Visible only to the journal owner. /// Visible only to the journal owner.
Private, Private,
} }
impl Default for JournalViewPermission { impl Default for JournalPrivacyPermission {
fn default() -> Self { fn default() -> Self {
Self::Private Self::Private
} }
@ -21,7 +21,7 @@ pub struct Journal {
pub created: usize, pub created: usize,
pub owner: usize, pub owner: usize,
pub title: String, pub title: String,
pub view: JournalViewPermission, pub privacy: JournalPrivacyPermission,
} }
impl Journal { impl Journal {
@ -32,7 +32,7 @@ impl Journal {
created: unix_epoch_timestamp(), created: unix_epoch_timestamp(),
owner, owner,
title, title,
view: JournalViewPermission::default(), privacy: JournalPrivacyPermission::default(),
} }
} }
} }

View file

@ -62,6 +62,12 @@ pub enum AppScope {
UserReadRequests, UserReadRequests,
/// Read questions as the user. /// Read questions as the user.
UserReadQuestions, UserReadQuestions,
/// Read the user's stacks.
UserReadStacks,
/// Read the user's journals.
UserReadJournals,
/// Read the user's notes.
UserReadNotes,
/// Create posts as the user. /// Create posts as the user.
UserCreatePosts, UserCreatePosts,
/// Create messages as the user. /// Create messages as the user.
@ -76,6 +82,10 @@ pub enum AppScope {
UserCreateCommunities, UserCreateCommunities,
/// Create stacks on behalf of the user. /// Create stacks on behalf of the user.
UserCreateStacks, UserCreateStacks,
/// Create journals on behalf of the user.
UserCreateJournals,
/// Create notes on behalf of the user.
UserCreateNotes,
/// Delete posts owned by the user. /// Delete posts owned by the user.
UserDeletePosts, UserDeletePosts,
/// Delete messages owned by the user. /// Delete messages owned by the user.
@ -106,6 +116,10 @@ pub enum AppScope {
UserManageRequests, UserManageRequests,
/// Manage the user's uploads. /// Manage the user's uploads.
UserManageUploads, UserManageUploads,
/// Manage the user's journals.
UserManageJournals,
/// Manage the user's notes.
UserManageNotes,
/// Edit posts created by the user. /// Edit posts created by the user.
UserEditPosts, UserEditPosts,
/// Edit drafts created by the user. /// Edit drafts created by the user.