tetratto/crates/core/src/model/auth.rs

535 lines
14 KiB
Rust
Raw Normal View History

use std::collections::HashMap;
use super::permissions::FinePermission;
use serde::{Deserialize, Serialize};
2025-04-04 21:42:08 -04:00
use totp_rs::TOTP;
use tetratto_shared::{
2025-03-21 01:38:07 -04:00
hash::{hash_salted, salt},
2025-05-06 16:13:48 -04:00
snow::Snowflake,
2025-03-21 01:38:07 -04:00
unix_epoch_timestamp,
};
/// `(ip, token, creation timestamp)`
pub type Token = (String, String, usize);
2025-03-21 01:38:07 -04:00
#[derive(Clone, Debug, Serialize, Deserialize)]
2025-03-21 01:38:07 -04:00
pub struct User {
pub id: usize,
pub created: usize,
pub username: String,
pub password: String,
pub salt: String,
pub settings: UserSettings,
pub tokens: Vec<Token>,
2025-03-23 16:37:43 -04:00
pub permissions: FinePermission,
pub is_verified: bool,
pub notification_count: usize,
pub follower_count: usize,
pub following_count: usize,
2025-04-02 14:11:01 -04:00
pub last_seen: usize,
2025-04-04 21:42:08 -04:00
/// The TOTP secret for this profile. An empty value means the user has TOTP disabled.
#[serde(default)]
pub totp: String,
/// The TOTP recovery codes for this profile.
#[serde(default)]
pub recovery_codes: Vec<String>,
2025-04-12 22:25:54 -04:00
#[serde(default)]
pub post_count: usize,
#[serde(default)]
pub request_count: usize,
/// External service connection details.
#[serde(default)]
pub connections: UserConnections,
2025-05-05 19:38:01 -04:00
/// The user's Stripe customer ID.
#[serde(default)]
pub stripe_id: String,
2025-03-21 01:38:07 -04:00
}
pub type UserConnections =
HashMap<ConnectionService, (ExternalConnectionInfo, ExternalConnectionData)>;
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum ThemePreference {
Auto,
Dark,
Light,
}
impl Default for ThemePreference {
fn default() -> Self {
Self::Auto
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum DefaultTimelineChoice {
MyCommunities,
MyCommunitiesQuestions,
PopularPosts,
PopularQuestions,
FollowingPosts,
FollowingQuestions,
AllPosts,
AllQuestions,
2025-05-09 16:23:40 -04:00
Stack(String),
}
impl Default for DefaultTimelineChoice {
fn default() -> Self {
Self::MyCommunities
}
}
impl DefaultTimelineChoice {
/// Get the relative URL that the timeline should bring you to.
2025-05-09 16:23:40 -04:00
pub fn relative_url(&self) -> String {
match &self {
2025-05-09 16:23:40 -04:00
Self::MyCommunities => "/".to_string(),
Self::MyCommunitiesQuestions => "/questions".to_string(),
Self::PopularPosts => "/popular".to_string(),
Self::PopularQuestions => "/popular/questions".to_string(),
Self::FollowingPosts => "/following".to_string(),
Self::FollowingQuestions => "/following/questions".to_string(),
Self::AllPosts => "/all".to_string(),
Self::AllQuestions => "/all/questions".to_string(),
Self::Stack(id) => format!("/stacks/{id}"),
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize, Default)]
pub struct UserSettings {
#[serde(default)]
pub policy_consent: bool,
#[serde(default)]
pub display_name: String,
#[serde(default)]
pub biography: String,
#[serde(default)]
pub warning: String,
#[serde(default)]
pub private_profile: bool,
#[serde(default)]
pub private_communities: bool,
/// The theme shown to the user.
#[serde(default)]
pub theme_preference: ThemePreference,
/// The theme used on the user's profile. Setting this to `Auto` will use
/// the viewing user's `theme_preference` setting.
#[serde(default)]
pub profile_theme: ThemePreference,
2025-04-02 14:11:01 -04:00
#[serde(default)]
pub private_last_seen: bool,
#[serde(default)]
pub theme_hue: String,
#[serde(default)]
pub theme_sat: String,
#[serde(default)]
pub theme_lit: String,
/// Page background.
#[serde(default)]
pub theme_color_surface: String,
/// Text on elements with the surface backgrounds.
#[serde(default)]
pub theme_color_text: String,
/// Links on all elements.
#[serde(default)]
pub theme_color_text_link: String,
/// Some cards, buttons, or anything else with a darker background color than the surface.
#[serde(default)]
pub theme_color_lowered: String,
/// Text on elements with the lowered backgrounds.
#[serde(default)]
pub theme_color_text_lowered: String,
/// Borders.
#[serde(default)]
pub theme_color_super_lowered: String,
/// Some cards, buttons, or anything else with a lighter background color than the surface.
#[serde(default)]
pub theme_color_raised: String,
/// Text on elements with the raised backgrounds.
#[serde(default)]
pub theme_color_text_raised: String,
/// Some borders.
#[serde(default)]
pub theme_color_super_raised: String,
/// Primary color; navigation bar, some buttons, etc.
#[serde(default)]
pub theme_color_primary: String,
/// Text on elements with the primary backgrounds.
#[serde(default)]
pub theme_color_text_primary: String,
/// Hover state for primary buttons.
#[serde(default)]
pub theme_color_primary_lowered: String,
/// Secondary color.
#[serde(default)]
pub theme_color_secondary: String,
/// Text on elements with the secondary backgrounds.
#[serde(default)]
pub theme_color_text_secondary: String,
/// Hover state for secondary buttons.
#[serde(default)]
pub theme_color_secondary_lowered: String,
/// Custom CSS input.
#[serde(default)]
pub theme_custom_css: String,
#[serde(default)]
pub disable_other_themes: bool,
#[serde(default)]
pub disable_other_theme_css: bool,
2025-04-12 22:25:54 -04:00
#[serde(default)]
pub enable_questions: bool,
/// A header shown in the place of "Ask question" if `enable_questions` is true.
#[serde(default)]
pub motivational_header: String,
2025-04-19 18:59:55 -04:00
/// If questions from anonymous users are allowed. Requires `enable_questions`.
#[serde(default)]
pub allow_anonymous_questions: bool,
/// The username used for anonymous users.
#[serde(default)]
pub anonymous_username: String,
/// The URL of the avatar used for anonymous users.
#[serde(default)]
pub anonymous_avatar_url: String,
/// If dislikes are hidden for the user.
#[serde(default)]
pub hide_dislikes: bool,
2025-04-27 23:11:37 -04:00
/// The timeline that the "Home" button takes you to.
#[serde(default)]
pub default_timeline: DefaultTimelineChoice,
2025-04-27 23:11:37 -04:00
/// If other users that you aren't following can add you to chats.
#[serde(default)]
pub private_chats: bool,
2025-05-03 11:29:31 -04:00
/// The user's status. Shows over connection info.
#[serde(default)]
pub status: String,
/// The mime type of the user's avatar.
#[serde(default = "mime_avif")]
pub avatar_mime: String,
/// The mime type of the user's banner.
#[serde(default = "mime_avif")]
pub banner_mime: String,
}
fn mime_avif() -> String {
"image/avif".to_string()
}
2025-03-21 01:38:07 -04:00
impl Default for User {
fn default() -> Self {
Self::new("<unknown>".to_string(), String::new())
}
}
2025-03-21 01:38:07 -04:00
impl User {
/// Create a new [`User`].
pub fn new(username: String, password: String) -> Self {
let salt = salt();
let password = hash_salted(password, salt.clone());
Self {
2025-05-06 16:13:48 -04:00
id: Snowflake::new().to_string().parse::<usize>().unwrap(),
2025-03-21 01:38:07 -04:00
created: unix_epoch_timestamp() as usize,
username,
password,
salt,
settings: UserSettings::default(),
tokens: Vec::new(),
2025-03-23 16:37:43 -04:00
permissions: FinePermission::DEFAULT,
is_verified: false,
notification_count: 0,
follower_count: 0,
following_count: 0,
2025-04-02 14:11:01 -04:00
last_seen: unix_epoch_timestamp() as usize,
2025-04-04 21:42:08 -04:00
totp: String::new(),
recovery_codes: Vec::new(),
2025-04-12 22:25:54 -04:00
post_count: 0,
request_count: 0,
connections: HashMap::new(),
2025-05-05 19:38:01 -04:00
stripe_id: String::new(),
2025-03-21 01:38:07 -04:00
}
}
/// Deleted user profile.
pub fn deleted() -> Self {
Self {
username: "<deleted>".to_string(),
id: 0,
..Default::default()
}
}
/// Banned user profile.
pub fn banned() -> Self {
Self {
username: "<banned>".to_string(),
id: 0,
..Default::default()
}
}
2025-04-19 18:59:55 -04:00
/// Anonymous user profile.
pub fn anonymous() -> Self {
Self {
username: "anonymous".to_string(),
id: 0,
..Default::default()
}
}
/// Create a new token
///
/// # Returns
/// `(unhashed id, token)`
pub fn create_token(ip: &str) -> (String, Token) {
let unhashed = tetratto_shared::hash::uuid();
(
unhashed.clone(),
(
ip.to_string(),
tetratto_shared::hash::hash(unhashed),
unix_epoch_timestamp() as usize,
),
)
}
/// Check if the given password is correct for the user.
pub fn check_password(&self, against: String) -> bool {
self.password == hash_salted(against, self.salt.clone())
}
/// Parse user mentions in a given `input`.
pub fn parse_mentions(input: &str) -> Vec<String> {
// state
let mut escape: bool = false;
let mut at: bool = false;
let mut buffer: String = String::new();
let mut out = Vec::new();
// parse
for char in input.chars() {
if (char == '\\') && !escape {
escape = true;
continue;
}
if (char == '@') && !escape {
at = true;
continue; // don't push @
}
if at {
if char == ' ' {
// reached space, end @
at = false;
if !out.contains(&buffer) {
out.push(buffer);
}
buffer = String::new();
continue;
}
// push mention text
buffer.push(char);
}
escape = false;
}
if !buffer.is_empty() {
out.push(buffer);
}
// return
out
}
2025-04-04 21:42:08 -04:00
/// Get a [`TOTP`] from the profile's `totp` secret value.
pub fn totp(&self, issuer: Option<String>) -> Option<TOTP> {
if self.totp.is_empty() {
return None;
}
2025-04-10 18:16:52 -04:00
TOTP::new(
2025-04-04 21:42:08 -04:00
totp_rs::Algorithm::SHA1,
6,
1,
30,
self.totp.as_bytes().to_owned(),
Some(issuer.unwrap_or("tetratto!".to_string())),
self.username.clone(),
)
.ok()
2025-04-04 21:42:08 -04:00
}
2025-03-21 01:38:07 -04:00
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum ConnectionService {
/// A connection to a Spotify account.
Spotify,
2025-04-26 19:23:30 -04:00
/// A connection to a last.fm account.
LastFm,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
pub enum ConnectionType {
2025-04-27 23:11:37 -04:00
/// A connection through a token which never expires.
Token,
/// <https://www.rfc-editor.org/rfc/rfc7636>
PKCE,
2025-04-27 23:11:37 -04:00
/// A connection with no stored authentication.
None,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct ExternalConnectionInfo {
pub con_type: ConnectionType,
pub data: HashMap<String, String>,
pub show_on_profile: bool,
}
2025-05-02 20:08:35 -04:00
#[derive(Clone, Debug, Serialize, Deserialize, Default)]
pub struct ExternalConnectionData {
pub external_urls: HashMap<String, String>,
pub data: HashMap<String, String>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Notification {
pub id: usize,
pub created: usize,
pub title: String,
pub content: String,
pub owner: usize,
pub read: bool,
pub tag: String,
}
impl Notification {
/// Returns a new [`Notification`].
pub fn new(title: String, content: String, owner: usize) -> Self {
Self {
2025-05-06 16:13:48 -04:00
id: Snowflake::new().to_string().parse::<usize>().unwrap(),
created: unix_epoch_timestamp() as usize,
title,
content,
owner,
read: false,
tag: String::new(),
}
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct UserFollow {
pub id: usize,
pub created: usize,
pub initiator: usize,
pub receiver: usize,
}
impl UserFollow {
/// Create a new [`UserFollow`].
pub fn new(initiator: usize, receiver: usize) -> Self {
Self {
2025-05-06 16:13:48 -04:00
id: Snowflake::new().to_string().parse::<usize>().unwrap(),
created: unix_epoch_timestamp() as usize,
initiator,
receiver,
}
}
}
#[derive(Serialize, Deserialize, PartialEq, Eq)]
pub enum FollowResult {
/// Request sent to follow other user.
Requested,
/// Successfully followed other user.
Followed,
}
#[derive(Serialize, Deserialize)]
pub struct UserBlock {
pub id: usize,
pub created: usize,
pub initiator: usize,
pub receiver: usize,
}
impl UserBlock {
/// Create a new [`UserBlock`].
pub fn new(initiator: usize, receiver: usize) -> Self {
Self {
2025-05-06 16:13:48 -04:00
id: Snowflake::new().to_string().parse::<usize>().unwrap(),
created: unix_epoch_timestamp() as usize,
initiator,
receiver,
}
}
}
2025-04-19 18:59:55 -04:00
#[derive(Serialize, Deserialize)]
pub struct IpBlock {
pub id: usize,
pub created: usize,
pub initiator: usize,
pub receiver: String,
}
impl IpBlock {
/// Create a new [`IpBlock`].
pub fn new(initiator: usize, receiver: String) -> Self {
Self {
2025-05-06 16:13:48 -04:00
id: Snowflake::new().to_string().parse::<usize>().unwrap(),
2025-04-19 18:59:55 -04:00
created: unix_epoch_timestamp() as usize,
initiator,
receiver,
}
}
}
#[derive(Serialize, Deserialize)]
pub struct IpBan {
pub ip: String,
pub created: usize,
pub reason: String,
pub moderator: usize,
}
impl IpBan {
/// Create a new [`IpBan`].
pub fn new(ip: String, moderator: usize, reason: String) -> Self {
Self {
ip,
created: unix_epoch_timestamp() as usize,
reason,
moderator,
}
}
}
2025-04-11 22:12:43 -04:00
#[derive(Serialize, Deserialize)]
pub struct UserWarning {
pub id: usize,
pub created: usize,
pub receiver: usize,
pub moderator: usize,
pub content: String,
}
impl UserWarning {
/// Create a new [`UserWarning`].
pub fn new(user: usize, moderator: usize, content: String) -> Self {
Self {
2025-05-06 16:13:48 -04:00
id: Snowflake::new().to_string().parse::<usize>().unwrap(),
2025-04-11 22:12:43 -04:00
created: unix_epoch_timestamp() as usize,
receiver: user,
moderator,
content,
}
}
}