Friends system for app (#2958)
* Friends system for app * Fix impl issues * move friends to in-memory store
This commit is contained in:
parent
7184c5f5c7
commit
47b0ccdf78
5
Cargo.lock
generated
5
Cargo.lock
generated
@ -280,13 +280,14 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "actix-ws"
|
name = "actix-ws"
|
||||||
version = "0.2.5"
|
version = "0.3.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "535aec173810be3ca6f25dd5b4d431ae7125d62000aa3cbae1ec739921b02cf3"
|
checksum = "a3a1fb4f9f2794b0aadaf2ba5f14a6f034c7e86957b458c506a8cb75953f2d99"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"actix-codec",
|
"actix-codec",
|
||||||
"actix-http",
|
"actix-http",
|
||||||
"actix-web",
|
"actix-web",
|
||||||
|
"bytestring",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"tokio 1.40.0",
|
"tokio 1.40.0",
|
||||||
]
|
]
|
||||||
|
|||||||
@ -9,7 +9,7 @@ import {
|
|||||||
SteamIcon,
|
SteamIcon,
|
||||||
GitLabIcon,
|
GitLabIcon,
|
||||||
} from '@/assets/external'
|
} from '@/assets/external'
|
||||||
import { login, login_2fa, create_account, login_pass } from '@/helpers/mr_auth.js'
|
import { login } from '@/helpers/mr_auth.js'
|
||||||
import { handleError, useNotifications } from '@/store/state.js'
|
import { handleError, useNotifications } from '@/store/state.js'
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { handleSevereError } from '@/store/error.js'
|
import { handleSevereError } from '@/store/error.js'
|
||||||
@ -72,7 +72,7 @@ const confirmPassword = ref('')
|
|||||||
const subscribe = ref(true)
|
const subscribe = ref(true)
|
||||||
|
|
||||||
async function signInOauth(provider) {
|
async function signInOauth(provider) {
|
||||||
const creds = await login(provider).catch(handleSevereError)
|
const creds = await login().catch(handleSevereError)
|
||||||
|
|
||||||
if (creds && creds.type === 'two_factor_required') {
|
if (creds && creds.type === 'two_factor_required') {
|
||||||
twoFactorFlow.value = creds.flow
|
twoFactorFlow.value = creds.flow
|
||||||
|
|||||||
@ -5,26 +5,8 @@
|
|||||||
*/
|
*/
|
||||||
import { invoke } from '@tauri-apps/api/core'
|
import { invoke } from '@tauri-apps/api/core'
|
||||||
|
|
||||||
export async function login(provider) {
|
export async function login() {
|
||||||
return await invoke('modrinth_auth_login', { provider })
|
return await invoke('plugin:mr-auth|modrinth_login')
|
||||||
}
|
|
||||||
|
|
||||||
export async function login_pass(username, password, challenge) {
|
|
||||||
return await invoke('plugin:mr-auth|login_pass', { username, password, challenge })
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function login_2fa(code, flow) {
|
|
||||||
return await invoke('plugin:mr-auth|login_2fa', { code, flow })
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function create_account(username, email, password, challenge, signUpNewsletter) {
|
|
||||||
return await invoke('plugin:mr-auth|create_account', {
|
|
||||||
username,
|
|
||||||
email,
|
|
||||||
password,
|
|
||||||
challenge,
|
|
||||||
signUpNewsletter,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function logout() {
|
export async function logout() {
|
||||||
|
|||||||
@ -102,13 +102,7 @@ fn main() {
|
|||||||
.plugin(
|
.plugin(
|
||||||
"mr-auth",
|
"mr-auth",
|
||||||
InlinedPlugin::new()
|
InlinedPlugin::new()
|
||||||
.commands(&[
|
.commands(&["modrinth_login", "logout", "get"])
|
||||||
"login_pass",
|
|
||||||
"login_2fa",
|
|
||||||
"create_account",
|
|
||||||
"logout",
|
|
||||||
"get",
|
|
||||||
])
|
|
||||||
.default_permission(
|
.default_permission(
|
||||||
DefaultPermissionRule::AllowAllCommands,
|
DefaultPermissionRule::AllowAllCommands,
|
||||||
),
|
),
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@ -2410,6 +2410,11 @@
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"const": "mr-auth:allow-logout"
|
"const": "mr-auth:allow-logout"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the modrinth_login command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "mr-auth:allow-modrinth-login"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"description": "Denies the create_account command without any pre-configured scope.",
|
"description": "Denies the create_account command without any pre-configured scope.",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@ -2435,6 +2440,11 @@
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"const": "mr-auth:deny-logout"
|
"const": "mr-auth:deny-logout"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the modrinth_login command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "mr-auth:deny-modrinth-login"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"description": "This permission set configures which\noperating system information are available\nto gather from the frontend.\n\n#### Granted Permissions\n\nAll information except the host name are available.\n\n",
|
"description": "This permission set configures which\noperating system information are available\nto gather from the frontend.\n\n#### Granted Permissions\n\nAll information except the host name are available.\n\n",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
|
|||||||
@ -2410,6 +2410,11 @@
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"const": "mr-auth:allow-logout"
|
"const": "mr-auth:allow-logout"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the modrinth_login command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "mr-auth:allow-modrinth-login"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"description": "Denies the create_account command without any pre-configured scope.",
|
"description": "Denies the create_account command without any pre-configured scope.",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@ -2435,6 +2440,11 @@
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"const": "mr-auth:deny-logout"
|
"const": "mr-auth:deny-logout"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the modrinth_login command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "mr-auth:deny-modrinth-login"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"description": "This permission set configures which\noperating system information are available\nto gather from the frontend.\n\n#### Granted Permissions\n\nAll information except the host name are available.\n\n",
|
"description": "This permission set configures which\noperating system information are available\nto gather from the frontend.\n\n#### Granted Permissions\n\nAll information except the host name are available.\n\n",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
|
|||||||
@ -1,27 +1,20 @@
|
|||||||
use crate::api::Result;
|
use crate::api::Result;
|
||||||
use chrono::{Duration, Utc};
|
use chrono::{Duration, Utc};
|
||||||
use tauri::plugin::TauriPlugin;
|
use tauri::plugin::TauriPlugin;
|
||||||
use tauri::{Manager, UserAttentionType};
|
use tauri::{Manager, Runtime, UserAttentionType};
|
||||||
use theseus::prelude::*;
|
use theseus::prelude::*;
|
||||||
|
|
||||||
pub fn init<R: tauri::Runtime>() -> TauriPlugin<R> {
|
pub fn init<R: tauri::Runtime>() -> TauriPlugin<R> {
|
||||||
tauri::plugin::Builder::new("mr-auth")
|
tauri::plugin::Builder::new("mr-auth")
|
||||||
.invoke_handler(tauri::generate_handler![
|
.invoke_handler(tauri::generate_handler![modrinth_login, logout, get,])
|
||||||
login_pass,
|
|
||||||
login_2fa,
|
|
||||||
create_account,
|
|
||||||
logout,
|
|
||||||
get,
|
|
||||||
])
|
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn modrinth_auth_login(
|
pub async fn modrinth_login<R: Runtime>(
|
||||||
app: tauri::AppHandle,
|
app: tauri::AppHandle<R>,
|
||||||
provider: &str,
|
) -> Result<Option<ModrinthCredentials>> {
|
||||||
) -> Result<Option<ModrinthCredentialsResult>> {
|
let redirect_uri = mr_auth::authenticate_begin_flow();
|
||||||
let redirect_uri = mr_auth::authenticate_begin_flow(provider);
|
|
||||||
|
|
||||||
let start = Utc::now();
|
let start = Utc::now();
|
||||||
|
|
||||||
@ -39,6 +32,10 @@ pub async fn modrinth_auth_login(
|
|||||||
.as_error()
|
.as_error()
|
||||||
})?),
|
})?),
|
||||||
)
|
)
|
||||||
|
.min_inner_size(420.0, 632.0)
|
||||||
|
.inner_size(420.0, 632.0)
|
||||||
|
.max_inner_size(420.0, 632.0)
|
||||||
|
.zoom_hotkeys_enabled(false)
|
||||||
.title("Sign into Modrinth")
|
.title("Sign into Modrinth")
|
||||||
.always_on_top(true)
|
.always_on_top(true)
|
||||||
.center()
|
.center()
|
||||||
@ -55,23 +52,21 @@ pub async fn modrinth_auth_login(
|
|||||||
if window
|
if window
|
||||||
.url()?
|
.url()?
|
||||||
.as_str()
|
.as_str()
|
||||||
.starts_with("https://launcher-files.modrinth.com/detect.txt")
|
.starts_with("https://launcher-files.modrinth.com")
|
||||||
{
|
{
|
||||||
let query = window
|
let url = window.url()?;
|
||||||
.url()?
|
|
||||||
.query_pairs()
|
let code = url.query_pairs().find(|(key, _)| key == "code");
|
||||||
.map(|(key, val)| {
|
|
||||||
(
|
|
||||||
key.to_string(),
|
|
||||||
serde_json::Value::String(val.to_string()),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
window.close()?;
|
window.close()?;
|
||||||
|
|
||||||
let val = mr_auth::authenticate_finish_flow(query).await?;
|
return if let Some((_, code)) = code {
|
||||||
|
let val = mr_auth::authenticate_finish_flow(&code).await?;
|
||||||
|
|
||||||
return Ok(Some(val));
|
Ok(Some(val))
|
||||||
|
} else {
|
||||||
|
Ok(None)
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
|
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
|
||||||
@ -81,38 +76,6 @@ pub async fn modrinth_auth_login(
|
|||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
pub async fn login_pass(
|
|
||||||
username: &str,
|
|
||||||
password: &str,
|
|
||||||
challenge: &str,
|
|
||||||
) -> Result<ModrinthCredentialsResult> {
|
|
||||||
Ok(theseus::mr_auth::login_password(username, password, challenge).await?)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
pub async fn login_2fa(code: &str, flow: &str) -> Result<ModrinthCredentials> {
|
|
||||||
Ok(theseus::mr_auth::login_2fa(code, flow).await?)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
pub async fn create_account(
|
|
||||||
username: &str,
|
|
||||||
email: &str,
|
|
||||||
password: &str,
|
|
||||||
challenge: &str,
|
|
||||||
sign_up_newsletter: bool,
|
|
||||||
) -> Result<ModrinthCredentials> {
|
|
||||||
Ok(theseus::mr_auth::create_account(
|
|
||||||
username,
|
|
||||||
email,
|
|
||||||
password,
|
|
||||||
challenge,
|
|
||||||
sign_up_newsletter,
|
|
||||||
)
|
|
||||||
.await?)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn logout() -> Result<()> {
|
pub async fn logout() -> Result<()> {
|
||||||
Ok(theseus::mr_auth::logout().await?)
|
Ok(theseus::mr_auth::logout().await?)
|
||||||
|
|||||||
@ -264,7 +264,6 @@ fn main() {
|
|||||||
initialize_state,
|
initialize_state,
|
||||||
is_dev,
|
is_dev,
|
||||||
toggle_decorations,
|
toggle_decorations,
|
||||||
api::mr_auth::modrinth_auth_login,
|
|
||||||
show_window,
|
show_window,
|
||||||
restart_app,
|
restart_app,
|
||||||
]);
|
]);
|
||||||
|
|||||||
@ -91,14 +91,14 @@
|
|||||||
"capabilities": ["ads", "core", "plugins"],
|
"capabilities": ["ads", "core", "plugins"],
|
||||||
"csp": {
|
"csp": {
|
||||||
"default-src": "'self' customprotocol: asset:",
|
"default-src": "'self' customprotocol: asset:",
|
||||||
"connect-src": "ipc: http://ipc.localhost https://modrinth.com https://*.modrinth.com https://*.posthog.com https://*.sentry.io https://*.cloudflare.com https://api.mclo.gs https://cmp.inmobi.com",
|
"connect-src": "ipc: http://ipc.localhost https://modrinth.com https://*.modrinth.com https://*.posthog.com https://*.sentry.io https://api.mclo.gs",
|
||||||
"font-src": [
|
"font-src": [
|
||||||
"https://cdn-raw.modrinth.com/fonts/inter/"
|
"https://cdn-raw.modrinth.com/fonts/inter/"
|
||||||
],
|
],
|
||||||
"img-src": "https: 'unsafe-inline' 'self' asset: http://asset.localhost blob: data:",
|
"img-src": "https: 'unsafe-inline' 'self' asset: http://asset.localhost blob: data:",
|
||||||
"style-src": "'unsafe-inline' 'self'",
|
"style-src": "'unsafe-inline' 'self'",
|
||||||
"script-src": "https://cmp.inmobi.com https://*.cloudflare.com https://*.posthog.com 'self'",
|
"script-src": "https://*.posthog.com 'self'",
|
||||||
"frame-src": "https://*.cloudflare.com https://www.youtube.com https://www.youtube-nocookie.com https://discord.com 'self'"
|
"frame-src": "https://www.youtube.com https://www.youtube-nocookie.com https://discord.com 'self'"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -111,7 +111,13 @@ export const getAuthUrl = (provider, redirect = "") => {
|
|||||||
if (redirect === "") {
|
if (redirect === "") {
|
||||||
redirect = route.path;
|
redirect = route.path;
|
||||||
}
|
}
|
||||||
const fullURL = `${config.public.siteUrl}${redirect}`;
|
|
||||||
|
let fullURL;
|
||||||
|
if (route.query.launcher) {
|
||||||
|
fullURL = `https://launcher-files.modrinth.com`;
|
||||||
|
} else {
|
||||||
|
fullURL = `${config.public.siteUrl}${redirect}`;
|
||||||
|
}
|
||||||
|
|
||||||
return `${config.public.apiBaseUrl}auth/init?provider=${provider}&url=${fullURL}`;
|
return `${config.public.apiBaseUrl}auth/init?provider=${provider}&url=${fullURL}`;
|
||||||
};
|
};
|
||||||
|
|||||||
1
apps/frontend/src/layouts/empty.vue
Normal file
1
apps/frontend/src/layouts/empty.vue
Normal file
@ -0,0 +1 @@
|
|||||||
|
<template><slot id="main" /></template>
|
||||||
5
apps/frontend/src/middleware/launcher-auth.ts
Normal file
5
apps/frontend/src/middleware/launcher-auth.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export default defineNuxtRouteMiddleware((to) => {
|
||||||
|
if (to.query.launcher) {
|
||||||
|
setPageLayout("empty");
|
||||||
|
}
|
||||||
|
});
|
||||||
@ -1,3 +1,8 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
definePageMeta({
|
||||||
|
middleware: ["launcher-auth"],
|
||||||
|
});
|
||||||
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<NuxtPage class="auth-container universal-card" />
|
<NuxtPage class="auth-container universal-card" />
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@ -94,12 +94,24 @@
|
|||||||
<div class="auth-form__additional-options">
|
<div class="auth-form__additional-options">
|
||||||
<IntlFormatted :message-id="messages.additionalOptionsLabel">
|
<IntlFormatted :message-id="messages.additionalOptionsLabel">
|
||||||
<template #forgot-password-link="{ children }">
|
<template #forgot-password-link="{ children }">
|
||||||
<NuxtLink class="text-link" to="/auth/reset-password">
|
<NuxtLink
|
||||||
|
class="text-link"
|
||||||
|
:to="{
|
||||||
|
path: '/auth/reset-password',
|
||||||
|
query: route.query,
|
||||||
|
}"
|
||||||
|
>
|
||||||
<component :is="() => children" />
|
<component :is="() => children" />
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
</template>
|
</template>
|
||||||
<template #create-account-link="{ children }">
|
<template #create-account-link="{ children }">
|
||||||
<NuxtLink class="text-link" :to="signUpLink">
|
<NuxtLink
|
||||||
|
class="text-link"
|
||||||
|
:to="{
|
||||||
|
path: '/auth/sign-up',
|
||||||
|
query: route.query,
|
||||||
|
}"
|
||||||
|
>
|
||||||
<component :is="() => children" />
|
<component :is="() => children" />
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
</template>
|
</template>
|
||||||
@ -193,10 +205,6 @@ const token = ref("");
|
|||||||
|
|
||||||
const flow = ref(route.query.flow);
|
const flow = ref(route.query.flow);
|
||||||
|
|
||||||
const signUpLink = computed(
|
|
||||||
() => `/auth/sign-up${route.query.redirect ? `?redirect=${route.query.redirect}` : ""}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
async function beginPasswordSignIn() {
|
async function beginPasswordSignIn() {
|
||||||
startLoading();
|
startLoading();
|
||||||
try {
|
try {
|
||||||
@ -252,6 +260,11 @@ async function begin2FASignIn() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function finishSignIn(token) {
|
async function finishSignIn(token) {
|
||||||
|
if (route.query.launcher) {
|
||||||
|
await navigateTo(`https://launcher-files.modrinth.com/?code=${token}`, { external: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (token) {
|
if (token) {
|
||||||
await useAuth(token);
|
await useAuth(token);
|
||||||
await useUser();
|
await useUser();
|
||||||
|
|||||||
@ -91,7 +91,7 @@
|
|||||||
:description="formatMessage(messages.subscribeLabel)"
|
:description="formatMessage(messages.subscribeLabel)"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<p>
|
<p v-if="!route.query.launcher">
|
||||||
<IntlFormatted :message-id="messages.legalDisclaimer">
|
<IntlFormatted :message-id="messages.legalDisclaimer">
|
||||||
<template #terms-link="{ children }">
|
<template #terms-link="{ children }">
|
||||||
<NuxtLink to="/legal/terms" class="text-link">
|
<NuxtLink to="/legal/terms" class="text-link">
|
||||||
@ -118,7 +118,13 @@
|
|||||||
|
|
||||||
<div class="auth-form__additional-options">
|
<div class="auth-form__additional-options">
|
||||||
{{ formatMessage(messages.alreadyHaveAccountLabel) }}
|
{{ formatMessage(messages.alreadyHaveAccountLabel) }}
|
||||||
<NuxtLink class="text-link" :to="signInLink">
|
<NuxtLink
|
||||||
|
class="text-link"
|
||||||
|
:to="{
|
||||||
|
path: '/auth/sign-in',
|
||||||
|
query: route.query,
|
||||||
|
}"
|
||||||
|
>
|
||||||
{{ formatMessage(commonMessages.signInButton) }}
|
{{ formatMessage(commonMessages.signInButton) }}
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
</div>
|
</div>
|
||||||
@ -214,10 +220,6 @@ const confirmPassword = ref("");
|
|||||||
const token = ref("");
|
const token = ref("");
|
||||||
const subscribe = ref(true);
|
const subscribe = ref(true);
|
||||||
|
|
||||||
const signInLink = computed(
|
|
||||||
() => `/auth/sign-in${route.query.redirect ? `?redirect=${route.query.redirect}` : ""}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
async function createAccount() {
|
async function createAccount() {
|
||||||
startLoading();
|
startLoading();
|
||||||
try {
|
try {
|
||||||
@ -245,6 +247,13 @@ async function createAccount() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (route.query.launcher) {
|
||||||
|
await navigateTo(`https://launcher-files.modrinth.com/?code=${res.session}`, {
|
||||||
|
external: true,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
await useAuth(res.session);
|
await useAuth(res.session);
|
||||||
await useUser();
|
await useUser();
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"db_name": "PostgreSQL",
|
"db_name": "PostgreSQL",
|
||||||
"query": "\n INSERT INTO users (\n id, username, email,\n avatar_url, raw_avatar_url, bio, created,\n github_id, discord_id, gitlab_id, google_id, steam_id, microsoft_id,\n email_verified, password, paypal_id, paypal_country, paypal_email,\n venmo_handle, stripe_customer_id\n )\n VALUES (\n $1, $2, $3, $4, $5,\n $6, $7,\n $8, $9, $10, $11, $12, $13,\n $14, $15, $16, $17, $18, $19, $20\n )\n ",
|
"query": "\n INSERT INTO users (\n id, username, email,\n avatar_url, raw_avatar_url, bio, created,\n github_id, discord_id, gitlab_id, google_id, steam_id, microsoft_id,\n email_verified, password, paypal_id, paypal_country, paypal_email,\n venmo_handle, stripe_customer_id, allow_friend_requests\n )\n VALUES (\n $1, $2, $3, $4, $5,\n $6, $7,\n $8, $9, $10, $11, $12, $13,\n $14, $15, $16, $17, $18, $19, $20, $21\n )\n ",
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [],
|
"columns": [],
|
||||||
"parameters": {
|
"parameters": {
|
||||||
@ -24,10 +24,11 @@
|
|||||||
"Text",
|
"Text",
|
||||||
"Text",
|
"Text",
|
||||||
"Text",
|
"Text",
|
||||||
"Text"
|
"Text",
|
||||||
|
"Bool"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"nullable": []
|
"nullable": []
|
||||||
},
|
},
|
||||||
"hash": "1d09169d25a30f4495778b0695ac00e48d682db15963a01195bbd5f981178ce9"
|
"hash": "32fa1030a3a69f6bc36c6ec916ba8d0724dfd683576629ab05f5df321d5f9a55"
|
||||||
}
|
}
|
||||||
14
apps/labrinth/.sqlx/query-61742fef80cb016f7c88985fe8170b27ff356dce5933490630491d385c72b365.json
generated
Normal file
14
apps/labrinth/.sqlx/query-61742fef80cb016f7c88985fe8170b27ff356dce5933490630491d385c72b365.json
generated
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"db_name": "PostgreSQL",
|
||||||
|
"query": "\n DELETE FROM friends\n WHERE user_id = $1 OR friend_id = $1\n ",
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"parameters": {
|
||||||
|
"Left": [
|
||||||
|
"Int8"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"nullable": []
|
||||||
|
},
|
||||||
|
"hash": "61742fef80cb016f7c88985fe8170b27ff356dce5933490630491d385c72b365"
|
||||||
|
}
|
||||||
15
apps/labrinth/.sqlx/query-66bdfd161a289694ab3245bbf079b29e7bef5723f28bb523b7b88a9b6a5feb4c.json
generated
Normal file
15
apps/labrinth/.sqlx/query-66bdfd161a289694ab3245bbf079b29e7bef5723f28bb523b7b88a9b6a5feb4c.json
generated
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"db_name": "PostgreSQL",
|
||||||
|
"query": "\n DELETE FROM friends\n WHERE (user_id = $1 AND friend_id = $2) OR (user_id = $2 AND friend_id = $1)\n ",
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"parameters": {
|
||||||
|
"Left": [
|
||||||
|
"Int8",
|
||||||
|
"Int8"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"nullable": []
|
||||||
|
},
|
||||||
|
"hash": "66bdfd161a289694ab3245bbf079b29e7bef5723f28bb523b7b88a9b6a5feb4c"
|
||||||
|
}
|
||||||
17
apps/labrinth/.sqlx/query-a8733e3dc014df728f785ef25e7b20d6d7d96bacc9e9824fe95f1abc8340d463.json
generated
Normal file
17
apps/labrinth/.sqlx/query-a8733e3dc014df728f785ef25e7b20d6d7d96bacc9e9824fe95f1abc8340d463.json
generated
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"db_name": "PostgreSQL",
|
||||||
|
"query": "\n INSERT INTO friends (user_id, friend_id, created, accepted)\n VALUES ($1, $2, $3, $4)\n ",
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"parameters": {
|
||||||
|
"Left": [
|
||||||
|
"Int8",
|
||||||
|
"Int8",
|
||||||
|
"Timestamptz",
|
||||||
|
"Bool"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"nullable": []
|
||||||
|
},
|
||||||
|
"hash": "a8733e3dc014df728f785ef25e7b20d6d7d96bacc9e9824fe95f1abc8340d463"
|
||||||
|
}
|
||||||
15
apps/labrinth/.sqlx/query-aee305585877a5733c06fc67147df275135a1b2f265bb46216854526e77863c2.json
generated
Normal file
15
apps/labrinth/.sqlx/query-aee305585877a5733c06fc67147df275135a1b2f265bb46216854526e77863c2.json
generated
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"db_name": "PostgreSQL",
|
||||||
|
"query": "\n UPDATE users\n SET allow_friend_requests = $1\n WHERE (id = $2)\n ",
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"parameters": {
|
||||||
|
"Left": [
|
||||||
|
"Bool",
|
||||||
|
"Int8"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"nullable": []
|
||||||
|
},
|
||||||
|
"hash": "aee305585877a5733c06fc67147df275135a1b2f265bb46216854526e77863c2"
|
||||||
|
}
|
||||||
40
apps/labrinth/.sqlx/query-b2a0e2820b4f464b16918613efc043ea52381e27f651f6eae38bc64768f27d5b.json
generated
Normal file
40
apps/labrinth/.sqlx/query-b2a0e2820b4f464b16918613efc043ea52381e27f651f6eae38bc64768f27d5b.json
generated
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
{
|
||||||
|
"db_name": "PostgreSQL",
|
||||||
|
"query": "\n SELECT f.user_id, f.friend_id, f.created, f.accepted\n FROM friends f\n WHERE f.user_id = $1 OR f.friend_id = $1\n ",
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"ordinal": 0,
|
||||||
|
"name": "user_id",
|
||||||
|
"type_info": "Int8"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 1,
|
||||||
|
"name": "friend_id",
|
||||||
|
"type_info": "Int8"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 2,
|
||||||
|
"name": "created",
|
||||||
|
"type_info": "Timestamptz"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 3,
|
||||||
|
"name": "accepted",
|
||||||
|
"type_info": "Bool"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Left": [
|
||||||
|
"Int8"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hash": "b2a0e2820b4f464b16918613efc043ea52381e27f651f6eae38bc64768f27d5b"
|
||||||
|
}
|
||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"db_name": "PostgreSQL",
|
"db_name": "PostgreSQL",
|
||||||
"query": "\n SELECT id, email,\n avatar_url, raw_avatar_url, username, bio,\n created, role, badges,\n github_id, discord_id, gitlab_id, google_id, steam_id, microsoft_id,\n email_verified, password, totp_secret, paypal_id, paypal_country, paypal_email,\n venmo_handle, stripe_customer_id\n FROM users\n WHERE id = ANY($1) OR LOWER(username) = ANY($2)\n ",
|
"query": "\n SELECT id, email,\n avatar_url, raw_avatar_url, username, bio,\n created, role, badges,\n github_id, discord_id, gitlab_id, google_id, steam_id, microsoft_id,\n email_verified, password, totp_secret, paypal_id, paypal_country, paypal_email,\n venmo_handle, stripe_customer_id, allow_friend_requests\n FROM users\n WHERE id = ANY($1) OR LOWER(username) = ANY($2)\n ",
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [
|
"columns": [
|
||||||
{
|
{
|
||||||
@ -117,6 +117,11 @@
|
|||||||
"ordinal": 22,
|
"ordinal": 22,
|
||||||
"name": "stripe_customer_id",
|
"name": "stripe_customer_id",
|
||||||
"type_info": "Text"
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 23,
|
||||||
|
"name": "allow_friend_requests",
|
||||||
|
"type_info": "Bool"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"parameters": {
|
"parameters": {
|
||||||
@ -148,8 +153,9 @@
|
|||||||
true,
|
true,
|
||||||
true,
|
true,
|
||||||
true,
|
true,
|
||||||
true
|
true,
|
||||||
|
false
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"hash": "5cce25ecda748f570de563bd3b312075dd09094b44d2aea2910011eb56778ee0"
|
"hash": "b5857aafa522ca62294bc296fb6cddd862eca2889203e43b4962df07ba1221a0"
|
||||||
}
|
}
|
||||||
16
apps/labrinth/.sqlx/query-c6214ac083ee2ff73e437a55112f5669b086b7a66959033847dac6d30a4cf445.json
generated
Normal file
16
apps/labrinth/.sqlx/query-c6214ac083ee2ff73e437a55112f5669b086b7a66959033847dac6d30a4cf445.json
generated
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"db_name": "PostgreSQL",
|
||||||
|
"query": "\n UPDATE friends\n SET accepted = $3\n WHERE (user_id = $1 AND friend_id = $2) OR (user_id = $2 AND friend_id = $1)\n ",
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"parameters": {
|
||||||
|
"Left": [
|
||||||
|
"Int8",
|
||||||
|
"Int8",
|
||||||
|
"Bool"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"nullable": []
|
||||||
|
},
|
||||||
|
"hash": "c6214ac083ee2ff73e437a55112f5669b086b7a66959033847dac6d30a4cf445"
|
||||||
|
}
|
||||||
41
apps/labrinth/.sqlx/query-deef2cd808aab305336fbc8e556da37ca07f64462085382f2fd0eabaefceec50.json
generated
Normal file
41
apps/labrinth/.sqlx/query-deef2cd808aab305336fbc8e556da37ca07f64462085382f2fd0eabaefceec50.json
generated
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
{
|
||||||
|
"db_name": "PostgreSQL",
|
||||||
|
"query": "\n SELECT f.user_id, f.friend_id, f.created, f.accepted\n FROM friends f\n WHERE (f.user_id = $1 AND f.friend_id = $2) OR (f.user_id = $2 AND f.friend_id = $1)\n ",
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"ordinal": 0,
|
||||||
|
"name": "user_id",
|
||||||
|
"type_info": "Int8"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 1,
|
||||||
|
"name": "friend_id",
|
||||||
|
"type_info": "Int8"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 2,
|
||||||
|
"name": "created",
|
||||||
|
"type_info": "Timestamptz"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 3,
|
||||||
|
"name": "accepted",
|
||||||
|
"type_info": "Bool"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Left": [
|
||||||
|
"Int8",
|
||||||
|
"Int8"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hash": "deef2cd808aab305336fbc8e556da37ca07f64462085382f2fd0eabaefceec50"
|
||||||
|
}
|
||||||
@ -15,7 +15,7 @@ actix-web = "4.4.1"
|
|||||||
actix-rt = "2.9.0"
|
actix-rt = "2.9.0"
|
||||||
actix-multipart = "0.6.1"
|
actix-multipart = "0.6.1"
|
||||||
actix-cors = "0.7.0"
|
actix-cors = "0.7.0"
|
||||||
actix-ws = "0.2.5"
|
actix-ws = "0.3.0"
|
||||||
actix-files = "0.6.5"
|
actix-files = "0.6.5"
|
||||||
actix-web-prom = { version = "0.8.0", features = ["process"] }
|
actix-web-prom = { version = "0.8.0", features = ["process"] }
|
||||||
governor = "0.6.3"
|
governor = "0.6.3"
|
||||||
|
|||||||
11
apps/labrinth/migrations/20241121232522_friends.sql
Normal file
11
apps/labrinth/migrations/20241121232522_friends.sql
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
CREATE TABLE friends (
|
||||||
|
user_id BIGINT NOT NULL REFERENCES users(id),
|
||||||
|
friend_id BIGINT NOT NULL REFERENCES users(id),
|
||||||
|
created TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
accepted BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
|
|
||||||
|
PRIMARY KEY (user_id, friend_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
ALTER TABLE users
|
||||||
|
ADD COLUMN allow_friend_requests BOOLEAN NOT NULL DEFAULT TRUE;
|
||||||
@ -18,7 +18,7 @@ const FLOWS_NAMESPACE: &str = "flows";
|
|||||||
pub enum Flow {
|
pub enum Flow {
|
||||||
OAuth {
|
OAuth {
|
||||||
user_id: Option<UserId>,
|
user_id: Option<UserId>,
|
||||||
url: Option<String>,
|
url: String,
|
||||||
provider: AuthProvider,
|
provider: AuthProvider,
|
||||||
},
|
},
|
||||||
Login2FA {
|
Login2FA {
|
||||||
|
|||||||
132
apps/labrinth/src/database/models/friend_item.rs
Normal file
132
apps/labrinth/src/database/models/friend_item.rs
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
use crate::database::models::UserId;
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
|
||||||
|
pub struct FriendItem {
|
||||||
|
pub user_id: UserId,
|
||||||
|
pub friend_id: UserId,
|
||||||
|
pub created: DateTime<Utc>,
|
||||||
|
pub accepted: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FriendItem {
|
||||||
|
pub async fn insert(
|
||||||
|
&self,
|
||||||
|
transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
|
||||||
|
) -> Result<(), sqlx::Error> {
|
||||||
|
sqlx::query!(
|
||||||
|
"
|
||||||
|
INSERT INTO friends (user_id, friend_id, created, accepted)
|
||||||
|
VALUES ($1, $2, $3, $4)
|
||||||
|
",
|
||||||
|
self.user_id.0,
|
||||||
|
self.friend_id.0,
|
||||||
|
self.created,
|
||||||
|
self.accepted,
|
||||||
|
)
|
||||||
|
.execute(&mut **transaction)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_friend<'a, E>(
|
||||||
|
user_id: UserId,
|
||||||
|
friend_id: UserId,
|
||||||
|
exec: E,
|
||||||
|
) -> Result<Option<FriendItem>, sqlx::Error>
|
||||||
|
where
|
||||||
|
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
|
||||||
|
{
|
||||||
|
let friend = sqlx::query!(
|
||||||
|
"
|
||||||
|
SELECT f.user_id, f.friend_id, f.created, f.accepted
|
||||||
|
FROM friends f
|
||||||
|
WHERE (f.user_id = $1 AND f.friend_id = $2) OR (f.user_id = $2 AND f.friend_id = $1)
|
||||||
|
",
|
||||||
|
user_id.0,
|
||||||
|
friend_id.0,
|
||||||
|
)
|
||||||
|
.fetch_optional(exec)
|
||||||
|
.await?
|
||||||
|
.map(|row| FriendItem {
|
||||||
|
user_id: UserId(row.user_id),
|
||||||
|
friend_id: UserId(row.friend_id),
|
||||||
|
created: row.created,
|
||||||
|
accepted: row.accepted,
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(friend)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn update_friend(
|
||||||
|
user_id: UserId,
|
||||||
|
friend_id: UserId,
|
||||||
|
accepted: bool,
|
||||||
|
transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
|
||||||
|
) -> Result<(), sqlx::Error> {
|
||||||
|
sqlx::query!(
|
||||||
|
"
|
||||||
|
UPDATE friends
|
||||||
|
SET accepted = $3
|
||||||
|
WHERE (user_id = $1 AND friend_id = $2) OR (user_id = $2 AND friend_id = $1)
|
||||||
|
",
|
||||||
|
user_id.0,
|
||||||
|
friend_id.0,
|
||||||
|
accepted,
|
||||||
|
)
|
||||||
|
.execute(&mut **transaction)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_user_friends<'a, E>(
|
||||||
|
user_id: UserId,
|
||||||
|
accepted: Option<bool>,
|
||||||
|
exec: E,
|
||||||
|
) -> Result<Vec<FriendItem>, sqlx::Error>
|
||||||
|
where
|
||||||
|
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
|
||||||
|
{
|
||||||
|
let friends = sqlx::query!(
|
||||||
|
"
|
||||||
|
SELECT f.user_id, f.friend_id, f.created, f.accepted
|
||||||
|
FROM friends f
|
||||||
|
WHERE f.user_id = $1 OR f.friend_id = $1
|
||||||
|
",
|
||||||
|
user_id.0,
|
||||||
|
)
|
||||||
|
.fetch_all(exec)
|
||||||
|
.await?
|
||||||
|
.into_iter()
|
||||||
|
.map(|row| FriendItem {
|
||||||
|
user_id: UserId(row.user_id),
|
||||||
|
friend_id: UserId(row.friend_id),
|
||||||
|
created: row.created,
|
||||||
|
accepted: row.accepted,
|
||||||
|
})
|
||||||
|
.filter(|x| accepted.map(|y| y == x.accepted).unwrap_or(true))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
Ok(friends)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn remove(
|
||||||
|
user_id: UserId,
|
||||||
|
friend_id: UserId,
|
||||||
|
transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
|
||||||
|
) -> Result<(), sqlx::Error> {
|
||||||
|
sqlx::query!(
|
||||||
|
"
|
||||||
|
DELETE FROM friends
|
||||||
|
WHERE (user_id = $1 AND friend_id = $2) OR (user_id = $2 AND friend_id = $1)
|
||||||
|
",
|
||||||
|
user_id.0 as i64,
|
||||||
|
friend_id.0 as i64,
|
||||||
|
)
|
||||||
|
.execute(&mut **transaction)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -4,6 +4,7 @@ pub mod categories;
|
|||||||
pub mod charge_item;
|
pub mod charge_item;
|
||||||
pub mod collection_item;
|
pub mod collection_item;
|
||||||
pub mod flow_item;
|
pub mod flow_item;
|
||||||
|
pub mod friend_item;
|
||||||
pub mod ids;
|
pub mod ids;
|
||||||
pub mod image_item;
|
pub mod image_item;
|
||||||
pub mod legacy_loader_fields;
|
pub mod legacy_loader_fields;
|
||||||
|
|||||||
@ -44,6 +44,8 @@ pub struct User {
|
|||||||
pub created: DateTime<Utc>,
|
pub created: DateTime<Utc>,
|
||||||
pub role: String,
|
pub role: String,
|
||||||
pub badges: Badges,
|
pub badges: Badges,
|
||||||
|
|
||||||
|
pub allow_friend_requests: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl User {
|
impl User {
|
||||||
@ -58,13 +60,13 @@ impl User {
|
|||||||
avatar_url, raw_avatar_url, bio, created,
|
avatar_url, raw_avatar_url, bio, created,
|
||||||
github_id, discord_id, gitlab_id, google_id, steam_id, microsoft_id,
|
github_id, discord_id, gitlab_id, google_id, steam_id, microsoft_id,
|
||||||
email_verified, password, paypal_id, paypal_country, paypal_email,
|
email_verified, password, paypal_id, paypal_country, paypal_email,
|
||||||
venmo_handle, stripe_customer_id
|
venmo_handle, stripe_customer_id, allow_friend_requests
|
||||||
)
|
)
|
||||||
VALUES (
|
VALUES (
|
||||||
$1, $2, $3, $4, $5,
|
$1, $2, $3, $4, $5,
|
||||||
$6, $7,
|
$6, $7,
|
||||||
$8, $9, $10, $11, $12, $13,
|
$8, $9, $10, $11, $12, $13,
|
||||||
$14, $15, $16, $17, $18, $19, $20
|
$14, $15, $16, $17, $18, $19, $20, $21
|
||||||
)
|
)
|
||||||
",
|
",
|
||||||
self.id as UserId,
|
self.id as UserId,
|
||||||
@ -86,7 +88,8 @@ impl User {
|
|||||||
self.paypal_country,
|
self.paypal_country,
|
||||||
self.paypal_email,
|
self.paypal_email,
|
||||||
self.venmo_handle,
|
self.venmo_handle,
|
||||||
self.stripe_customer_id
|
self.stripe_customer_id,
|
||||||
|
self.allow_friend_requests,
|
||||||
)
|
)
|
||||||
.execute(&mut **transaction)
|
.execute(&mut **transaction)
|
||||||
.await?;
|
.await?;
|
||||||
@ -172,7 +175,7 @@ impl User {
|
|||||||
created, role, badges,
|
created, role, badges,
|
||||||
github_id, discord_id, gitlab_id, google_id, steam_id, microsoft_id,
|
github_id, discord_id, gitlab_id, google_id, steam_id, microsoft_id,
|
||||||
email_verified, password, totp_secret, paypal_id, paypal_country, paypal_email,
|
email_verified, password, totp_secret, paypal_id, paypal_country, paypal_email,
|
||||||
venmo_handle, stripe_customer_id
|
venmo_handle, stripe_customer_id, allow_friend_requests
|
||||||
FROM users
|
FROM users
|
||||||
WHERE id = ANY($1) OR LOWER(username) = ANY($2)
|
WHERE id = ANY($1) OR LOWER(username) = ANY($2)
|
||||||
",
|
",
|
||||||
@ -205,6 +208,7 @@ impl User {
|
|||||||
venmo_handle: u.venmo_handle,
|
venmo_handle: u.venmo_handle,
|
||||||
stripe_customer_id: u.stripe_customer_id,
|
stripe_customer_id: u.stripe_customer_id,
|
||||||
totp_secret: u.totp_secret,
|
totp_secret: u.totp_secret,
|
||||||
|
allow_friend_requests: u.allow_friend_requests,
|
||||||
};
|
};
|
||||||
|
|
||||||
acc.insert(u.id, (Some(u.username), user));
|
acc.insert(u.id, (Some(u.username), user));
|
||||||
@ -643,6 +647,16 @@ impl User {
|
|||||||
.execute(&mut **transaction)
|
.execute(&mut **transaction)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
sqlx::query!(
|
||||||
|
"
|
||||||
|
DELETE FROM friends
|
||||||
|
WHERE user_id = $1 OR friend_id = $1
|
||||||
|
",
|
||||||
|
id as UserId,
|
||||||
|
)
|
||||||
|
.execute(&mut **transaction)
|
||||||
|
.await?;
|
||||||
|
|
||||||
sqlx::query!(
|
sqlx::query!(
|
||||||
"
|
"
|
||||||
DELETE FROM user_backup_codes
|
DELETE FROM user_backup_codes
|
||||||
|
|||||||
@ -547,6 +547,23 @@ impl RedisConnection {
|
|||||||
Ok(res)
|
Ok(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn get_many(
|
||||||
|
&mut self,
|
||||||
|
namespace: &str,
|
||||||
|
ids: &[String],
|
||||||
|
) -> Result<Vec<Option<String>>, DatabaseError> {
|
||||||
|
let mut cmd = cmd("MGET");
|
||||||
|
redis_args(
|
||||||
|
&mut cmd,
|
||||||
|
ids.iter()
|
||||||
|
.map(|x| format!("{}_{}:{}", self.meta_namespace, namespace, x))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.as_slice(),
|
||||||
|
);
|
||||||
|
let res = redis_execute(&mut cmd, &mut self.connection).await?;
|
||||||
|
Ok(res)
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn get_deserialized_from_json<R>(
|
pub async fn get_deserialized_from_json<R>(
|
||||||
&mut self,
|
&mut self,
|
||||||
namespace: &str,
|
namespace: &str,
|
||||||
@ -561,6 +578,22 @@ impl RedisConnection {
|
|||||||
.and_then(|x| serde_json::from_str(&x).ok()))
|
.and_then(|x| serde_json::from_str(&x).ok()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn get_many_deserialized_from_json<R>(
|
||||||
|
&mut self,
|
||||||
|
namespace: &str,
|
||||||
|
ids: &[String],
|
||||||
|
) -> Result<Vec<Option<R>>, DatabaseError>
|
||||||
|
where
|
||||||
|
R: for<'a> serde::Deserialize<'a>,
|
||||||
|
{
|
||||||
|
Ok(self
|
||||||
|
.get_many(namespace, ids)
|
||||||
|
.await?
|
||||||
|
.into_iter()
|
||||||
|
.map(|x| x.and_then(|val| serde_json::from_str::<R>(&val).ok()))
|
||||||
|
.collect::<Vec<_>>())
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn delete<T1>(
|
pub async fn delete<T1>(
|
||||||
&mut self,
|
&mut self,
|
||||||
namespace: &str,
|
namespace: &str,
|
||||||
|
|||||||
@ -10,7 +10,6 @@ use queue::{
|
|||||||
socket::ActiveSockets,
|
socket::ActiveSockets,
|
||||||
};
|
};
|
||||||
use sqlx::Postgres;
|
use sqlx::Postgres;
|
||||||
use tokio::sync::RwLock;
|
|
||||||
|
|
||||||
extern crate clickhouse as clickhouse_crate;
|
extern crate clickhouse as clickhouse_crate;
|
||||||
use clickhouse_crate::Client;
|
use clickhouse_crate::Client;
|
||||||
@ -56,7 +55,7 @@ pub struct LabrinthConfig {
|
|||||||
pub session_queue: web::Data<AuthQueue>,
|
pub session_queue: web::Data<AuthQueue>,
|
||||||
pub payouts_queue: web::Data<PayoutsQueue>,
|
pub payouts_queue: web::Data<PayoutsQueue>,
|
||||||
pub analytics_queue: Arc<AnalyticsQueue>,
|
pub analytics_queue: Arc<AnalyticsQueue>,
|
||||||
pub active_sockets: web::Data<RwLock<ActiveSockets>>,
|
pub active_sockets: web::Data<ActiveSockets>,
|
||||||
pub automated_moderation_queue: web::Data<AutomatedModerationQueue>,
|
pub automated_moderation_queue: web::Data<AutomatedModerationQueue>,
|
||||||
pub rate_limiter: KeyedRateLimiter,
|
pub rate_limiter: KeyedRateLimiter,
|
||||||
pub stripe_client: stripe::Client,
|
pub stripe_client: stripe::Client,
|
||||||
@ -303,7 +302,7 @@ pub fn app_setup(
|
|||||||
};
|
};
|
||||||
|
|
||||||
let payouts_queue = web::Data::new(PayoutsQueue::new());
|
let payouts_queue = web::Data::new(PayoutsQueue::new());
|
||||||
let active_sockets = web::Data::new(RwLock::new(ActiveSockets::default()));
|
let active_sockets = web::Data::new(ActiveSockets::default());
|
||||||
|
|
||||||
LabrinthConfig {
|
LabrinthConfig {
|
||||||
pool,
|
pool,
|
||||||
|
|||||||
@ -52,6 +52,7 @@ pub struct User {
|
|||||||
pub has_totp: Option<bool>,
|
pub has_totp: Option<bool>,
|
||||||
pub payout_data: Option<UserPayoutData>,
|
pub payout_data: Option<UserPayoutData>,
|
||||||
pub stripe_customer_id: Option<String>,
|
pub stripe_customer_id: Option<String>,
|
||||||
|
pub allow_friend_requests: Option<bool>,
|
||||||
|
|
||||||
// DEPRECATED. Always returns None
|
// DEPRECATED. Always returns None
|
||||||
pub github_id: Option<u64>,
|
pub github_id: Option<u64>,
|
||||||
@ -85,6 +86,7 @@ impl From<DBUser> for User {
|
|||||||
has_totp: None,
|
has_totp: None,
|
||||||
github_id: None,
|
github_id: None,
|
||||||
stripe_customer_id: None,
|
stripe_customer_id: None,
|
||||||
|
allow_friend_requests: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -136,6 +138,7 @@ impl User {
|
|||||||
balance: Decimal::ZERO,
|
balance: Decimal::ZERO,
|
||||||
}),
|
}),
|
||||||
stripe_customer_id: db_user.stripe_customer_id,
|
stripe_customer_id: db_user.stripe_customer_id,
|
||||||
|
allow_friend_requests: Some(db_user.allow_friend_requests),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -185,3 +188,29 @@ impl Role {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub struct UserFriend {
|
||||||
|
pub id: UserId,
|
||||||
|
pub pending: bool,
|
||||||
|
pub created: DateTime<Utc>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UserFriend {
|
||||||
|
pub fn from(
|
||||||
|
data: crate::database::models::friend_item::FriendItem,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
id: data.friend_id.into(),
|
||||||
|
pending: data.accepted,
|
||||||
|
created: data.created,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone)]
|
||||||
|
pub struct UserStatus {
|
||||||
|
pub user_id: UserId,
|
||||||
|
pub profile_name: Option<String>,
|
||||||
|
pub last_update: DateTime<Utc>,
|
||||||
|
}
|
||||||
|
|||||||
@ -1,9 +1,10 @@
|
|||||||
//! "Database" for Hydra
|
//! "Database" for Hydra
|
||||||
|
use crate::models::users::{UserId, UserStatus};
|
||||||
use actix_ws::Session;
|
use actix_ws::Session;
|
||||||
use dashmap::DashMap;
|
use dashmap::DashMap;
|
||||||
|
|
||||||
pub struct ActiveSockets {
|
pub struct ActiveSockets {
|
||||||
pub auth_sockets: DashMap<String, Session>,
|
pub auth_sockets: DashMap<UserId, (UserStatus, Session)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for ActiveSockets {
|
impl Default for ActiveSockets {
|
||||||
|
|||||||
@ -9,7 +9,6 @@ use crate::models::ids::random_base62_rng;
|
|||||||
use crate::models::pats::Scopes;
|
use crate::models::pats::Scopes;
|
||||||
use crate::models::users::{Badges, Role};
|
use crate::models::users::{Badges, Role};
|
||||||
use crate::queue::session::AuthQueue;
|
use crate::queue::session::AuthQueue;
|
||||||
use crate::queue::socket::ActiveSockets;
|
|
||||||
use crate::routes::internal::session::issue_session;
|
use crate::routes::internal::session::issue_session;
|
||||||
use crate::routes::ApiError;
|
use crate::routes::ApiError;
|
||||||
use crate::util::captcha::check_hcaptcha;
|
use crate::util::captcha::check_hcaptcha;
|
||||||
@ -17,9 +16,8 @@ use crate::util::env::parse_strings_from_var;
|
|||||||
use crate::util::ext::get_image_ext;
|
use crate::util::ext::get_image_ext;
|
||||||
use crate::util::img::upload_image_optimized;
|
use crate::util::img::upload_image_optimized;
|
||||||
use crate::util::validate::{validation_errors_to_string, RE_URL_SAFE};
|
use crate::util::validate::{validation_errors_to_string, RE_URL_SAFE};
|
||||||
use actix_web::web::{scope, Data, Payload, Query, ServiceConfig};
|
use actix_web::web::{scope, Data, Query, ServiceConfig};
|
||||||
use actix_web::{delete, get, patch, post, web, HttpRequest, HttpResponse};
|
use actix_web::{delete, get, patch, post, web, HttpRequest, HttpResponse};
|
||||||
use actix_ws::Closed;
|
|
||||||
use argon2::password_hash::SaltString;
|
use argon2::password_hash::SaltString;
|
||||||
use argon2::{Argon2, PasswordHash, PasswordHasher, PasswordVerifier};
|
use argon2::{Argon2, PasswordHash, PasswordHasher, PasswordVerifier};
|
||||||
use base64::Engine;
|
use base64::Engine;
|
||||||
@ -32,13 +30,11 @@ use sqlx::postgres::PgPool;
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tokio::sync::RwLock;
|
|
||||||
use validator::Validate;
|
use validator::Validate;
|
||||||
|
|
||||||
pub fn config(cfg: &mut ServiceConfig) {
|
pub fn config(cfg: &mut ServiceConfig) {
|
||||||
cfg.service(
|
cfg.service(
|
||||||
scope("auth")
|
scope("auth")
|
||||||
.service(ws_init)
|
|
||||||
.service(init)
|
.service(init)
|
||||||
.service(auth_callback)
|
.service(auth_callback)
|
||||||
.service(delete_auth_provider)
|
.service(delete_auth_provider)
|
||||||
@ -233,6 +229,7 @@ impl TempUser {
|
|||||||
created: Utc::now(),
|
created: Utc::now(),
|
||||||
role: Role::Developer.to_string(),
|
role: Role::Developer.to_string(),
|
||||||
badges: Badges::default(),
|
badges: Badges::default(),
|
||||||
|
allow_friend_requests: true,
|
||||||
}
|
}
|
||||||
.insert(transaction)
|
.insert(transaction)
|
||||||
.await?;
|
.await?;
|
||||||
@ -1090,7 +1087,7 @@ pub async fn init(
|
|||||||
|
|
||||||
let state = Flow::OAuth {
|
let state = Flow::OAuth {
|
||||||
user_id,
|
user_id,
|
||||||
url: Some(info.url),
|
url: info.url,
|
||||||
provider: info.provider,
|
provider: info.provider,
|
||||||
}
|
}
|
||||||
.insert(Duration::minutes(30), &redis)
|
.insert(Duration::minutes(30), &redis)
|
||||||
@ -1102,59 +1099,10 @@ pub async fn init(
|
|||||||
.json(serde_json::json!({ "url": url })))
|
.json(serde_json::json!({ "url": url })))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
|
||||||
pub struct WsInit {
|
|
||||||
pub provider: AuthProvider,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[get("ws")]
|
|
||||||
pub async fn ws_init(
|
|
||||||
req: HttpRequest,
|
|
||||||
Query(info): Query<WsInit>,
|
|
||||||
body: Payload,
|
|
||||||
db: Data<RwLock<ActiveSockets>>,
|
|
||||||
redis: Data<RedisPool>,
|
|
||||||
) -> Result<HttpResponse, actix_web::Error> {
|
|
||||||
let (res, session, _msg_stream) = actix_ws::handle(&req, body)?;
|
|
||||||
|
|
||||||
async fn sock(
|
|
||||||
mut ws_stream: actix_ws::Session,
|
|
||||||
info: WsInit,
|
|
||||||
db: Data<RwLock<ActiveSockets>>,
|
|
||||||
redis: Data<RedisPool>,
|
|
||||||
) -> Result<(), Closed> {
|
|
||||||
let flow = Flow::OAuth {
|
|
||||||
user_id: None,
|
|
||||||
url: None,
|
|
||||||
provider: info.provider,
|
|
||||||
}
|
|
||||||
.insert(Duration::minutes(30), &redis)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
if let Ok(state) = flow {
|
|
||||||
if let Ok(url) = info.provider.get_redirect_url(state.clone()) {
|
|
||||||
ws_stream
|
|
||||||
.text(serde_json::json!({ "url": url }).to_string())
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let db = db.write().await;
|
|
||||||
db.auth_sockets.insert(state, ws_stream);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
let _ = sock(session, info, db, redis).await;
|
|
||||||
|
|
||||||
Ok(res)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[get("callback")]
|
#[get("callback")]
|
||||||
pub async fn auth_callback(
|
pub async fn auth_callback(
|
||||||
req: HttpRequest,
|
req: HttpRequest,
|
||||||
Query(query): Query<HashMap<String, String>>,
|
Query(query): Query<HashMap<String, String>>,
|
||||||
active_sockets: Data<RwLock<ActiveSockets>>,
|
|
||||||
client: Data<PgPool>,
|
client: Data<PgPool>,
|
||||||
file_host: Data<Arc<dyn FileHost + Send + Sync>>,
|
file_host: Data<Arc<dyn FileHost + Send + Sync>>,
|
||||||
redis: Data<RedisPool>,
|
redis: Data<RedisPool>,
|
||||||
@ -1164,10 +1112,8 @@ pub async fn auth_callback(
|
|||||||
.ok_or_else(|| AuthenticationError::InvalidCredentials)?
|
.ok_or_else(|| AuthenticationError::InvalidCredentials)?
|
||||||
.clone();
|
.clone();
|
||||||
|
|
||||||
let sockets = active_sockets.clone();
|
|
||||||
let state = state_string.clone();
|
let state = state_string.clone();
|
||||||
let res: Result<HttpResponse, AuthenticationError> = async move {
|
let res: Result<HttpResponse, AuthenticationError> = async move {
|
||||||
|
|
||||||
let flow = Flow::get(&state, &redis).await?;
|
let flow = Flow::get(&state, &redis).await?;
|
||||||
|
|
||||||
// Extract cookie header from request
|
// Extract cookie header from request
|
||||||
@ -1223,13 +1169,9 @@ pub async fn auth_callback(
|
|||||||
transaction.commit().await?;
|
transaction.commit().await?;
|
||||||
crate::database::models::User::clear_caches(&[(id, None)], &redis).await?;
|
crate::database::models::User::clear_caches(&[(id, None)], &redis).await?;
|
||||||
|
|
||||||
if let Some(url) = url {
|
Ok(HttpResponse::TemporaryRedirect()
|
||||||
Ok(HttpResponse::TemporaryRedirect()
|
.append_header(("Location", &*url))
|
||||||
.append_header(("Location", &*url))
|
.json(serde_json::json!({ "url": url })))
|
||||||
.json(serde_json::json!({ "url": url })))
|
|
||||||
} else {
|
|
||||||
Err(AuthenticationError::InvalidCredentials)
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
let user_id = if let Some(user_id) = user_id_opt {
|
let user_id = if let Some(user_id) = user_id_opt {
|
||||||
let user = crate::database::models::User::get_id(user_id, &**client, &redis)
|
let user = crate::database::models::User::get_id(user_id, &**client, &redis)
|
||||||
@ -1241,45 +1183,16 @@ pub async fn auth_callback(
|
|||||||
.insert(Duration::minutes(30), &redis)
|
.insert(Duration::minutes(30), &redis)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
if let Some(url) = url {
|
let redirect_url = format!(
|
||||||
let redirect_url = format!(
|
"{}{}error=2fa_required&flow={}",
|
||||||
"{}{}error=2fa_required&flow={}",
|
url,
|
||||||
url,
|
if url.contains('?') { "&" } else { "?" },
|
||||||
if url.contains('?') { "&" } else { "?" },
|
flow
|
||||||
flow
|
);
|
||||||
);
|
|
||||||
|
|
||||||
return Ok(HttpResponse::TemporaryRedirect()
|
return Ok(HttpResponse::TemporaryRedirect()
|
||||||
.append_header(("Location", &*redirect_url))
|
.append_header(("Location", &*redirect_url))
|
||||||
.json(serde_json::json!({ "url": redirect_url })));
|
.json(serde_json::json!({ "url": redirect_url })));
|
||||||
} else {
|
|
||||||
let mut ws_conn = {
|
|
||||||
let db = sockets.read().await;
|
|
||||||
|
|
||||||
let mut x = db
|
|
||||||
.auth_sockets
|
|
||||||
.get_mut(&state)
|
|
||||||
.ok_or_else(|| AuthenticationError::SocketError)?;
|
|
||||||
|
|
||||||
x.value_mut().clone()
|
|
||||||
};
|
|
||||||
|
|
||||||
ws_conn
|
|
||||||
.text(
|
|
||||||
serde_json::json!({
|
|
||||||
"error": "2fa_required",
|
|
||||||
"flow": flow,
|
|
||||||
}).to_string()
|
|
||||||
)
|
|
||||||
.await.map_err(|_| AuthenticationError::SocketError)?;
|
|
||||||
|
|
||||||
let _ = ws_conn.close(None).await;
|
|
||||||
|
|
||||||
return Ok(crate::auth::templates::Success {
|
|
||||||
icon: user.avatar_url.as_deref().unwrap_or("https://cdn-raw.modrinth.com/placeholder.svg"),
|
|
||||||
name: &user.username,
|
|
||||||
}.render());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
user_id
|
user_id
|
||||||
@ -1290,83 +1203,27 @@ pub async fn auth_callback(
|
|||||||
let session = issue_session(req, user_id, &mut transaction, &redis).await?;
|
let session = issue_session(req, user_id, &mut transaction, &redis).await?;
|
||||||
transaction.commit().await?;
|
transaction.commit().await?;
|
||||||
|
|
||||||
if let Some(url) = url {
|
let redirect_url = format!(
|
||||||
let redirect_url = format!(
|
"{}{}code={}{}",
|
||||||
"{}{}code={}{}",
|
url,
|
||||||
url,
|
if url.contains('?') { '&' } else { '?' },
|
||||||
if url.contains('?') { '&' } else { '?' },
|
session.session,
|
||||||
session.session,
|
if user_id_opt.is_none() {
|
||||||
if user_id_opt.is_none() {
|
"&new_account=true"
|
||||||
"&new_account=true"
|
} else {
|
||||||
} else {
|
""
|
||||||
""
|
}
|
||||||
}
|
);
|
||||||
);
|
|
||||||
|
|
||||||
Ok(HttpResponse::TemporaryRedirect()
|
Ok(HttpResponse::TemporaryRedirect()
|
||||||
.append_header(("Location", &*redirect_url))
|
.append_header(("Location", &*redirect_url))
|
||||||
.json(serde_json::json!({ "url": redirect_url })))
|
.json(serde_json::json!({ "url": redirect_url })))
|
||||||
} else {
|
|
||||||
let user = crate::database::models::user_item::User::get_id(
|
|
||||||
user_id,
|
|
||||||
&**client,
|
|
||||||
&redis,
|
|
||||||
)
|
|
||||||
.await?.ok_or_else(|| AuthenticationError::InvalidCredentials)?;
|
|
||||||
|
|
||||||
let mut ws_conn = {
|
|
||||||
let db = sockets.read().await;
|
|
||||||
|
|
||||||
let mut x = db
|
|
||||||
.auth_sockets
|
|
||||||
.get_mut(&state)
|
|
||||||
.ok_or_else(|| AuthenticationError::SocketError)?;
|
|
||||||
|
|
||||||
x.value_mut().clone()
|
|
||||||
};
|
|
||||||
|
|
||||||
ws_conn
|
|
||||||
.text(
|
|
||||||
serde_json::json!({
|
|
||||||
"code": session.session,
|
|
||||||
}).to_string()
|
|
||||||
)
|
|
||||||
.await.map_err(|_| AuthenticationError::SocketError)?;
|
|
||||||
let _ = ws_conn.close(None).await;
|
|
||||||
|
|
||||||
return Ok(crate::auth::templates::Success {
|
|
||||||
icon: user.avatar_url.as_deref().unwrap_or("https://cdn-raw.modrinth.com/placeholder.svg"),
|
|
||||||
name: &user.username,
|
|
||||||
}.render());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Err::<HttpResponse, AuthenticationError>(AuthenticationError::InvalidCredentials)
|
Err::<HttpResponse, AuthenticationError>(AuthenticationError::InvalidCredentials)
|
||||||
}
|
}
|
||||||
}.await;
|
}.await;
|
||||||
|
|
||||||
// Because this is callback route, if we have an error, we need to ensure we close the original socket if it exists
|
|
||||||
if let Err(ref e) = res {
|
|
||||||
let db = active_sockets.read().await;
|
|
||||||
let mut x = db.auth_sockets.get_mut(&state_string);
|
|
||||||
|
|
||||||
if let Some(x) = x.as_mut() {
|
|
||||||
let mut ws_conn = x.value_mut().clone();
|
|
||||||
|
|
||||||
ws_conn
|
|
||||||
.text(
|
|
||||||
serde_json::json!({
|
|
||||||
"error": &e.error_name(),
|
|
||||||
"description": &e.to_string(),
|
|
||||||
} )
|
|
||||||
.to_string(),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.map_err(|_| AuthenticationError::SocketError)?;
|
|
||||||
let _ = ws_conn.close(None).await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(res?)
|
Ok(res?)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1557,6 +1414,7 @@ pub async fn create_account_with_password(
|
|||||||
created: Utc::now(),
|
created: Utc::now(),
|
||||||
role: Role::Developer.to_string(),
|
role: Role::Developer.to_string(),
|
||||||
badges: Badges::default(),
|
badges: Badges::default(),
|
||||||
|
allow_friend_requests: true,
|
||||||
}
|
}
|
||||||
.insert(&mut transaction)
|
.insert(&mut transaction)
|
||||||
.await?;
|
.await?;
|
||||||
|
|||||||
@ -6,6 +6,8 @@ pub mod moderation;
|
|||||||
pub mod pats;
|
pub mod pats;
|
||||||
pub mod session;
|
pub mod session;
|
||||||
|
|
||||||
|
pub mod statuses;
|
||||||
|
|
||||||
use super::v3::oauth_clients;
|
use super::v3::oauth_clients;
|
||||||
pub use super::ApiError;
|
pub use super::ApiError;
|
||||||
use crate::util::cors::default_cors;
|
use crate::util::cors::default_cors;
|
||||||
@ -21,6 +23,7 @@ pub fn config(cfg: &mut actix_web::web::ServiceConfig) {
|
|||||||
.configure(pats::config)
|
.configure(pats::config)
|
||||||
.configure(moderation::config)
|
.configure(moderation::config)
|
||||||
.configure(billing::config)
|
.configure(billing::config)
|
||||||
.configure(gdpr::config),
|
.configure(gdpr::config)
|
||||||
|
.configure(statuses::config),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
255
apps/labrinth/src/routes/internal/statuses.rs
Normal file
255
apps/labrinth/src/routes/internal/statuses.rs
Normal file
@ -0,0 +1,255 @@
|
|||||||
|
use crate::auth::validate::get_user_record_from_bearer_token;
|
||||||
|
use crate::auth::AuthenticationError;
|
||||||
|
use crate::database::models::friend_item::FriendItem;
|
||||||
|
use crate::database::redis::RedisPool;
|
||||||
|
use crate::models::ids::UserId;
|
||||||
|
use crate::models::pats::Scopes;
|
||||||
|
use crate::models::users::{User, UserStatus};
|
||||||
|
use crate::queue::session::AuthQueue;
|
||||||
|
use crate::queue::socket::ActiveSockets;
|
||||||
|
use crate::routes::ApiError;
|
||||||
|
use actix_web::web::{Data, Payload};
|
||||||
|
use actix_web::{get, web, HttpRequest, HttpResponse};
|
||||||
|
use actix_ws::AggregatedMessage;
|
||||||
|
use chrono::Utc;
|
||||||
|
use futures_util::StreamExt;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use sqlx::PgPool;
|
||||||
|
|
||||||
|
pub fn config(cfg: &mut web::ServiceConfig) {
|
||||||
|
cfg.service(ws_init);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
#[serde(tag = "type", rename_all = "snake_case")]
|
||||||
|
pub enum ClientToServerMessage {
|
||||||
|
StatusUpdate { profile_name: Option<String> },
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
#[serde(tag = "type", rename_all = "snake_case")]
|
||||||
|
pub enum ServerToClientMessage {
|
||||||
|
StatusUpdate { status: UserStatus },
|
||||||
|
UserOffline { id: UserId },
|
||||||
|
FriendStatuses { statuses: Vec<UserStatus> },
|
||||||
|
FriendRequest { from: UserId },
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct LauncherHeartbeatInit {
|
||||||
|
code: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("launcher_heartbeat")]
|
||||||
|
pub async fn ws_init(
|
||||||
|
req: HttpRequest,
|
||||||
|
pool: Data<PgPool>,
|
||||||
|
web::Query(auth): web::Query<LauncherHeartbeatInit>,
|
||||||
|
body: Payload,
|
||||||
|
db: Data<ActiveSockets>,
|
||||||
|
redis: Data<RedisPool>,
|
||||||
|
session_queue: Data<AuthQueue>,
|
||||||
|
) -> Result<HttpResponse, ApiError> {
|
||||||
|
let (scopes, db_user) = get_user_record_from_bearer_token(
|
||||||
|
&req,
|
||||||
|
Some(&auth.code),
|
||||||
|
&**pool,
|
||||||
|
&redis,
|
||||||
|
&session_queue,
|
||||||
|
)
|
||||||
|
.await?
|
||||||
|
.ok_or_else(|| {
|
||||||
|
ApiError::Authentication(AuthenticationError::InvalidCredentials)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
if !scopes.contains(Scopes::SESSION_ACCESS) {
|
||||||
|
return Err(ApiError::Authentication(
|
||||||
|
AuthenticationError::InvalidCredentials,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let user = User::from_full(db_user);
|
||||||
|
|
||||||
|
if let Some((_, (_, session))) = db.auth_sockets.remove(&user.id) {
|
||||||
|
let _ = session.close(None).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
let (res, mut session, msg_stream) = match actix_ws::handle(&req, body) {
|
||||||
|
Ok(x) => x,
|
||||||
|
Err(e) => return Ok(e.error_response()),
|
||||||
|
};
|
||||||
|
|
||||||
|
let status = UserStatus {
|
||||||
|
user_id: user.id,
|
||||||
|
profile_name: None,
|
||||||
|
last_update: Utc::now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let friends =
|
||||||
|
FriendItem::get_user_friends(user.id.into(), Some(true), &**pool)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let friend_statuses = if !friends.is_empty() {
|
||||||
|
friends
|
||||||
|
.iter()
|
||||||
|
.filter_map(|x| {
|
||||||
|
db.auth_sockets.get(
|
||||||
|
&if x.user_id == user.id.into() {
|
||||||
|
x.friend_id
|
||||||
|
} else {
|
||||||
|
x.user_id
|
||||||
|
}
|
||||||
|
.into(),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.map(|x| x.value().0.clone())
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
} else {
|
||||||
|
Vec::new()
|
||||||
|
};
|
||||||
|
|
||||||
|
let _ = session
|
||||||
|
.text(serde_json::to_string(
|
||||||
|
&ServerToClientMessage::FriendStatuses {
|
||||||
|
statuses: friend_statuses,
|
||||||
|
},
|
||||||
|
)?)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
db.auth_sockets.insert(user.id, (status.clone(), session));
|
||||||
|
|
||||||
|
broadcast_friends(
|
||||||
|
user.id,
|
||||||
|
ServerToClientMessage::StatusUpdate { status },
|
||||||
|
&pool,
|
||||||
|
&redis,
|
||||||
|
&db,
|
||||||
|
Some(friends),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let mut stream = msg_stream
|
||||||
|
.aggregate_continuations()
|
||||||
|
// aggregate continuation frames up to 1MiB
|
||||||
|
.max_continuation_size(2_usize.pow(20));
|
||||||
|
|
||||||
|
actix_web::rt::spawn(async move {
|
||||||
|
// receive messages from websocket
|
||||||
|
while let Some(msg) = stream.next().await {
|
||||||
|
match msg {
|
||||||
|
Ok(AggregatedMessage::Text(text)) => {
|
||||||
|
if let Ok(message) =
|
||||||
|
serde_json::from_str::<ClientToServerMessage>(&text)
|
||||||
|
{
|
||||||
|
match message {
|
||||||
|
ClientToServerMessage::StatusUpdate {
|
||||||
|
profile_name,
|
||||||
|
} => {
|
||||||
|
if let Some(mut pair) =
|
||||||
|
db.auth_sockets.get_mut(&user.id)
|
||||||
|
{
|
||||||
|
let (status, _) = pair.value_mut();
|
||||||
|
|
||||||
|
if status.profile_name.as_ref().map(|x| x.len() > 64).unwrap_or(false) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
status.profile_name = profile_name;
|
||||||
|
status.last_update = Utc::now();
|
||||||
|
|
||||||
|
let _ = broadcast_friends(
|
||||||
|
user.id,
|
||||||
|
ServerToClientMessage::StatusUpdate {
|
||||||
|
status: status.clone(),
|
||||||
|
},
|
||||||
|
&pool,
|
||||||
|
&redis,
|
||||||
|
&db,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(AggregatedMessage::Close(_)) => {
|
||||||
|
let _ = close_socket(user.id, &pool, &redis, &db).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn broadcast_friends(
|
||||||
|
user_id: UserId,
|
||||||
|
message: ServerToClientMessage,
|
||||||
|
pool: &PgPool,
|
||||||
|
redis: &RedisPool,
|
||||||
|
sockets: &ActiveSockets,
|
||||||
|
friends: Option<Vec<FriendItem>>,
|
||||||
|
) -> Result<(), crate::database::models::DatabaseError> {
|
||||||
|
let friends = if let Some(friends) = friends {
|
||||||
|
friends
|
||||||
|
} else {
|
||||||
|
FriendItem::get_user_friends(user_id.into(), Some(true), pool).await?
|
||||||
|
};
|
||||||
|
|
||||||
|
for friend in friends {
|
||||||
|
let friend_id = if friend.user_id == user_id.into() {
|
||||||
|
friend.friend_id
|
||||||
|
} else {
|
||||||
|
friend.user_id
|
||||||
|
};
|
||||||
|
|
||||||
|
if friend.accepted {
|
||||||
|
if let Some(mut socket) =
|
||||||
|
sockets.auth_sockets.get_mut(&friend_id.into())
|
||||||
|
{
|
||||||
|
let (_, socket) = socket.value_mut();
|
||||||
|
|
||||||
|
// TODO: bulk close sockets for better perf
|
||||||
|
if socket.text(serde_json::to_string(&message)?).await.is_err()
|
||||||
|
{
|
||||||
|
Box::pin(close_socket(
|
||||||
|
friend_id.into(),
|
||||||
|
pool,
|
||||||
|
redis,
|
||||||
|
sockets,
|
||||||
|
))
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn close_socket(
|
||||||
|
id: UserId,
|
||||||
|
pool: &PgPool,
|
||||||
|
redis: &RedisPool,
|
||||||
|
sockets: &ActiveSockets,
|
||||||
|
) -> Result<(), crate::database::models::DatabaseError> {
|
||||||
|
if let Some((_, (_, socket))) = sockets.auth_sockets.remove(&id) {
|
||||||
|
let _ = socket.close(None).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
broadcast_friends(
|
||||||
|
id,
|
||||||
|
ServerToClientMessage::UserOffline { id },
|
||||||
|
pool,
|
||||||
|
redis,
|
||||||
|
sockets,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@ -156,6 +156,7 @@ pub struct EditUser {
|
|||||||
pub bio: Option<Option<String>>,
|
pub bio: Option<Option<String>>,
|
||||||
pub role: Option<Role>,
|
pub role: Option<Role>,
|
||||||
pub badges: Option<Badges>,
|
pub badges: Option<Badges>,
|
||||||
|
pub allow_friend_requests: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[patch("{id}")]
|
#[patch("{id}")]
|
||||||
@ -178,6 +179,7 @@ pub async fn user_edit(
|
|||||||
role: new_user.role,
|
role: new_user.role,
|
||||||
badges: new_user.badges,
|
badges: new_user.badges,
|
||||||
venmo_handle: None,
|
venmo_handle: None,
|
||||||
|
allow_friend_requests: new_user.allow_friend_requests,
|
||||||
}),
|
}),
|
||||||
pool,
|
pool,
|
||||||
redis,
|
redis,
|
||||||
|
|||||||
242
apps/labrinth/src/routes/v3/friends.rs
Normal file
242
apps/labrinth/src/routes/v3/friends.rs
Normal file
@ -0,0 +1,242 @@
|
|||||||
|
use crate::auth::get_user_from_headers;
|
||||||
|
use crate::database::models::UserId;
|
||||||
|
use crate::database::redis::RedisPool;
|
||||||
|
use crate::models::pats::Scopes;
|
||||||
|
use crate::models::users::UserFriend;
|
||||||
|
use crate::queue::session::AuthQueue;
|
||||||
|
use crate::queue::socket::ActiveSockets;
|
||||||
|
use crate::routes::internal::statuses::{close_socket, ServerToClientMessage};
|
||||||
|
use crate::routes::ApiError;
|
||||||
|
use actix_web::{delete, get, post, web, HttpRequest, HttpResponse};
|
||||||
|
use chrono::Utc;
|
||||||
|
use sqlx::PgPool;
|
||||||
|
|
||||||
|
pub fn config(cfg: &mut web::ServiceConfig) {
|
||||||
|
cfg.service(add_friend);
|
||||||
|
cfg.service(remove_friend);
|
||||||
|
cfg.service(friends);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[post("friend/{id}")]
|
||||||
|
pub async fn add_friend(
|
||||||
|
req: HttpRequest,
|
||||||
|
info: web::Path<(String,)>,
|
||||||
|
pool: web::Data<PgPool>,
|
||||||
|
redis: web::Data<RedisPool>,
|
||||||
|
session_queue: web::Data<AuthQueue>,
|
||||||
|
db: web::Data<ActiveSockets>,
|
||||||
|
) -> Result<HttpResponse, ApiError> {
|
||||||
|
let user = get_user_from_headers(
|
||||||
|
&req,
|
||||||
|
&**pool,
|
||||||
|
&redis,
|
||||||
|
&session_queue,
|
||||||
|
Some(&[Scopes::USER_WRITE]),
|
||||||
|
)
|
||||||
|
.await?
|
||||||
|
.1;
|
||||||
|
|
||||||
|
let string = info.into_inner().0;
|
||||||
|
let friend =
|
||||||
|
crate::database::models::User::get(&string, &**pool, &redis).await?;
|
||||||
|
|
||||||
|
if let Some(friend) = friend {
|
||||||
|
let mut transaction = pool.begin().await?;
|
||||||
|
|
||||||
|
if let Some(friend) =
|
||||||
|
crate::database::models::friend_item::FriendItem::get_friend(
|
||||||
|
user.id.into(),
|
||||||
|
friend.id,
|
||||||
|
&**pool,
|
||||||
|
)
|
||||||
|
.await?
|
||||||
|
{
|
||||||
|
if friend.accepted {
|
||||||
|
return Err(ApiError::InvalidInput(
|
||||||
|
"You are already friends with this user!".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
if !friend.accepted && user.id != friend.friend_id.into() {
|
||||||
|
return Err(ApiError::InvalidInput(
|
||||||
|
"You cannot accept your own friend request!".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
crate::database::models::friend_item::FriendItem::update_friend(
|
||||||
|
friend.user_id,
|
||||||
|
friend.friend_id,
|
||||||
|
true,
|
||||||
|
&mut transaction,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
async fn send_friend_status(
|
||||||
|
user_id: UserId,
|
||||||
|
friend_id: UserId,
|
||||||
|
pool: &PgPool,
|
||||||
|
redis: &RedisPool,
|
||||||
|
sockets: &ActiveSockets,
|
||||||
|
) -> Result<(), ApiError> {
|
||||||
|
if let Some(pair) = sockets.auth_sockets.get(&user_id.into()) {
|
||||||
|
let (friend_status, _) = pair.value();
|
||||||
|
if let Some(mut socket) =
|
||||||
|
sockets.auth_sockets.get_mut(&friend_id.into())
|
||||||
|
{
|
||||||
|
let (_, socket) = socket.value_mut();
|
||||||
|
|
||||||
|
if socket
|
||||||
|
.text(serde_json::to_string(
|
||||||
|
&ServerToClientMessage::StatusUpdate {
|
||||||
|
status: friend_status.clone(),
|
||||||
|
},
|
||||||
|
)?)
|
||||||
|
.await
|
||||||
|
.is_err()
|
||||||
|
{
|
||||||
|
close_socket(
|
||||||
|
friend_id.into(),
|
||||||
|
pool,
|
||||||
|
redis,
|
||||||
|
sockets,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
send_friend_status(
|
||||||
|
friend.user_id,
|
||||||
|
friend.friend_id,
|
||||||
|
&pool,
|
||||||
|
&redis,
|
||||||
|
&db,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
send_friend_status(
|
||||||
|
friend.friend_id,
|
||||||
|
friend.user_id,
|
||||||
|
&pool,
|
||||||
|
&redis,
|
||||||
|
&db,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
} else {
|
||||||
|
if friend.id == user.id.into() {
|
||||||
|
return Err(ApiError::InvalidInput(
|
||||||
|
"You cannot add yourself as a friend!".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
if !friend.allow_friend_requests {
|
||||||
|
return Err(ApiError::InvalidInput(
|
||||||
|
"Friend requests are disabled for this user!".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
crate::database::models::friend_item::FriendItem {
|
||||||
|
user_id: user.id.into(),
|
||||||
|
friend_id: friend.id,
|
||||||
|
created: Utc::now(),
|
||||||
|
accepted: false,
|
||||||
|
}
|
||||||
|
.insert(&mut transaction)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if let Some(mut socket) = db.auth_sockets.get_mut(&friend.id.into())
|
||||||
|
{
|
||||||
|
let (_, socket) = socket.value_mut();
|
||||||
|
|
||||||
|
if socket
|
||||||
|
.text(serde_json::to_string(
|
||||||
|
&ServerToClientMessage::FriendRequest { from: user.id },
|
||||||
|
)?)
|
||||||
|
.await
|
||||||
|
.is_err()
|
||||||
|
{
|
||||||
|
close_socket(user.id, &pool, &redis, &db).await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
transaction.commit().await?;
|
||||||
|
|
||||||
|
Ok(HttpResponse::NoContent().body(""))
|
||||||
|
} else {
|
||||||
|
Err(ApiError::NotFound)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[delete("friend/{id}")]
|
||||||
|
pub async fn remove_friend(
|
||||||
|
req: HttpRequest,
|
||||||
|
info: web::Path<(String,)>,
|
||||||
|
pool: web::Data<PgPool>,
|
||||||
|
redis: web::Data<RedisPool>,
|
||||||
|
session_queue: web::Data<AuthQueue>,
|
||||||
|
) -> Result<HttpResponse, ApiError> {
|
||||||
|
let user = get_user_from_headers(
|
||||||
|
&req,
|
||||||
|
&**pool,
|
||||||
|
&redis,
|
||||||
|
&session_queue,
|
||||||
|
Some(&[Scopes::USER_WRITE]),
|
||||||
|
)
|
||||||
|
.await?
|
||||||
|
.1;
|
||||||
|
|
||||||
|
let string = info.into_inner().0;
|
||||||
|
let friend =
|
||||||
|
crate::database::models::User::get(&string, &**pool, &redis).await?;
|
||||||
|
|
||||||
|
if let Some(friend) = friend {
|
||||||
|
let mut transaction = pool.begin().await?;
|
||||||
|
|
||||||
|
crate::database::models::friend_item::FriendItem::remove(
|
||||||
|
user.id.into(),
|
||||||
|
friend.id,
|
||||||
|
&mut transaction,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
transaction.commit().await?;
|
||||||
|
|
||||||
|
Ok(HttpResponse::NoContent().body(""))
|
||||||
|
} else {
|
||||||
|
Err(ApiError::NotFound)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("friends")]
|
||||||
|
pub async fn friends(
|
||||||
|
req: HttpRequest,
|
||||||
|
pool: web::Data<PgPool>,
|
||||||
|
redis: web::Data<RedisPool>,
|
||||||
|
session_queue: web::Data<AuthQueue>,
|
||||||
|
) -> Result<HttpResponse, ApiError> {
|
||||||
|
let user = get_user_from_headers(
|
||||||
|
&req,
|
||||||
|
&**pool,
|
||||||
|
&redis,
|
||||||
|
&session_queue,
|
||||||
|
Some(&[Scopes::USER_READ]),
|
||||||
|
)
|
||||||
|
.await?
|
||||||
|
.1;
|
||||||
|
|
||||||
|
let friends =
|
||||||
|
crate::database::models::friend_item::FriendItem::get_user_friends(
|
||||||
|
user.id.into(),
|
||||||
|
None,
|
||||||
|
&**pool,
|
||||||
|
)
|
||||||
|
.await?
|
||||||
|
.into_iter()
|
||||||
|
.map(UserFriend::from)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
Ok(HttpResponse::Ok().json(friends))
|
||||||
|
}
|
||||||
@ -5,6 +5,7 @@ use serde_json::json;
|
|||||||
|
|
||||||
pub mod analytics_get;
|
pub mod analytics_get;
|
||||||
pub mod collections;
|
pub mod collections;
|
||||||
|
pub mod friends;
|
||||||
pub mod images;
|
pub mod images;
|
||||||
pub mod notifications;
|
pub mod notifications;
|
||||||
pub mod organizations;
|
pub mod organizations;
|
||||||
@ -42,7 +43,8 @@ pub fn config(cfg: &mut web::ServiceConfig) {
|
|||||||
.configure(users::config)
|
.configure(users::config)
|
||||||
.configure(version_file::config)
|
.configure(version_file::config)
|
||||||
.configure(payouts::config)
|
.configure(payouts::config)
|
||||||
.configure(versions::config),
|
.configure(versions::config)
|
||||||
|
.configure(friends::config),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -300,6 +300,7 @@ pub struct EditUser {
|
|||||||
pub badges: Option<Badges>,
|
pub badges: Option<Badges>,
|
||||||
#[validate(length(max = 160))]
|
#[validate(length(max = 160))]
|
||||||
pub venmo_handle: Option<String>,
|
pub venmo_handle: Option<String>,
|
||||||
|
pub allow_friend_requests: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn user_edit(
|
pub async fn user_edit(
|
||||||
@ -438,6 +439,20 @@ pub async fn user_edit(
|
|||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(allow_friend_requests) = &user.allow_friend_requests {
|
||||||
|
sqlx::query!(
|
||||||
|
"
|
||||||
|
UPDATE users
|
||||||
|
SET allow_friend_requests = $1
|
||||||
|
WHERE (id = $2)
|
||||||
|
",
|
||||||
|
allow_friend_requests,
|
||||||
|
id as crate::database::models::ids::UserId,
|
||||||
|
)
|
||||||
|
.execute(&mut *transaction)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
transaction.commit().await?;
|
transaction.commit().await?;
|
||||||
User::clear_caches(&[(id, Some(actual_user.username))], &redis)
|
User::clear_caches(&[(id, Some(actual_user.username))], &redis)
|
||||||
.await?;
|
.await?;
|
||||||
|
|||||||
@ -16,9 +16,9 @@ pub mod data {
|
|||||||
pub use crate::state::{
|
pub use crate::state::{
|
||||||
CacheBehaviour, CacheValueType, Credentials, Dependency, DirectoryInfo,
|
CacheBehaviour, CacheValueType, Credentials, Dependency, DirectoryInfo,
|
||||||
Hooks, JavaVersion, LinkedData, MemorySettings, ModLoader,
|
Hooks, JavaVersion, LinkedData, MemorySettings, ModLoader,
|
||||||
ModrinthCredentials, ModrinthCredentialsResult, Organization,
|
ModrinthCredentials, Organization, ProcessMetadata, ProfileFile,
|
||||||
ProcessMetadata, ProfileFile, Project, ProjectType, SearchResult,
|
Project, ProjectType, SearchResult, SearchResults, Settings,
|
||||||
SearchResults, Settings, TeamMember, Theme, User, Version, WindowSize,
|
TeamMember, Theme, User, Version, WindowSize,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,84 +1,18 @@
|
|||||||
use crate::state::{ModrinthCredentials, ModrinthCredentialsResult};
|
use crate::state::ModrinthCredentials;
|
||||||
use serde_json::Value;
|
|
||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
#[tracing::instrument]
|
#[tracing::instrument]
|
||||||
pub fn authenticate_begin_flow(provider: &str) -> String {
|
pub fn authenticate_begin_flow() -> &'static str {
|
||||||
crate::state::get_login_url(provider)
|
crate::state::get_login_url()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument]
|
#[tracing::instrument]
|
||||||
pub async fn authenticate_finish_flow(
|
pub async fn authenticate_finish_flow(
|
||||||
response: HashMap<String, Value>,
|
code: &str,
|
||||||
) -> crate::Result<ModrinthCredentialsResult> {
|
) -> crate::Result<ModrinthCredentials> {
|
||||||
let state = crate::State::get().await?;
|
let state = crate::State::get().await?;
|
||||||
|
|
||||||
let creds = crate::state::finish_login_flow(
|
let creds = crate::state::finish_login_flow(
|
||||||
response,
|
code,
|
||||||
&state.api_semaphore,
|
|
||||||
&state.pool,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
if let ModrinthCredentialsResult::Credentials(creds) = &creds {
|
|
||||||
creds.upsert(&state.pool).await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(creds)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn login_password(
|
|
||||||
username: &str,
|
|
||||||
password: &str,
|
|
||||||
challenge: &str,
|
|
||||||
) -> crate::Result<ModrinthCredentialsResult> {
|
|
||||||
let state = crate::State::get().await?;
|
|
||||||
let creds = crate::state::login_password(
|
|
||||||
username,
|
|
||||||
password,
|
|
||||||
challenge,
|
|
||||||
&state.api_semaphore,
|
|
||||||
&state.pool,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
if let ModrinthCredentialsResult::Credentials(creds) = &creds {
|
|
||||||
creds.upsert(&state.pool).await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(creds)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tracing::instrument]
|
|
||||||
pub async fn login_2fa(
|
|
||||||
code: &str,
|
|
||||||
flow: &str,
|
|
||||||
) -> crate::Result<ModrinthCredentials> {
|
|
||||||
let state = crate::State::get().await?;
|
|
||||||
let creds =
|
|
||||||
crate::state::login_2fa(code, flow, &state.api_semaphore, &state.pool)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
creds.upsert(&state.pool).await?;
|
|
||||||
|
|
||||||
Ok(creds)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tracing::instrument]
|
|
||||||
pub async fn create_account(
|
|
||||||
username: &str,
|
|
||||||
email: &str,
|
|
||||||
password: &str,
|
|
||||||
challenge: &str,
|
|
||||||
sign_up_newsletter: bool,
|
|
||||||
) -> crate::Result<ModrinthCredentials> {
|
|
||||||
let state = crate::State::get().await?;
|
|
||||||
let creds = crate::state::create_account(
|
|
||||||
username,
|
|
||||||
email,
|
|
||||||
password,
|
|
||||||
challenge,
|
|
||||||
sign_up_newsletter,
|
|
||||||
&state.api_semaphore,
|
&state.api_semaphore,
|
||||||
&state.pool,
|
&state.pool,
|
||||||
)
|
)
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
//! Configuration structs
|
//! Configuration structs
|
||||||
|
|
||||||
pub const MODRINTH_API_URL: &str = "https://staging-api.modrinth.com/v2/";
|
pub const MODRINTH_API_URL: &str = "https://api.modrinth.com/v2/";
|
||||||
pub const MODRINTH_API_URL_V3: &str = "https://staging-api.modrinth.com/v3/";
|
pub const MODRINTH_API_URL_V3: &str = "https://api.modrinth.com/v3/";
|
||||||
|
|
||||||
pub const META_URL: &str = "https://launcher-meta.modrinth.com/";
|
pub const META_URL: &str = "https://launcher-meta.modrinth.com/";
|
||||||
|
|||||||
@ -6,8 +6,6 @@ use dashmap::DashMap;
|
|||||||
use futures::TryStreamExt;
|
use futures::TryStreamExt;
|
||||||
use reqwest::Method;
|
use reqwest::Method;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json::Value;
|
|
||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
pub struct ModrinthCredentials {
|
pub struct ModrinthCredentials {
|
||||||
@ -192,176 +190,23 @@ impl ModrinthCredentials {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Debug)]
|
pub fn get_login_url() -> &'static str {
|
||||||
#[serde(tag = "type")]
|
"https:/modrinth.com/auth/sign-in?launcher=true"
|
||||||
#[serde(rename_all = "snake_case")]
|
|
||||||
pub enum ModrinthCredentialsResult {
|
|
||||||
TwoFactorRequired { flow: String },
|
|
||||||
Credentials(ModrinthCredentials),
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn get_result_from_res(
|
|
||||||
code_key: &str,
|
|
||||||
response: HashMap<String, Value>,
|
|
||||||
semaphore: &FetchSemaphore,
|
|
||||||
exec: impl sqlx::Executor<'_, Database = sqlx::Sqlite>,
|
|
||||||
) -> crate::Result<ModrinthCredentialsResult> {
|
|
||||||
if let Some(flow) = response.get("flow").and_then(|x| x.as_str()) {
|
|
||||||
Ok(ModrinthCredentialsResult::TwoFactorRequired {
|
|
||||||
flow: flow.to_string(),
|
|
||||||
})
|
|
||||||
} else if let Some(code) = response.get(code_key).and_then(|x| x.as_str()) {
|
|
||||||
let info = fetch_info(code, semaphore, exec).await?;
|
|
||||||
|
|
||||||
Ok(ModrinthCredentialsResult::Credentials(
|
|
||||||
ModrinthCredentials {
|
|
||||||
session: code.to_string(),
|
|
||||||
expires: Utc::now() + Duration::weeks(2),
|
|
||||||
user_id: info.id,
|
|
||||||
active: true,
|
|
||||||
},
|
|
||||||
))
|
|
||||||
} else if let Some(error) =
|
|
||||||
response.get("description").and_then(|x| x.as_str())
|
|
||||||
{
|
|
||||||
Err(crate::ErrorKind::OtherError(format!(
|
|
||||||
"Failed to login with error {error}"
|
|
||||||
))
|
|
||||||
.as_error())
|
|
||||||
} else {
|
|
||||||
Err(crate::ErrorKind::OtherError(String::from(
|
|
||||||
"Flow/code/error not found in response!",
|
|
||||||
))
|
|
||||||
.as_error())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn get_creds_from_res(
|
|
||||||
response: HashMap<String, Value>,
|
|
||||||
semaphore: &FetchSemaphore,
|
|
||||||
exec: impl sqlx::Executor<'_, Database = sqlx::Sqlite>,
|
|
||||||
) -> crate::Result<ModrinthCredentials> {
|
|
||||||
if let Some(code) = response.get("session").and_then(|x| x.as_str()) {
|
|
||||||
let info = fetch_info(code, semaphore, exec).await?;
|
|
||||||
|
|
||||||
Ok(ModrinthCredentials {
|
|
||||||
session: code.to_string(),
|
|
||||||
expires: Utc::now() + Duration::weeks(2),
|
|
||||||
user_id: info.id,
|
|
||||||
active: true,
|
|
||||||
})
|
|
||||||
} else if let Some(error) =
|
|
||||||
response.get("description").and_then(|x| x.as_str())
|
|
||||||
{
|
|
||||||
Err(crate::ErrorKind::OtherError(format!(
|
|
||||||
"Failed to login with error {error}"
|
|
||||||
))
|
|
||||||
.as_error())
|
|
||||||
} else {
|
|
||||||
Err(crate::ErrorKind::OtherError(String::from(
|
|
||||||
"Flow/code/error not found in response!",
|
|
||||||
))
|
|
||||||
.as_error())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_login_url(provider: &str) -> String {
|
|
||||||
format!(
|
|
||||||
"{MODRINTH_API_URL}auth/init?url={}&provider={provider}",
|
|
||||||
urlencoding::encode("https://launcher-files.modrinth.com/detect.txt")
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn finish_login_flow(
|
pub async fn finish_login_flow(
|
||||||
response: HashMap<String, Value>,
|
code: &str,
|
||||||
semaphore: &FetchSemaphore,
|
semaphore: &FetchSemaphore,
|
||||||
exec: impl sqlx::Executor<'_, Database = sqlx::Sqlite>,
|
exec: impl sqlx::Executor<'_, Database = sqlx::Sqlite>,
|
||||||
) -> crate::Result<ModrinthCredentialsResult> {
|
|
||||||
get_result_from_res("code", response, semaphore, exec).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn login_password(
|
|
||||||
username: &str,
|
|
||||||
password: &str,
|
|
||||||
challenge: &str,
|
|
||||||
semaphore: &FetchSemaphore,
|
|
||||||
exec: impl sqlx::Executor<'_, Database = sqlx::Sqlite> + Copy,
|
|
||||||
) -> crate::Result<ModrinthCredentialsResult> {
|
|
||||||
let resp = fetch_advanced(
|
|
||||||
Method::POST,
|
|
||||||
&format!("{MODRINTH_API_URL}auth/login"),
|
|
||||||
None,
|
|
||||||
Some(serde_json::json!({
|
|
||||||
"username": username,
|
|
||||||
"password": password,
|
|
||||||
"challenge": challenge,
|
|
||||||
})),
|
|
||||||
None,
|
|
||||||
None,
|
|
||||||
semaphore,
|
|
||||||
exec,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
let value = serde_json::from_slice::<HashMap<String, Value>>(&resp)?;
|
|
||||||
|
|
||||||
get_result_from_res("session", value, semaphore, exec).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn login_2fa(
|
|
||||||
code: &str,
|
|
||||||
flow: &str,
|
|
||||||
semaphore: &FetchSemaphore,
|
|
||||||
exec: impl sqlx::Executor<'_, Database = sqlx::Sqlite> + Copy,
|
|
||||||
) -> crate::Result<ModrinthCredentials> {
|
) -> crate::Result<ModrinthCredentials> {
|
||||||
let resp = fetch_advanced(
|
let info = fetch_info(code, semaphore, exec).await?;
|
||||||
Method::POST,
|
|
||||||
&format!("{MODRINTH_API_URL}auth/login/2fa"),
|
|
||||||
None,
|
|
||||||
Some(serde_json::json!({
|
|
||||||
"code": code,
|
|
||||||
"flow": flow,
|
|
||||||
})),
|
|
||||||
None,
|
|
||||||
None,
|
|
||||||
semaphore,
|
|
||||||
exec,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let response = serde_json::from_slice::<HashMap<String, Value>>(&resp)?;
|
Ok(ModrinthCredentials {
|
||||||
|
session: code.to_string(),
|
||||||
get_creds_from_res(response, semaphore, exec).await
|
expires: Utc::now() + Duration::weeks(2),
|
||||||
}
|
user_id: info.id,
|
||||||
|
active: true,
|
||||||
pub async fn create_account(
|
})
|
||||||
username: &str,
|
|
||||||
email: &str,
|
|
||||||
password: &str,
|
|
||||||
challenge: &str,
|
|
||||||
sign_up_newsletter: bool,
|
|
||||||
semaphore: &FetchSemaphore,
|
|
||||||
exec: impl sqlx::Executor<'_, Database = sqlx::Sqlite> + Copy,
|
|
||||||
) -> crate::Result<ModrinthCredentials> {
|
|
||||||
let resp = fetch_advanced(
|
|
||||||
Method::POST,
|
|
||||||
&format!("{MODRINTH_API_URL}auth/create"),
|
|
||||||
None,
|
|
||||||
Some(serde_json::json!({
|
|
||||||
"username": username,
|
|
||||||
"email": email,
|
|
||||||
"password": password,
|
|
||||||
"challenge": challenge,
|
|
||||||
"sign_up_newsletter": sign_up_newsletter,
|
|
||||||
})),
|
|
||||||
None,
|
|
||||||
None,
|
|
||||||
semaphore,
|
|
||||||
exec,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
let response = serde_json::from_slice::<HashMap<String, Value>>(&resp)?;
|
|
||||||
|
|
||||||
get_creds_from_res(response, semaphore, exec).await
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn fetch_info(
|
async fn fetch_info(
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user