1 Commits

56 changed files with 638 additions and 3253 deletions

4
.gitattributes vendored
View File

@@ -1,2 +1,2 @@
# Normalize EOL for all files that Git considers text files.
* text=auto eol=lf
# Auto detect text files and perform LF normalization
* text=auto

12
.gitignore vendored
View File

@@ -13,15 +13,3 @@ Cargo.lock
# MSVC Windows builds of rustc generate these, which store debugging information
*.pdb
/.idea
client/.idea
client/.gradle
client/build
server/.idea
server/.env
auth/.env
*.db
*.db-shm
*.db-wal

42
Client/.gitignore vendored Normal file
View File

@@ -0,0 +1,42 @@
.gradle
build/
!gradle/wrapper/gradle-wrapper.jar
!**/src/main/**/build/
!**/src/test/**/build/
### IntelliJ IDEA ###
.idea/modules.xml
.idea/jarRepositories.xml
.idea/compiler.xml
.idea/libraries/
*.iws
*.iml
*.ipr
out/
!**/src/main/**/out/
!**/src/test/**/out/
### Eclipse ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
bin/
!**/src/main/**/bin/
!**/src/test/**/bin/
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
### VS Code ###
.vscode/
### Mac OS ###
.DS_Store

3
Client/.idea/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,3 @@
# Default ignored files
/shelf/
/workspace.xml

17
Client/.idea/gradle.xml generated Normal file
View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GradleMigrationSettings" migrationVersion="1" />
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleJvm" value="temurin-20" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
</set>
</option>
</GradleProjectSettings>
</option>
</component>
</project>

6
Client/.idea/kotlinc.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="KotlinJpsPluginSettings">
<option name="version" value="1.9.22" />
</component>
</project>

7
Client/.idea/misc.xml generated Normal file
View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_20" project-jdk-name="openjdk-22" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>

7
Client/.idea/vcs.xml generated Normal file
View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

View File

@@ -0,0 +1,21 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="desktop" type="GradleRunConfiguration" factoryName="Gradle">
<ExternalSystemSettings>
<option name="executionName" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="externalSystemIdString" value="GRADLE" />
<option name="scriptParameters" value="" />
<option name="taskDescriptions">
<list />
</option>
<option name="taskNames">
<list>
<option value="run" />
</list>
</option>
<option name="vmOptions" value="" />
</ExternalSystemSettings>
<GradleScriptDebugEnabled>true</GradleScriptDebugEnabled>
<method v="2" />
</configuration>
</component>

35
Client/build.gradle.kts Normal file
View File

@@ -0,0 +1,35 @@
import org.jetbrains.compose.desktop.application.dsl.TargetFormat
plugins {
kotlin("jvm")
id("org.jetbrains.compose")
}
group = "me.joshuafhiggins"
version = "1.0-SNAPSHOT"
repositories {
mavenCentral()
maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
google()
}
dependencies {
// Note, if you develop a library, you should use compose.desktop.common.
// compose.desktop.currentOs should be used in launcher-sourceSet
// (in a separate module for demo project and in testMain).
// With compose.desktop.common you will also lose @Preview functionality
implementation(compose.desktop.currentOs)
}
compose.desktop {
application {
mainClass = "MainKt"
nativeDistributions {
targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb)
packageName = "Client"
packageVersion = "1.0.0"
}
}
}

4
Client/gradle.properties Normal file
View File

@@ -0,0 +1,4 @@
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
kotlin.code.style=official
kotlin.version=1.9.22
compose.version=1.6.0

BIN
Client/gradle/wrapper/gradle-wrapper.jar vendored Normal file

Binary file not shown.

View File

@@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

234
Client/gradlew vendored Normal file
View File

@@ -0,0 +1,234 @@
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
APP_NAME="Gradle"
APP_BASE_NAME=${0##*/}
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Collect all arguments for the java command;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
# shell script including quotes and variable substitutions, so put them in
# double quotes to make sure that they get re-expanded; and
# * put everything else in single quotes, so that it's not re-expanded.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"

89
Client/gradlew.bat vendored Normal file
View File

@@ -0,0 +1,89 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

View File

@@ -0,0 +1,15 @@
pluginManagement {
repositories {
maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
google()
gradlePluginPortal()
mavenCentral()
}
plugins {
kotlin("jvm").version(extra["kotlin.version"] as String)
id("org.jetbrains.compose").version(extra["compose.version"] as String)
}
}
rootProject.name = "Client"

View File

@@ -0,0 +1,31 @@
import androidx.compose.desktop.ui.tooling.preview.Preview
import androidx.compose.material.Button
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application
@Composable
@Preview
fun App() {
var text by remember { mutableStateOf("Hello, World!") }
MaterialTheme {
Button(onClick = {
text = "Hello, Desktop!"
}) {
Text(text)
}
}
}
fun main() = application {
Window(onCloseRequest = ::exitApplication) {
App()
}
}

29
Server/.gitignore vendored Normal file
View File

@@ -0,0 +1,29 @@
### IntelliJ IDEA ###
out/
!**/src/main/**/out/
!**/src/test/**/out/
### Eclipse ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
bin/
!**/src/main/**/bin/
!**/src/test/**/bin/
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
### VS Code ###
.vscode/
### Mac OS ###
.DS_Store

3
Server/.idea/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,3 @@
# Default ignored files
/shelf/
/workspace.xml

View File

@@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="ReplaceUntilWithRangeUntil" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
</profile>
</component>

10
Server/.idea/kotlinc.xml generated Normal file
View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Kotlin2JvmCompilerArguments">
<option name="jvmTarget" value="1.8" />
</component>
<component name="KotlinCommonCompilerArguments">
<option name="apiVersion" value="1.9" />
<option name="languageVersion" value="1.9" />
</component>
</project>

View File

@@ -0,0 +1,17 @@
<component name="libraryTable">
<library name="KotlinJavaRuntime" type="repository">
<properties maven-id="org.jetbrains.kotlin:kotlin-stdlib:1.9.23" />
<CLASSES>
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib/1.9.23/kotlin-stdlib-1.9.23.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/annotations/13.0/annotations-13.0.jar!/" />
</CLASSES>
<JAVADOC>
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib/1.9.23/kotlin-stdlib-1.9.23-javadoc.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/annotations/13.0/annotations-13.0-javadoc.jar!/" />
</JAVADOC>
<SOURCES>
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib/1.9.23/kotlin-stdlib-1.9.23-sources.jar!/" />
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/annotations/13.0/annotations-13.0-sources.jar!/" />
</SOURCES>
</library>
</component>

6
Server/.idea/misc.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager" version="2" languageLevel="JDK_22" default="true" project-jdk-name="openjdk-22" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>

8
Server/.idea/modules.xml generated Normal file
View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/Server.iml" filepath="$PROJECT_DIR$/Server.iml" />
</modules>
</component>
</project>

6
Server/.idea/vcs.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

15
Server/Server.iml Normal file
View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/resources" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/test" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/testResources" type="java-test-resource" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="KotlinJavaRuntime" level="project" />
</component>
</module>

14
Server/src/Main.kt Normal file
View File

@@ -0,0 +1,14 @@
//TIP To <b>Run</b> code, press <shortcut actionId="Run"/> or
// click the <icon src="AllIcons.Actions.Execute"/> icon in the gutter.
fun main() {
val name = "Kotlin"
//TIP Press <shortcut actionId="ShowIntentionActions"/> with your caret at the highlighted text
// to see how IntelliJ IDEA suggests fixing it.
println("Hello, " + name + "!")
for (i in 1..5) {
//TIP Press <shortcut actionId="Debug"/> to start debugging your code. We have set one <icon src="AllIcons.Debugger.Db_set_breakpoint"/> breakpoint
// for you, but you can always add more by pressing <shortcut actionId="ToggleLineBreakpoint"/>.
println("i = $i")
}
}

View File

@@ -1,22 +0,0 @@
[package]
name = "realm_auth"
version = "0.1.0"
edition = "2021"
[dependencies]
anyhow = "1.0.89"
futures = "0.3.30"
tarpc = { version = "0.34.0", features = ["full"] }
tokio = { version = "1.40.0", features = ["macros", "net", "rt-multi-thread"] }
tracing = "0.1.40"
tracing-subscriber = "0.3.18"
serde = { version = "1.0.210", features = ["derive"] }
chrono = { version = "0.4.38", features = ["serde"] }
dotenvy = "0.15.7"
sqlx = { version = "0.8.2", features = [ "runtime-tokio", "tls-rustls", "sqlite", "macros", "migrate", "chrono" ] }
sha3 = "0.10.8"
hex = "0.4.3"
rand = "0.8.5"
lettre = "0.11.9"
regex = "1.10.5"
realm_shared = { path = "../shared" }

View File

@@ -1,5 +0,0 @@
// generated by `sqlx migrate build-script`
fn main() {
// trigger recompilation when a new migration is added
println!("cargo:rerun-if-changed=migrations");
}

View File

@@ -1,12 +0,0 @@
DATABASE_URL=sqlite:auth.db
PORT=5052
GOOGLE_CLIENT_ID=
GOOGLE_PRIVATE_KEY=
DOMAIN=
SERVER_MAIL_PORT=
SERVER_MAIL_ADDRESS=
SERVER_MAIL_NAME=
SERVER_MAIL_FROM_ADDRESS=
SERVER_MAIL_USERNAME=
SERVER_MAIL_PASSWORD=

View File

@@ -1,68 +0,0 @@
<h1>
<svg width="200" height="50" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" viewBox="0 0 400 100">
<defs>
<style>
.cls-1 {
fill: #010101;
}
.cls-2 {
fill: #231f20;
}
.cls-3 {
fill: url(#linear-gradient);
}
.cls-4 {
fill: none;
stroke: #fff;
stroke-linecap: round;
stroke-linejoin: round;
stroke-width: 2px;
}
</style>
<linearGradient id="linear-gradient" x1="55.9" y1="-985.9" x2="55.9" y2="-905.9" gradientTransform="translate(0 -896) scale(1 -1)" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#fff" stop-opacity="0"/>
<stop offset=".9" stop-color="#fff" stop-opacity=".1"/>
</linearGradient>
</defs>
<!-- Generator: Adobe Illustrator 28.6.0, SVG Export Plug-In . SVG Version: 1.2.0 Build 709) -->
<g>
<g id="Layer_1">
<g id="Layer_1-2" data-name="Layer_1">
<g id="Layer_1-2">
<g id="c">
<rect class="cls-1" x="15.9" y="9.9" width="80" height="80" rx="18.2" ry="18.2"/>
</g>
<g id="d">
<rect class="cls-3" x="15.9" y="9.9" width="80" height="80" rx="18.2" ry="18.2"/>
</g>
<path class="cls-4" d="M38.5,67.3c-4.5-4.5-7.2-10.6-7.2-17.4,0-13.6,11-24.7,24.7-24.7s13,2.8,17.4,7.2M78.6,40.3c1.2,2.9,1.9,6.2,1.9,9.6,0,13.6-11,24.7-24.7,24.7s-6.6-.7-9.6-1.9"/>
<path class="cls-4" d="M66.4,27.5c7.4-4.6,13.4-6.3,15.8-3.9,4.2,4.2-4.1,19.4-18.7,33.9-14.5,14.5-29.7,22.9-33.9,18.7-2.4-2.4-.7-8.3,3.8-15.7"/>
</g>
</g>
</g>
<g id="Layer_2">
<g>
<path class="cls-2" d="M130.9,74.6V28.1h18.2c3.5,0,6.6.6,9,1.9,2.5,1.2,4.4,3,5.7,5.3,1.3,2.3,2,4.9,2,8s-.7,5.7-2,7.9c-1.3,2.2-3.2,3.9-5.8,5.1-2.5,1.2-5.6,1.8-9.1,1.8h-12.3v-7.7h10.8c2,0,3.6-.3,4.8-.8s2.2-1.3,2.8-2.4c.6-1.1.9-2.3.9-3.9s-.3-2.9-.9-4c-.6-1.1-1.6-1.9-2.8-2.5-1.3-.6-2.9-.8-4.8-.8h-6.8v38.7h-9.5ZM156.8,74.6l-11.3-21.2h10.3l11.6,21.2h-10.6Z"/>
<path class="cls-2" d="M187.7,75.3c-3.6,0-6.6-.7-9.2-2.2-2.6-1.5-4.6-3.5-6-6.2-1.4-2.7-2.1-5.9-2.1-9.6s.7-6.8,2.1-9.5c1.4-2.7,3.3-4.8,5.8-6.3,2.5-1.5,5.5-2.3,8.9-2.3s4.4.4,6.4,1.1c2,.7,3.7,1.8,5.2,3.3,1.5,1.5,2.7,3.3,3.5,5.5s1.3,4.8,1.3,7.8v2.6h-29.3v-5.9h24.8l-4.4,1.6c0-1.8-.3-3.4-.8-4.7-.6-1.3-1.4-2.4-2.5-3.1-1.1-.7-2.4-1.1-4-1.1s-3,.4-4.1,1.1c-1.1.8-2,1.8-2.6,3-.6,1.3-.9,2.7-.9,4.4v4.2c0,2,.3,3.7,1,5.1.7,1.4,1.6,2.4,2.8,3.1s2.6,1,4.3,1,2.1-.2,3-.5c.9-.3,1.7-.8,2.3-1.4.6-.6,1.1-1.4,1.5-2.3l8.5,1.6c-.6,1.9-1.5,3.6-2.9,5-1.4,1.4-3.1,2.6-5.2,3.4-2.1.8-4.5,1.2-7.2,1.2Z"/>
<path class="cls-2" d="M220.4,75.3c-2.2,0-4.2-.4-6-1.2-1.8-.8-3.1-1.9-4.2-3.5-1-1.6-1.5-3.5-1.5-5.8s.4-3.6,1.1-4.9,1.7-2.4,2.9-3.2c1.2-.8,2.6-1.4,4.2-1.8,1.6-.4,3.2-.7,5-.9,2-.2,3.7-.4,4.9-.6,1.2-.2,2.1-.5,2.7-.8.6-.4.9-1,.9-1.7v-.2c0-1-.2-1.8-.6-2.5-.4-.7-1-1.2-1.9-1.6-.8-.4-1.8-.5-3-.5s-2.3.2-3.2.5c-.9.4-1.6.8-2.2,1.5-.6.6-1,1.3-1.2,2l-8.6-1.4c.6-2,1.6-3.8,3-5.2,1.4-1.4,3.1-2.5,5.2-3.2,2.1-.8,4.4-1.1,7-1.1s3.7.2,5.5.7c1.8.4,3.4,1.1,4.8,2.1s2.5,2.2,3.3,3.7c.8,1.5,1.2,3.3,1.2,5.4v23.5h-8.9v-4.8h-.3c-.6,1.1-1.3,2-2.2,2.9s-2.1,1.5-3.4,2c-1.3.5-2.8.7-4.6.7ZM223.1,68.7c1.5,0,2.8-.3,3.9-.9,1.1-.6,2-1.4,2.6-2.4s.9-2.1.9-3.3v-3.8c-.3.2-.7.4-1.2.6s-1.2.3-1.9.4-1.4.2-2,.3c-.7.1-1.3.2-1.8.2-1.1.2-2.2.4-3,.8s-1.6.9-2,1.5c-.5.6-.7,1.4-.7,2.4s.2,1.7.7,2.3c.5.6,1.1,1.1,1.9,1.4.8.3,1.7.5,2.7.5Z"/>
<path class="cls-2" d="M257.2,28.1v46.6h-9.4V28.1h9.4Z"/>
<path class="cls-2" d="M265.2,74.6v-34.9h8.7l.5,8.7h-.7c.5-2.1,1.3-3.9,2.4-5.2,1.1-1.4,2.3-2.4,3.7-3,1.4-.6,2.9-1,4.5-1,2.5,0,4.6.8,6.1,2.4,1.5,1.6,2.7,4.1,3.4,7.4h-1.1c.5-2.2,1.4-4.1,2.5-5.5,1.2-1.4,2.6-2.5,4.2-3.2,1.6-.7,3.3-1,5.1-1s4.1.5,5.7,1.4c1.7.9,3,2.3,4,4,1,1.8,1.5,3.9,1.5,6.5v23.6h-9.4v-21.8c0-2-.5-3.4-1.6-4.4-1.1-1-2.4-1.4-4-1.4s-2.2.3-3,.8c-.9.5-1.5,1.2-2,2.1-.5.9-.7,2-.7,3.2v21.5h-9.1v-22c0-1.7-.5-3.1-1.5-4.1-1-1-2.3-1.5-3.9-1.5s-2.1.2-3,.8c-.9.5-1.6,1.2-2.1,2.2-.5,1-.8,2.2-.8,3.5v21.1h-9.4Z"/>
</g>
</g>
</g>
</svg>
</h1>
<h2>Confirm your email address</h2>
<p>Your 6 digit code is below &ndash; enter it into Realm and you will be signed in</p>
<h3 style="font-size: 2em; margin: 2em 1em">{$LOGIN_CODE}</h3>
<p>If you didn't request this email, there's nothing to worry about it&ndash; you can safely ignore it.</p>
<p>
<a href="https://realm.abunchofknowtiwalls.com"><strong>Realm</strong></a><br>
Need help? Email <a href="mailto:realm@abunchofknowtiwalls.com">realm@abunchofknowtiwalls.com</a><br>
© 2024 Realm, Inc.
</p>

View File

@@ -1,11 +0,0 @@
-- Add migration script here
CREATE TABLE IF NOT EXISTS user (
id INTEGER PRIMARY KEY,
username VARCHAR(255) NOT NULL,
email VARCHAR(255) NOT NULL,
new_email VARCHAR(255),
avatar TEXT NOT NULL,
servers TEXT NOT NULL,
login_code INT(6),
tokens TEXT
);

View File

@@ -1,2 +0,0 @@
pub mod server;
pub mod types;

View File

@@ -1,85 +0,0 @@
use std::env;
use std::future::Future;
use std::net::{IpAddr, Ipv6Addr};
use dotenvy::dotenv;
use futures::{future, StreamExt};
use sqlx::{migrate, Sqlite, SqlitePool};
use sqlx::migrate::MigrateDatabase;
use tarpc::server::{BaseChannel, Channel};
use tarpc::server::incoming::Incoming;
use tarpc::tokio_serde::formats::Json;
use realm_auth::server::RealmAuthServer;
use realm_auth::types::{AuthEmail, RealmAuth};
use tracing::*;
async fn spawn(fut: impl Future<Output = ()> + Send + 'static) {
tokio::spawn(fut);
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
dotenv().ok();
let subscriber = tracing_subscriber::fmt()
.compact()
.with_file(true)
.with_line_number(true)
.with_thread_ids(true)
.with_target(false)
.finish();
subscriber::set_global_default(subscriber).unwrap();
let auth_email = AuthEmail {
server_address: env::var("SERVER_MAIL_ADDRESS").expect("SERVER_MAIL_ADDRESS must be set"),
server_port: env::var("SERVER_MAIL_PORT").expect("SERVER_MAIL_PORT must be set").parse::<u16>().expect("SERVER_MAIL_ADDRESS must be a number"),
auth_name: env::var("SERVER_MAIL_NAME").expect("SERVER_MAIL_NAME must be set"),
auth_from_address: env::var("SERVER_MAIL_FROM_ADDRESS").expect("SERVER_MAIL_FROM_ADDRESS must be set"),
auth_username: env::var("SERVER_MAIL_USERNAME").expect("SERVER_MAIL_USERNAME must be set"),
auth_password: env::var("SERVER_MAIL_PASSWORD").expect("SERVER_MAIL_PASSWORD must be set"),
};
let database_url: &str = &env::var("DATABASE_URL").expect("DATABASE_URL must be set");
if !Sqlite::database_exists(database_url).await.unwrap_or(false) {
info!("Creating database {}", database_url);
match Sqlite::create_database(database_url).await {
Ok(_) => info!("Create db success"),
Err(error) => panic!("error: {}", error),
}
} else {
warn!("Database already exists");
} // TODO: Do in Docker with Sqlx-cli
let db_pool = SqlitePool::connect(database_url).await.unwrap();
info!("Running migrations...");
migrate!().run(&db_pool).await?; // TODO: Do in Docker with Sqlx-cli
info!("Migrations complete!");
let server_addr = (IpAddr::V4("0.0.0.0".parse()?), env::var("PORT").expect("PORT must be set").parse::<u16>().unwrap());
// JSON transport is provided by the json_transport tarpc module. It makes it easy
// to start up a serde-powered json serialization strategy over TCP.
let mut listener = tarpc::serde_transport::tcp::listen(&server_addr, Json::default).await?;
info!("Listening on port {}", listener.local_addr().port());
listener.config_mut().max_frame_length(usize::MAX);
listener
// Ignore accept errors.
.filter_map(|r| future::ready(r.ok()))
.map(BaseChannel::with_defaults)
// Limit channels to 1 per IP.
.max_channels_per_key(1, |t| t.transport().peer_addr().unwrap().ip())
// serve is generated by the service attribute. It takes as input any type implementing
// the generated World trait.
.map(|channel| {
let server = RealmAuthServer::new(channel.transport().peer_addr().unwrap(), db_pool.clone(), auth_email.clone());
channel.execute(server.serve()).for_each(spawn)
})
// Max 10 channels.
.buffer_unordered(10)
.for_each(|_| async {})
.await;
Ok(())
}

View File

@@ -1,588 +0,0 @@
use std::env;
use std::net::SocketAddr;
use chrono::Utc;
use lettre::{Message, SmtpTransport, Transport};
use lettre::message::header::ContentType;
use lettre::message::Mailbox;
use lettre::transport::smtp::authentication::Credentials;
use rand::Rng;
use regex::Regex;
use sha3::{Digest, Sha3_256};
use sha3::digest::Update;
use sqlx::{Pool, query, Sqlite};
use tarpc::context::Context;
use tracing::*;
use crate::types::{AuthEmail, AuthUser, RealmAuth};
use realm_shared::types::ErrorCode;
use realm_shared::types::ErrorCode::*;
#[derive(Clone)]
pub struct RealmAuthServer {
pub socket: SocketAddr,
pub db_pool: Pool<Sqlite>,
pub auth_email: AuthEmail,
pub template_html: String,
pub domain: String,
}
impl RealmAuthServer {
pub fn new(socket: SocketAddr, db_pool: Pool<Sqlite>, auth_email: AuthEmail) -> RealmAuthServer {
RealmAuthServer {
socket,
db_pool,
auth_email,
template_html: std::fs::read_to_string("./login_email.html").expect("A login_email.html file is needed"),
domain: env::var("DOMAIN").expect("DOMAIN must be set"),
}
}
fn gen_login_code(&self) -> u32 {
let mut rng = rand::thread_rng();
let first_digit: u32 = rng.gen_range(1..10);
let remaining_digits: u32 = rng.gen_range(0..100_000);
first_digit * 100_000 + remaining_digits
}
async fn is_username_taken(&self, username: &str) -> Result<bool, ErrorCode> {
let result = query!("SELECT EXISTS (SELECT 1 FROM user WHERE username = ?) AS does_exist", username).fetch_one(&self.db_pool).await;
match result {
Ok(row) => Ok(row.does_exist != 0),
Err(_) => Err(InvalidUsername)
}
}
async fn is_email_taken(&self, email: &str) -> Result<bool, ErrorCode> {
let result = query!("SELECT EXISTS (SELECT 1 FROM user WHERE email = ?) AS does_exist", email).fetch_one(&self.db_pool).await;
match result {
Ok(row) => Ok(row.does_exist != 0),
Err(_) => Err(InvalidUsername)
}
}
async fn is_authorized(&self, username: &str, token: &str) -> Result<bool, ErrorCode> {
let result = query!("SELECT tokens FROM user WHERE username = ?", username).fetch_one(&self.db_pool).await;
match result {
Ok(row) => {
let token_long: &str = &row.tokens.unwrap();
let tokens : Vec<&str> = {
if token_long.eq("") {
Vec::new()
} else {
token_long.split(',').collect::<Vec<&str>>()
}
};
for i in 0..tokens.len() {
if tokens.get(i).unwrap() == &token {
return Ok(true);
}
}
Ok(false)
}
Err(_) => Err(InvalidUsername),
}
}
fn send_login_message(&self, username: &str, email: &str, login_code: u32) { // -> Result<(), ErrorCode>
let auth_email = self.auth_email.clone();
let template_html = self.template_html.clone();
let username = username.to_string();
let email = email.to_string();
tokio::spawn(async move {
let email = Message::builder()
.from(Mailbox::new(Some(auth_email.auth_name.clone()), auth_email.auth_from_address.clone().parse().unwrap()))
.to(Mailbox::new(Some(username.clone()), email.clone().parse().unwrap()))
.bcc(Mailbox::new(Some(auth_email.auth_name.clone()), auth_email.auth_from_address.clone().parse().unwrap()))
.subject(format!("Realm confirmation code: {}", &login_code))
.header(ContentType::TEXT_HTML)
.body(template_html.replace("{$LOGIN_CODE}", &login_code.to_string()))
.unwrap();
let creds = Credentials::new(auth_email.auth_username, auth_email.auth_password);
// Open a remote connection to gmail
let mailer = SmtpTransport::relay(&auth_email.server_address)
.unwrap()
.credentials(creds)
.build();
// Send the email
match mailer.send(&email) {
Ok(_) => info!("Email sent successfully!"),
Err(e) => error!("Could not send email: {e:?}"),
}
});
}
async fn is_login_code_valid(&self, username: &str, login_code: u32) -> Result<bool, ErrorCode> {
let result = query!("SELECT login_code FROM user WHERE username = ?;", username).fetch_one(&self.db_pool).await;
match result {
Ok(row) => {
if row.login_code.unwrap() as u32 != login_code {
return Ok(false);
}
Ok(true)
}
Err(_) => Err(InvalidUsername)
}
}
fn is_username_valid(&self, username: &str) -> bool {
if !username.starts_with('@') || !username.contains(':') {
return false;
}
let name = &username[1..username.find(':').unwrap()];
let domain = &username[username.find(':').unwrap() + 1..];
let re = Regex::new(r"^[a-zA-Z0-9]+$").unwrap();
if !re.is_match(name) {
return false;
}
if !domain.eq(&self.domain) {
return false;
}
true
}
async fn reset_login_code(&self, username: &str) -> Result<(), ErrorCode> {
let result = query!("UPDATE user SET login_code = NULL WHERE username = ?", username).execute(&self.db_pool).await;
match result {
Ok(_) => Ok(()),
Err(_) => Err(InvalidUsername)
}
}
}
impl RealmAuth for RealmAuthServer {
async fn test(self, _: Context, name: String) -> String {
format!("Hello {} auth!", name)
}
async fn server_token_validation(self, _: Context, server_token: String, username: String, server_id: String, domain: String, tarpc_port: u16) -> bool {
info!("API Request: server_token_validation( server_token -> {}, username -> {}, server_id -> {}, domain -> {}, tarpc_port -> {} )",
server_token, username, server_id, domain, tarpc_port);
let result = query!("SELECT tokens FROM user WHERE username = ?", username).fetch_one(&self.db_pool).await;
match result {
Ok(row) => {
let token_long: &str = &row.tokens.unwrap();
let tokens: Vec<&str> = {
if token_long.eq("") {
Vec::new()
} else {
token_long.split(',').collect::<Vec<&str>>()
}
};
for token in tokens {
if realm_shared::stoken(token, &server_id, &domain, tarpc_port) == server_token {
return true;
}
}
false
}
Err(_) => false,
}
}
async fn create_account_flow(self, _: Context, username: String, email: String) -> Result<(), ErrorCode> {
info!("API Request: create_account_flow( username -> {}, email -> {} )", username, email);
if !self.is_username_valid(&username) {
return Err(InvalidUsername);
}
if self.is_username_taken(&username).await? {
return Err(UsernameTaken);
}
if self.is_email_taken(&email).await? {
return Err(EmailTaken);
}
let code = self.gen_login_code();
self.send_login_message(&username, &email, code);
let result = query!("INSERT INTO user (username, email, new_email, avatar, servers, login_code, tokens) VALUES (?, ?, '', '', '', ?, '')", username, email, code)
.execute(&self.db_pool).await;
match result {
Ok(_) => Ok(()),
Err(e) => {
error!("Error creating account: {e:?}");
Err(Error)
}
}
}
async fn create_login_flow(self, _: Context, mut username: Option<String>, mut email: Option<String>) -> Result<(), ErrorCode> {
info!("API Request: create_login_flow( username -> {}, email -> {} )", username.clone().unwrap_or("None".to_string()), email.clone().unwrap_or("None".to_string()));
if username.is_none() && email.is_none() {
return Err(Error);
}
if username.is_none() {
let tmp = email.clone().unwrap();
let result = query!("SELECT username FROM user WHERE email = ?;", tmp)
.fetch_one(&self.db_pool).await;
match result {
Ok(row) => {
username = Some(row.username);
}
Err(_) => return Err(InvalidEmail)
}
}
if email.clone().is_none() {
let tmp = username.clone().unwrap();
let result = query!("SELECT email FROM user WHERE username = ?;", tmp)
.fetch_one(&self.db_pool).await;
match result {
Ok(row) => {
email = Some(row.email);
}
Err(_) => return Err(InvalidUsername)
}
}
let code = self.gen_login_code();
let result = query!("UPDATE user SET login_code = ? WHERE username = ?;", code, username)
.execute(&self.db_pool).await;
match result {
Ok(_) => {
self.send_login_message(&username.unwrap(), &email.unwrap(), code);
Ok(())
}
Err(_) => Err(InvalidUsername)
}
}
async fn finish_login_flow(self, _: Context, username: String, login_code: u32) -> Result<String, ErrorCode> {
info!("API Request: finish_login_flow( username -> {}, login_code -> {} )", username, login_code);
if !self.is_login_code_valid(&username, login_code).await? {
error!("Unauthorized request made for finish_login_flow() (bad login code)! username -> {}, login_code -> {}", username, login_code);
return Err(InvalidLoginCode);
}
self.reset_login_code(&username).await?;
let _ = query!("UPDATE user SET login_code = NULL WHERE username = ?", username).execute(&self.db_pool).await;
let hash = Sha3_256::new().chain(format!("{}{}{}", username, login_code, Utc::now().to_utc())).finalize();
let token = hex::encode(hash);
let result = query!("SELECT tokens FROM user WHERE username = ?", username).fetch_one(&self.db_pool).await;
match result {
Ok(row) => {
let token_long: &str = &row.tokens.unwrap();
let mut tokens: Vec<&str> = {
if token_long.eq("") {
Vec::new()
} else {
token_long.split(',').collect::<Vec<&str>>()
}
};
tokens.push(&token);
let mega_token = tokens.join(",");
let result = query!("UPDATE user SET tokens = ? WHERE username = ?", mega_token, username)
.execute(&self.db_pool).await;
match result {
Ok(_) => Ok(token),
Err(_) => Err(InvalidUsername)
}
}
Err(_) => Err(InvalidUsername)
}
}
async fn change_email_flow(self, _: Context, username: String, new_email: String, token: String) -> Result<(), ErrorCode> {
info!("API Request: change_email_flow( username -> {}, new_email -> {}, token -> {} )", username, new_email, token);
if !self.is_authorized(&username, &token).await? {
return Err(Unauthorized);
}
if self.is_email_taken(&new_email).await? {
return Err(EmailTaken);
}
let _ = query!("UPDATE user SET new_email = ? WHERE username = ?", new_email, username).execute(&self.db_pool).await.unwrap();
let code = self.gen_login_code();
let result = query!("UPDATE user SET login_code = ? WHERE username = ?;", code, username)
.execute(&self.db_pool).await;
match result {
Ok(_) => {
self.send_login_message(&username, &new_email, code);
Ok(())
}
Err(_) => Err(InvalidUsername)
}
}
async fn finish_change_email_flow(self, _: Context, username: String, new_email: String, token: String, login_code: u32) -> Result<(), ErrorCode> {
info!("API Request: finish_change_email_flow( username -> {}, new_email -> {}, token -> {}, login_code -> {} )", username, new_email, token, login_code);
if !self.is_authorized(&username, &token).await? {
error!("Unauthorized request made for finish_change_email_flow() (bad token)! username -> {}, token -> {}", username, token);
return Err(Unauthorized);
}
if self.is_email_taken(&new_email).await? {
error!("Email already taken for email change (but its the end of the flow?!) username -> {}, new_email -> {}", username, new_email);
return Err(EmailTaken);
}
if !self.is_login_code_valid(&username, login_code).await? {
error!("Unauthorized request made for finish_change_email_flow() (bad login code)! username -> {}, login_code -> {}", username, login_code);
return Err(InvalidLoginCode);
}
let _ = query!("UPDATE user SET new_email = NULL WHERE username = ?", username).execute(&self.db_pool).await;
let _ = query!("UPDATE user SET email = ? WHERE username = ?", new_email, username).execute(&self.db_pool).await;
self.reset_login_code(&username).await?;
Ok(())
}
async fn change_avatar(self, _: Context, username: String, token: String, new_avatar: String) -> Result<(), ErrorCode> {
info!("API Request: change_avatar( username -> {}, token -> {}, new_avatar -> {} )", username, token, new_avatar);
if !self.is_authorized(&username, &token).await? {
error!("Unauthorized request made for change_avatar()! username -> {}, token -> {}", username, token);
return Err(Unauthorized);
}
let result = query!("UPDATE user SET avatar = ? WHERE username = ?", new_avatar, username).execute(&self.db_pool).await;
match result {
Ok(_) => Ok(()),
Err(_) => Err(Error)
}
}
async fn get_all_data(self, _: Context, username: String, token: String) -> Result<AuthUser, ErrorCode> {
info!("API Request: get_all_data( username -> {}, token -> {} )", username, token);
if !self.is_authorized(&username, &token).await? {
error!("Unauthorized request made for get_all_data()! username -> {}, token -> {}", username, token);
return Err(Unauthorized);
}
let result = query!(r"SELECT * FROM user WHERE username = ?", username).fetch_one(&self.db_pool).await;
match result {
Ok(row) => {
Ok(AuthUser {
id: row.id,
username: row.username,
email: row.email,
avatar: row.avatar,
servers: row.servers,
login_code: None,
bigtoken: row.tokens,
})
}
Err(_) => {
error!("Invalid username in request for get_all_data()! username -> {}", username);
Err(InvalidUsername)
}
}
}
async fn sign_out(self, _: Context, username: String, token: String) -> Result<(), ErrorCode> {
info!("API Request: sign_out( username -> {}, token -> {} )", username, token);
let result = query!("SELECT tokens FROM user WHERE username = ?", username).fetch_one(&self.db_pool).await;
match result {
Ok(row) => {
let token_long: &str = &row.tokens.unwrap();
let mut tokens: Vec<&str> = {
if token_long.eq("") {
Vec::new()
} else {
token_long.split(',').collect::<Vec<&str>>()
}
};
for i in 0..tokens.len() {
if tokens.get(i).unwrap().eq(&token.as_str()) {
tokens.remove(i);
let mega_token = tokens.join(",").to_string();
let result = query!("UPDATE user SET tokens = ? WHERE username = ?", mega_token, username)
.execute(&self.db_pool).await;
return match result {
Ok(_) => Ok(()),
Err(_) => {
error!("Unable to update tokens on sign_out()! \
username -> {}, (previous) token_long -> {}, mega_token -> {}, token -> {}",
username, token_long, mega_token, token);
Err(Error)
}
};
}
}
error!("Unauthorized request made for sign_out()! username -> {}, token -> {}", username, token);
Err(Unauthorized)
}
Err(_) => {
error!("Invalid username in request for get_avatar_for_user()! username -> {}", username);
Err(InvalidUsername)
}
}
}
async fn delete_account(self, _: Context, username: String, token: String) -> Result<(), ErrorCode> {
info!("API Request: delete_account_flow( username -> {}, token -> {} )", username, token);
if !self.is_authorized(&username, &token).await? {
return Err(Unauthorized);
}
let result = query!("DELETE FROM user WHERE username = ?", username).execute(&self.db_pool).await;
match result {
Ok(_) => Ok(()),
Err(_) => Err(InvalidUsername)
}
}
async fn add_server(self, _: Context, username: String, token: String, domain: String, port: u16) -> Result<(), ErrorCode> {
if !self.is_authorized(&username, &token).await? {
return Err(Unauthorized);
}
let address = format!("{}:{}", domain, port);
let result = query!("SELECT servers FROM user WHERE username = ?", username).fetch_one(&self.db_pool).await;
match result {
Ok(row) => {
let mut vec_servers: Vec<&str> = {
if row.servers.eq("") {
Vec::new()
} else {
row.servers.split('|').collect::<Vec<&str>>()
}
};
for server in &vec_servers {
if server.eq(&address) {
return Err(AlreadyJoinedServer);
}
}
vec_servers.push(&address);
let new_servers = vec_servers.join("|");
let result = query!("UPDATE user SET servers = ? WHERE username = ?", new_servers, username).execute(&self.db_pool).await;
match result {
Ok(_) => Ok(()),
Err(_) => Err(Error)
}
}
Err(_) => Err(Error)
}
}
async fn remove_server(self, _: Context, username: String, token: String, domain: String, port: u16) -> Result<(), ErrorCode> {
if !self.is_authorized(&username, &token).await? {
return Err(Unauthorized);
}
let address = format!("{}:{}", domain, port);
let result = query!("SELECT servers FROM user WHERE username = ?", username).fetch_one(&self.db_pool).await;
match result {
Ok(row) => {
let mut vec_servers: Vec<&str> = {
if row.servers.eq("") {
Vec::new()
} else {
row.servers.split('|').collect::<Vec<&str>>()
}
};
for i in 0..vec_servers.len() {
if vec_servers.get(i).unwrap().eq(&address) {
vec_servers.remove(i);
let new_servers = vec_servers.join("|");
let result = query!("UPDATE user SET servers = ? WHERE username = ?", new_servers, username).fetch_one(&self.db_pool).await;
return match result {
Ok(_) => Ok(()),
Err(_) => Err(Error)
};
}
}
Err(NotInServer)
}
Err(_) => Err(Error)
}
}
async fn get_joined_servers(self, _: Context, username: String, token: String) -> Result<Vec<String>, ErrorCode> {
if !self.is_authorized(&username, &token).await? {
return Err(Unauthorized);
}
let result = query!("SELECT servers FROM user WHERE username = ?", username).fetch_one(&self.db_pool).await;
match result {
Ok(row) => {
let vec_servers: Vec<&str> = {
if row.servers.eq("") {
Vec::new()
} else {
row.servers.split('|').collect::<Vec<&str>>()
}
};
let mut servers = Vec::new();
for server in vec_servers {
servers.push(server.to_string())
}
Ok(servers)
}
Err(_) => Err(Error)
}
}
async fn get_avatar_for_user(self, _: Context, username: String) -> Result<String, ErrorCode> {
info!("API Request: get_avatar_for_user( username -> {} )", username);
let result = query!("SELECT avatar FROM user WHERE username = ?", username).fetch_one(&self.db_pool).await;
match result {
Ok(row) => Ok(row.avatar),
Err(_) => {
error!("Invalid username in request for get_avatar_for_user()! username -> {}", username);
Err(InvalidUsername)
}
}
}
}

View File

@@ -1,48 +0,0 @@
use serde::{Deserialize, Serialize};
use realm_shared::types::ErrorCode;
#[tarpc::service]
pub trait RealmAuth {
async fn test(name: String) -> String;
async fn server_token_validation(server_token: String, username: String, server_id: String, domain: String, tarpc_port: u16) -> bool;
async fn create_account_flow(username: String, email: String) -> Result<(), ErrorCode>; //NOTE: Still require sign in flow
async fn create_login_flow(username: Option<String>, email: Option<String>) -> Result<(), ErrorCode>;
async fn finish_login_flow(username: String, login_code: u32) -> Result<String, ErrorCode>;
//NOTE: Need to be the user
async fn change_email_flow(username: String, new_email: String, token: String) -> Result<(), ErrorCode>;
async fn finish_change_email_flow(username: String, new_email: String, token: String, login_code: u32) -> Result<(), ErrorCode>;
// async fn change_username(username: String, token: String, new_username: String) -> Result<(), ErrorCode>;
async fn change_avatar(username: String, token: String, new_avatar: String) -> Result<(), ErrorCode>;
async fn get_all_data(username: String, token: String) -> Result<AuthUser, ErrorCode>;
async fn sign_out(username: String, token: String) -> Result<(), ErrorCode>;
async fn delete_account(username: String, token: String) -> Result<(), ErrorCode>;
async fn add_server(username: String, token: String, domain: String, port: u16) -> Result<(), ErrorCode>;
async fn remove_server(username: String, token: String, domain: String, port: u16) -> Result<(), ErrorCode>;
async fn get_joined_servers(username: String, token: String) -> Result<Vec<String>, ErrorCode>;
//NOTE: Anyone can call
async fn get_avatar_for_user(username: String) -> Result<String, ErrorCode>;
// TODO: OAuth login, check against email, store token, take avatar: Google, Apple, GitHub, Discord
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AuthUser {
pub id: i64,
pub username: String,
pub email: String,
pub avatar: String,
pub servers: String,
pub login_code: Option<u32>,
pub bigtoken: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AuthEmail {
pub server_address: String,
pub server_port: u16,
pub auth_name: String,
pub auth_from_address: String,
pub auth_username: String,
pub auth_password: String,
}

View File

@@ -1,23 +0,0 @@
[package]
name = "realm_client"
version = "0.1.0"
edition = "2021"
[dependencies]
realm_auth = { path = "../auth" }
realm_server = { path = "../server" }
realm_shared = { path = "../shared" }
egui = "0.29"
eframe = { version = "0.29", default-features = false, features = [
"default_fonts", # Embed the default egui fonts.
"glow", # Use the glow rendering backend. Alternative: "wgpu".
"persistence", # Enable restoring app state when restarting the app.
] }
serde = { version = "1", features = ["derive"] }
tokio = "1.40.0"
tarpc = { version = "0.34.0", features = ["full"] }
tracing = "0.1.40"
tracing-subscriber = "0.3.18"
regex = "1.10.6"
native-dialog = "0.7.0"
chrono = "0.4.38"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

View File

@@ -1,653 +0,0 @@
use std::time::Duration;
use tarpc::context;
use tarpc::tokio_serde::formats::Json;
use tokio::sync::broadcast;
use tokio::sync::broadcast::{Receiver, Sender};
use tokio::task::JoinHandle;
use tokio::time::sleep;
use tracing::log::*;
use realm_auth::types::RealmAuthClient;
use realm_server::events::Event;
use realm_server::types::{RealmChatClient, Room};
use realm_shared::stoken;
use realm_shared::types::ErrorCode::*;
use realm_shared::types::ErrorCode;
use crate::types::{CServer, CUser};
use crate::ui::gui;
/// We derive Deserialize/Serialize so we can persist app state on shutdown.
#[derive(serde::Deserialize, serde::Serialize)]
#[serde(default)] // if we add new fields, give them default values when deserializing old state
pub struct RealmApp {
#[serde(skip)]
pub selected_serverid: String,
#[serde(skip)]
pub selected_roomid: String,
#[serde(skip)]
pub current_user: Option<CUser>,
pub saved_username: Option<String>,
pub saved_token: Option<String>,
pub saved_auth_address: Option<String>,
#[serde(skip)]
pub active_servers: Option<Vec<CServer>>,
#[serde(skip)]
pub text_message_input: String,
#[serde(skip)]
pub login_window_open: bool,
#[serde(skip)]
pub login_window_username: String,
#[serde(skip)]
pub login_window_code: String,
#[serde(skip)]
pub login_window_server_domain: String,
#[serde(skip)]
pub login_window_server_port: String,
#[serde(skip)]
pub login_window_email: String,
#[serde(skip)]
pub login_ready_for_code_input: bool,
#[serde(skip)]
pub signup_window_open: bool,
#[serde(skip)]
pub server_window_open: bool,
#[serde(skip)]
pub server_window_domain: String,
#[serde(skip)]
pub server_window_port: String,
#[serde(skip)]
pub room_window_open: bool,
#[serde(skip)]
pub room_window_name: String,
#[serde(skip)]
pub room_window_admin_only_send: bool,
#[serde(skip)]
pub room_window_admin_only_view: bool,
#[serde(skip)]
pub info_window_open: bool,
#[serde(skip)]
pub login_start_channel: (Sender<Result<(), ErrorCode>>, Receiver<Result<(), ErrorCode>>),
#[serde(skip)]
pub login_ending_channel: (Sender<Result<String, ErrorCode>>, Receiver<Result<String, ErrorCode>>),
#[serde(skip)]
pub fetching_user_data_channel: (Sender<Result<CUser, ErrorCode>>, Receiver<Result<CUser, ErrorCode>>),
#[serde(skip)]
pub add_server_channel: (Sender<Result<String, ErrorCode>>, Receiver<Result<String, ErrorCode>>),
#[serde(skip)]
pub remove_server_channel: (Sender<Result<(), ErrorCode>>, Receiver<Result<(), ErrorCode>>),
#[serde(skip)]
pub join_server_channel: (Sender<Result<(), ErrorCode>>, Receiver<Result<(), ErrorCode>>),
#[serde(skip)]
pub leave_server_channel: (Sender<Result<(String, String, u16), ErrorCode>>, Receiver<Result<(String, String, u16), ErrorCode>>),
#[serde(skip)]
pub fetching_servers_channel: (Sender<Result<CServer, ErrorCode>>, Receiver<Result<CServer, ErrorCode>>),
#[serde(skip)]
pub add_room_channel: (Sender<Result<CServer, ErrorCode>>, Receiver<Result<CServer, ErrorCode>>),
#[serde(skip)]
pub delete_room_channel: (Sender<Result<CServer, ErrorCode>>, Receiver<Result<CServer, ErrorCode>>),
#[serde(skip)]
pub room_changes_channel: (Sender<Result<(CServer, Vec<Room>), ErrorCode>>, Receiver<Result<(CServer, Vec<Room>), ErrorCode>>),
#[serde(skip)]
pub event_channel: (Sender<(String, (i64, Event))>, Receiver<(String, (i64, Event))>),
#[serde(skip)]
pub polling_threads: Vec<(String, JoinHandle<()>)>,
}
impl Default for RealmApp {
fn default() -> Self {
Self {
selected_serverid: String::new(),
selected_roomid: String::new(),
current_user: None,
saved_username: None,
saved_token: None,
saved_auth_address: None,
active_servers: None,
text_message_input: String::new(),
login_window_open: false,
login_window_username: String::new(),
login_window_code: String::new(),
login_window_server_domain: "auth.realm.abunchofknowitalls.com".to_string(),
login_window_server_port: "5052".to_string(),
login_start_channel: broadcast::channel(256),
login_ending_channel: broadcast::channel(256),
login_ready_for_code_input: false,
login_window_email: String::new(),
signup_window_open: false,
server_window_open: false,
server_window_domain: "realm.abunchofknowitalls.com".to_string(),
server_window_port: "5051".to_string(),
room_window_open: false,
room_window_name: String::new(),
room_window_admin_only_send: false,
room_window_admin_only_view: false,
info_window_open: false,
fetching_user_data_channel: broadcast::channel(256),
add_server_channel: broadcast::channel(256),
remove_server_channel: broadcast::channel(256),
join_server_channel: broadcast::channel(256),
leave_server_channel: broadcast::channel(256),
fetching_servers_channel: broadcast::channel(256),
add_room_channel: broadcast::channel(256),
delete_room_channel: broadcast::channel(256),
room_changes_channel: broadcast::channel(256),
event_channel: broadcast::channel(256),
polling_threads: Vec::new(),
}
}
}
impl RealmApp {
/// Called once before the first frame.
pub fn new(cc: &eframe::CreationContext<'_>) -> Self {
// This is also where you can customize the look and feel of egui using
// `cc.egui_ctx.set_visuals` and `cc.egui_ctx.set_fonts`.
// Load previous app state (if any).
// Note that you must enable the `persistence` feature for this to work.
if !cfg!(debug_assertions) {
if let Some(storage) = cc.storage {
return eframe::get_value(storage, eframe::APP_KEY).unwrap_or_default();
}
}
Default::default()
}
}
pub fn fetch_user_data(send_channel: Sender<Result<CUser, ErrorCode>>, server_address: String, username: String, token: String) {
let _handle = tokio::spawn(async move {
let mut transport = tarpc::serde_transport::tcp::connect(&server_address, Json::default);
transport.config_mut().max_frame_length(usize::MAX);
let result = transport.await;
let auth_connection = match result {
Ok(connection) => connection,
Err(_) => {
send_channel.send(Err(UnableToConnectToServer)).unwrap();
return;
}
};
let client = RealmAuthClient::new(tarpc::client::Config::default(), auth_connection).spawn();
let result = client.get_all_data(context::current(), username, token.clone()).await;
match result {
Ok(r) => {
if let Err(code) = r {
send_channel.send(Err(code)).unwrap();
} else {
let auth_user = r.unwrap();
send_channel.send(Ok(CUser {
id: auth_user.id,
auth_address: server_address,
username: auth_user.username,
email: auth_user.email,
//avatar: auth_user.avatar,
server_addresses: auth_user.servers.split('|').map(|s| s.to_string()).collect(),
token,
})).unwrap();
}
}
Err(_) => {
send_channel.send(Err(RPCError)).unwrap();
}
};
});
}
pub fn fetch_server_data(channel: Sender<Result<CServer, ErrorCode>>, addresses: Vec<String>, token: String, username: String){
for server_address in addresses {
let send_channel = channel.clone();
let token = token.clone();
let userid = username.clone();
let _handle = tokio::spawn(async move {
let mut transport = tarpc::serde_transport::tcp::connect(&server_address, Json::default);
transport.config_mut().max_frame_length(usize::MAX);
let result = transport.await;
let connection = match result {
Ok(connection) => connection,
Err(_) => {
send_channel.send(Err(UnableToConnectToServer)).unwrap();
return;
}
};
let client = RealmChatClient::new(tarpc::client::Config::default(), connection).spawn();
let info = client.get_info(context::current()).await.unwrap();
let domain = server_address.split(':').collect::<Vec<&str>>()[0].to_string();
let port = server_address.split(':').collect::<Vec<&str>>()[1].to_string().parse::<u16>().unwrap();
let stoken = stoken(&token, &info.server_id, &domain, port);
let is_admin = client.is_user_admin(context::current(), userid.clone()).await.unwrap();
let is_owner = client.is_user_owner(context::current(), userid.clone()).await.unwrap();
let rooms = client.get_rooms(context::current(), stoken.clone(), userid.clone()).await.unwrap().unwrap();
send_channel.send(Ok(CServer {
tarpc_conn: client,
server_id: info.server_id,
domain,
port,
is_admin,
is_owner,
last_event_index: 0,
messages: Vec::new(),
rooms,
})).unwrap();
});
}
}
pub fn fetch_rooms_data(send_channel: Sender<Result<(CServer, Vec<Room>), ErrorCode>>, server: CServer, token: String, userid: String) {
let _handle = tokio::spawn(async move {
let result = server.tarpc_conn.get_rooms(
context::current(),
stoken(&token, &server.server_id, &server.domain, server.port),
userid
).await;
match result {
Ok(r) => {
if let Ok(rooms) = r {
send_channel.send(Ok((server, rooms))).unwrap();
} else {
send_channel.send(Err(r.unwrap_err())).unwrap();
}
}
Err(_) => { send_channel.send(Err(RPCError)).unwrap(); }
}
});
}
impl eframe::App for RealmApp {
/// Called each time the UI needs repainting, which may be many times per second.
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
// Put your widgets into a `SidePanel`, `TopBottomPanel`, `CentralPanel`, `Window` or `Area`.
// For inspiration and more examples, go to https://emilk.github.io/egui
// Init at launch and refresh our user data from the auth server
if self.current_user.is_none() &&
self.saved_token.is_some() &&
self.saved_auth_address.is_some() &&
self.saved_username.is_some()
{
let send_channel = self.fetching_user_data_channel.0.clone();
let server_address = self.saved_auth_address.clone().unwrap();
let username = self.saved_username.clone().unwrap();
let token = self.saved_token.clone().unwrap();
fetch_user_data(send_channel, server_address, username, token);
}
// Fetching servers
if self.active_servers.is_none() && self.current_user.is_some() {
self.active_servers = Some(Vec::new());
fetch_server_data(
self.fetching_servers_channel.0.clone(),
self.current_user.as_ref().unwrap().server_addresses.clone(),
self.current_user.as_ref().unwrap().token.clone(),
self.current_user.as_ref().unwrap().username.clone()
);
}
// Starting the login flow
while let Ok(result) = self.login_start_channel.1.try_recv() {
match result {
Ok(_) => self.login_ready_for_code_input = true,
Err(e) => tracing::error!("Error in login/account creation flow: {:?}", e),
}
}
// End of the login flow
while let Ok(result) = self.login_ending_channel.1.try_recv() {
match result {
Ok(token) => {
info!("Login successful! Token: {token}");
self.login_ready_for_code_input = false;
self.login_window_open = false;
self.signup_window_open = false;
self.login_window_code.clear();
info!("Fetching user data...");
let send_channel = self.fetching_user_data_channel.0.clone();
let server_address = format!("{}:{}", self.login_window_server_domain, self.login_window_server_port);
let port = self.login_window_server_port.clone().parse::<u16>().unwrap();
let username = format!("@{}:{}", self.login_window_username, self.login_window_server_domain);
self.saved_token = Some(token.clone());
self.saved_username = Some(username.clone());
self.saved_auth_address = Some(server_address.clone());
fetch_user_data(send_channel, server_address, username, token);
}
Err(e) => tracing::error!("Error in login flow: {:?}", e),
}
}
// Fetching user data
while let Ok(result) = self.fetching_user_data_channel.1.try_recv() {
match result {
Ok(client_user) => {
info!("Got data! User: {:?}", client_user);
self.current_user.replace(client_user);
}
Err(e) => error!("Error fetching data: {:?}", e),
}
}
// Adding a server (auth)
while let Ok(result) = self.add_server_channel.1.try_recv() {
match result {
Ok(address) => {
info!("New server added at: {:?}", address);
self.server_window_open = false;
let send_channel = self.join_server_channel.0.clone();
let auth_address = self.saved_auth_address.clone().unwrap();
let username = self.saved_username.clone().unwrap();
let token = self.saved_token.clone().unwrap();
let thread_username = username.clone();
let thread_token = token.clone();
let _handle = tokio::spawn(async move {
let mut transport = tarpc::serde_transport::tcp::connect(&address, Json::default);
transport.config_mut().max_frame_length(usize::MAX);
let result = transport.await;
let connection = match result {
Ok(connection) => connection,
Err(_) => {
send_channel.clone().send(Err(UnableToConnectToServer)).unwrap();
return;
}
};
let client = RealmChatClient::new(tarpc::client::Config::default(), connection).spawn();
let domain = address.split(':').collect::<Vec<&str>>()[0].to_string();
let port = address.split(':').collect::<Vec<&str>>()[1].to_string().parse::<u16>().unwrap();
let info = client.get_info(context::current()).await.unwrap();
let result = client.join_server(context::current(), stoken(&thread_token, &info.server_id, &domain, port), thread_username).await;
match result {
Ok(r) => {
info!("Joined server!");
match r {
Ok(_) => { send_channel.send(Ok(())).unwrap(); },
Err(e) => { send_channel.send(Err(e)).unwrap(); },
}
},
Err(_) => {
error!("Error joining server");
send_channel.send(Err(RPCError)).unwrap();
},
}
});
fetch_user_data(self.fetching_user_data_channel.0.clone(), auth_address, username, token);
}
Err(e) => error!("Error in adding a server: {:?}", e),
}
}
// Joining a server
while let Ok(result) = self.join_server_channel.1.try_recv() {
match result {
Ok(_) => {
info!("Successfully joined a server");
fetch_server_data(
self.fetching_servers_channel.0.clone(),
self.current_user.as_ref().unwrap().server_addresses.clone(),
self.current_user.as_ref().unwrap().token.clone(),
self.current_user.as_ref().unwrap().username.clone()
);
},
Err(code) => {
error!("Error joining server: {:?}", code);
}
}
}
// Leaving a server
while let Ok(result) = self.leave_server_channel.1.try_recv() {
match result {
Ok((serverid, domain, port)) => {
info!("Successfully left a server");
self.active_servers.as_mut().unwrap().retain(|s| !s.server_id.eq(&serverid));
self.selected_serverid.clear();
self.selected_roomid.clear();
let send_channel = self.remove_server_channel.0.clone();
let auth_address = self.current_user.clone().unwrap().auth_address;
let username = self.current_user.clone().unwrap().username;
let token = self.current_user.clone().unwrap().token;
let _handle = tokio::spawn(async move {
let mut transport = tarpc::serde_transport::tcp::connect(&auth_address, Json::default);
transport.config_mut().max_frame_length(usize::MAX);
let result = transport.await;
let connection = match result {
Ok(connection) => connection,
Err(_) => {
send_channel.clone().send(Err(UnableToConnectToServer)).unwrap();
return;
}
};
let client = RealmAuthClient::new(tarpc::client::Config::default(), connection).spawn();
let result = client.remove_server(context::current(), username, token, domain, port).await;
match result {
Ok(r) => { send_channel.send(r).unwrap(); },
Err(_) => { send_channel.send(Err(RPCError)).unwrap(); },
}
});
},
Err(code) => {
error!("Error leaving server: {:?}", code);
}
}
}
// Removing a server (auth)
while let Ok(result) = self.remove_server_channel.1.try_recv() {
match result {
Ok(_) => {
let send_channel = self.fetching_user_data_channel.0.clone();
let server_address = self.saved_auth_address.clone().unwrap();
let username = self.saved_username.clone().unwrap();
let token = self.saved_token.clone().unwrap();
fetch_user_data(send_channel, server_address, username, token);
info!("Successfully removed a server");
}
Err(code) => {
error!("Error removing server: {:?}", code);
}
}
}
// Fetching servers
while let Ok(result) = self.fetching_servers_channel.1.try_recv() {
match result {
Ok(server) => {
info!("Got server data! Server: {:?}", server);
if let Some(active_servers) = &mut self.active_servers {
active_servers.push(server);
}
}
Err(e) => error!("Error fetching server data: {:?}", e),
}
}
// Added Room
while let Ok(result) = self.add_room_channel.1.try_recv() {
match result {
Ok(server) => {
info!("Got room add! Fetching them...");
fetch_rooms_data(
self.room_changes_channel.0.clone(),
server,
self.current_user.as_ref().unwrap().token.clone(),
self.current_user.as_ref().unwrap().username.clone()
);
self.room_window_open = false;
}
Err(e) => error!("Error adding room: {:?}", e),
}
}
// Deleting a room
while let Ok(result) = self.delete_room_channel.1.try_recv() {
match result {
Ok(server) => {
info!("Got room delete! Fetching them...");
self.selected_roomid.clear();
fetch_rooms_data(
self.room_changes_channel.0.clone(),
server,
self.current_user.as_ref().unwrap().token.clone(),
self.current_user.as_ref().unwrap().username.clone()
);
}
Err(e) => error!("Error deleting room: {:?}", e),
}
}
// Fetching rooms
while let Ok(result) = self.room_changes_channel.1.try_recv() {
match result {
Ok(tuple) => {
info!("Got room data for a server: {:?}", tuple);
if let Some(servers) = &mut self.active_servers {
for server in servers {
if server.server_id.eq(&tuple.0.server_id) {
server.rooms = tuple.1.clone();
}
}
}
}
Err(e) => error!("Error fetching room data: {:?}", e),
}
}
// Polling events
while let Ok((serverid, (index, event))) = self.event_channel.1.try_recv() {
if let Some(active_servers) = &mut self.active_servers {
for server in active_servers {
if server.server_id.eq(&serverid) {
match event.clone() {
Event::NewMessage(message) => {
server.messages.push(message);
}
Event::NewRoom(room) => {
server.rooms.push(room);
}
Event::DeleteRoom(roomid) => {
server.rooms.retain(|r| !r.roomid.eq(&roomid));
if self.selected_roomid.eq(&roomid) {
self.selected_roomid.clear();
}
},
_ => { }
}
if index > server.last_event_index {
server.last_event_index = index;
}
}
}
}
}
// Manage polling threads
if let Some(active_servers) = &mut self.active_servers {
if self.polling_threads.len() != active_servers.len() {
let running_thread_serverids = self.polling_threads.iter().map(|t| t.0.clone()).collect::<Vec<String>>();
let missing_servers = active_servers.clone().into_iter().filter(|s| !running_thread_serverids.contains(&s.server_id)).collect::<Vec<CServer>>();
for server in missing_servers {
let send_channel = self.event_channel.0.clone();
let serverid = server.server_id.clone();
let token = self.current_user.as_ref().unwrap().token.clone();
let userid = self.current_user.as_ref().unwrap().username.clone();
let handle = tokio::spawn(async move {
let mut transport = tarpc::serde_transport::tcp::connect(format!("{}:{}", server.domain, server.port), Json::default);
transport.config_mut().max_frame_length(usize::MAX);
let result = transport.await;
let connection = match result {
Ok(connection) => connection,
Err(_) => {
return;
}
};
let client = RealmChatClient::new(tarpc::client::Config::default(), connection).spawn();
let mut last_event_index = 0;
loop {
let result = client.get_messages_since(
context::current(),
stoken(&token, &serverid, &server.domain, server.port),
userid.clone(),
last_event_index
).await;
match result {
Ok(messages) => {
if let Ok(messages) = messages {
if let Some(last) = messages.last() {
last_event_index = last.id;
}
for message in messages {
send_channel.send((serverid.clone(), (message.id, Event::NewMessage(message)))).unwrap();
}
}
}
Err(_) => break,
}
sleep(Duration::from_millis(1000)).await;
}
});
self.polling_threads.push((server.server_id.clone(), handle));
}
}
}
// File -> Quit
gui::top_panel(self, ctx);
gui::servers(self, ctx);
gui::rooms(self, ctx);
gui::messages(self, ctx);
gui::modals(self, ctx)
}
/// Called by the frame work to save state before shutdown.
fn save(&mut self, storage: &mut dyn eframe::Storage) {
if !cfg!(debug_assertions) {
eframe::set_value(storage, eframe::APP_KEY, self);
}
}
}

View File

@@ -1,3 +0,0 @@
pub mod types;
pub mod app;
pub mod ui;

View File

@@ -1,31 +0,0 @@
use tracing::*;
#[tokio::main]
async fn main() -> eframe::Result {
let subscriber = tracing_subscriber::fmt()
.compact()
.with_file(true)
.with_line_number(true)
.with_thread_ids(true)
.with_target(false)
.finish();
subscriber::set_global_default(subscriber).unwrap();
let native_options = eframe::NativeOptions {
viewport: egui::ViewportBuilder::default()
.with_inner_size([720.0, 500.0])
.with_min_inner_size([500.0, 300.0])
.with_icon(
// NOTE: Adding an icon is optional
eframe::icon_data::from_png_bytes(&include_bytes!("../assets/icon-256.png")[..])
.expect("Failed to load icon"),
),
..Default::default()
};
eframe::run_native(
"Realm",
native_options,
Box::new(|cc| Ok(Box::new(realm_client::app::RealmApp::new(cc)))),
)
}

View File

@@ -1,25 +0,0 @@
use realm_server::types::{Message, RealmChatClient, Room};
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
pub struct CUser {
pub id: i64,
pub auth_address: String,
pub username: String,
pub email: String,
//pub avatar: String,
pub server_addresses: Vec<String>,
pub token: String,
}
#[derive(Clone, Debug)]
pub struct CServer {
pub tarpc_conn: RealmChatClient,
pub server_id: String,
pub domain: String,
pub port: u16,
pub is_admin: bool,
pub is_owner: bool,
pub rooms: Vec<Room>,
pub last_event_index: i64,
pub messages: Vec<Message>,
}

View File

@@ -1,582 +0,0 @@
use chrono::Utc;
use egui::{Context, SelectableLabel};
use tarpc::context;
use tarpc::tokio_serde::formats::Json;
use realm_auth::types::RealmAuthClient;
use realm_shared::types::ErrorCode::RPCError;
use regex::Regex;
use tracing::log::*;
use realm_server::types::{Message, MessageData, Room, User};
use realm_shared::stoken;
use crate::app::RealmApp;
use crate::types::CServer;
pub fn top_panel(app: &mut RealmApp, ctx: &Context) {
egui::TopBottomPanel::top("top_panel").show(ctx, |ui| {
egui::menu::bar(ui, |ui| {
if app.current_user.is_none() && ui.button("Sign Up").clicked() {
app.signup_window_open = true;
}
if app.current_user.is_none() && ui.button("Login").clicked() {
app.login_window_open = true;
}
if app.current_user.is_some() && ui.button("Logout").clicked() {
let address = app.current_user.clone().unwrap().auth_address;
let username = app.current_user.clone().unwrap().username;
let token = app.current_user.clone().unwrap().token;
let _handle = tokio::spawn(async move {
let mut transport = tarpc::serde_transport::tcp::connect(&address, Json::default);
transport.config_mut().max_frame_length(usize::MAX);
let result = transport.await;
let connection = match result {
Ok(connection) => connection,
Err(e) => {
tracing::error!("Failed to connect to server: {}", e);
return;
}
};
let client = RealmAuthClient::new(tarpc::client::Config::default(), connection).spawn();
let result = client.sign_out(context::current(), username, token).await;
match result {
Ok(_) => info!("Signed out!"), // TODO: properly handle this
Err(e) => error!("Error signing out: {:?}", e),
}
});
app.current_user = None;
app.saved_username = None;
app.saved_token = None;
app.saved_auth_address = None;
app.active_servers = None;
app.selected_roomid.clear();
app.selected_serverid.clear();
}
if ui.button("Quit").clicked() {
ctx.send_viewport_cmd(egui::ViewportCommand::Close);
}
ui.with_layout(egui::Layout::right_to_left(egui::Align::TOP), |ui| {
egui::widgets::global_theme_preference_buttons(ui);
if ui.button("").clicked() {
app.info_window_open = true;
}
if app.current_user.is_some() && ui.button("Delete Account").clicked() {
let address = app.current_user.clone().unwrap().auth_address;
let username = app.current_user.clone().unwrap().username;
let token = app.current_user.clone().unwrap().token;
let _handle = tokio::spawn(async move {
let mut transport = tarpc::serde_transport::tcp::connect(address, Json::default);
transport.config_mut().max_frame_length(usize::MAX);
let result = transport.await;
let connection = match result {
Ok(connection) => connection,
Err(e) => {
tracing::error!("Failed to connect to server: {}", e);
return;
}
};
let client = RealmAuthClient::new(tarpc::client::Config::default(), connection).spawn();
let result = client.delete_account(context::current(), username, token).await;
match result {
Ok(_) => info!("Account deleted successfully!"),
Err(e) => error!("Error deleting account: {:?}", e),
}
});
app.current_user = None;
app.saved_username = None;
app.saved_token = None;
app.saved_auth_address = None;
app.active_servers = None;
app.selected_roomid.clear();
app.selected_serverid.clear();
}
});
});
});
}
pub fn servers(app: &mut RealmApp, ctx: &Context) {
egui::SidePanel::left("servers").show(ctx, |ui| {
ui.horizontal(|ui| {
ui.heading("Servers");
if app.current_user.is_some() && ui.button("+").clicked() {
app.server_window_open = true;
}
if !app.selected_serverid.is_empty() && ui.button("-").clicked() {
let server = app.active_servers.clone().unwrap().into_iter().find(|s| s.server_id.eq(&app.selected_serverid)).unwrap();
let token = app.current_user.as_ref().unwrap().token.clone();
let userid = app.current_user.as_ref().unwrap().username.clone();
let send_channel = app.leave_server_channel.0.clone();
let _handle = tokio::spawn(async move {
let result = server.tarpc_conn.leave_server(
context::current(),
stoken(&token, &server.server_id, &server.domain, server.port),
userid
).await;
match result {
Ok(r) => {
match r {
Ok(_) => send_channel.send(Ok((server.server_id.clone(), server.domain.clone(), server.port))).unwrap(),
Err(e) => send_channel.send(Err(e)).unwrap()
}
},
Err(_) => send_channel.send(Err(RPCError)).unwrap(),
}
});
}
});
ui.separator();
if let Some(active_servers) = &mut app.active_servers {
for server in active_servers {
if ui.add(SelectableLabel::new(server.server_id.eq(&app.selected_serverid), server.server_id.clone())).clicked() {
if app.selected_serverid.eq(&server.server_id) {
app.selected_serverid.clear();
} else {
app.selected_serverid = server.server_id.clone();
}
app.selected_roomid.clear();
}
}
}
});
}
pub fn rooms(app: &mut RealmApp, ctx: &Context) {
egui::SidePanel::left("rooms").show(ctx, |ui| {
let mut current_server: Option<CServer> = None;
if let Some(servers) = app.active_servers.clone() {
for server in servers {
if server.server_id.eq(&app.selected_serverid) {
current_server = Some(server);
}
}
}
ui.horizontal(|ui| {
ui.heading("Rooms");
if let Some(server) = current_server.clone() {
if server.is_admin && ui.button("+").clicked() {
app.room_window_open = true;
}
if server.is_admin && !app.selected_roomid.is_empty() && ui.button("-").clicked() {
let token = app.current_user.as_ref().unwrap().token.clone();
let roomid = app.selected_roomid.clone();
let userid = app.current_user.as_ref().unwrap().username.clone();
let send_channel = app.delete_room_channel.0.clone();
let _handle = tokio::spawn(async move {
let result = server.tarpc_conn.delete_room(
context::current(),
stoken(&token, &server.server_id, &server.domain, server.port),
userid,
roomid
).await;
match result {
Ok(r) => {
match r {
Ok(_) => send_channel.send(Ok(server)).unwrap(),
Err(e) => send_channel.send(Err(e)).unwrap()
}
},
Err(_) => send_channel.send(Err(RPCError)).unwrap(),
}
});
}
}
});
ui.separator();
if let Some(server) = current_server {
for room in &server.rooms {
if ui.add(SelectableLabel::new(room.roomid.eq(&app.selected_roomid), room.roomid.clone())).clicked() {
if app.selected_roomid.eq(&room.roomid) {
app.selected_roomid.clear();
} else {
app.selected_roomid = room.roomid.clone();
}
}
}
}
});
}
pub fn messages(app: &mut RealmApp, ctx: &Context) {
egui::CentralPanel::default().show(ctx, |ui| {
ui.with_layout(egui::Layout::bottom_up(egui::Align::TOP), |ui| {
ui.with_layout(egui::Layout::right_to_left(egui::Align::Max), |ui| {
if ui.button("").on_hover_text("Send a message").clicked() {
if let Some(active_servers) = &app.active_servers {
for server in active_servers.clone() {
if server.server_id.eq(&app.selected_serverid) {
let username = app.current_user.as_ref().unwrap().username.clone();
let token = app.current_user.as_ref().unwrap().token.clone();
let room = server.rooms.iter().find(|r| r.roomid.eq(&app.selected_roomid)).unwrap().clone();
let text_message = app.text_message_input.clone();
let _handle = tokio::spawn(async move {
let result = server.tarpc_conn.send_message(
context::current(),
stoken(&token, &server.server_id, &server.domain, server.port),
Message {
id: 0,
timestamp: Utc::now(),
user: server.tarpc_conn.get_user(context::current(), username.clone()).await.unwrap().unwrap(),
room,
data: MessageData::Text(text_message),
}
).await;
});
app.text_message_input.clear();
}
}
}
}
ui.add(
egui::TextEdit::multiline(&mut app.text_message_input)
.desired_rows(1)
.desired_width(ui.available_width())
.hint_text("Send a message...")
);
});
ui.with_layout(egui::Layout::top_down_justified(egui::Align::Min), |ui| {
egui::ScrollArea::vertical().show(ui, |ui| {
if let Some(active_servers) = &app.active_servers {
for server in active_servers {
let messages_to_display = server.messages
.iter()
.filter(|m| m.room.roomid.eq(&app.selected_roomid))
.collect::<Vec<&Message>>();
for message in messages_to_display {
match message.clone().data {
MessageData::Text(text) => {
ui.label(format!("{} - {}: {}",
message.timestamp.format("%Y-%m-%d %H:%M:%S"),
message.user.userid.split(':').collect::<Vec<&str>>()[0],
text));
}
MessageData::Attachment(_) => {}
MessageData::Reply(_) => {}
MessageData::Edit(_) => {}
MessageData::Reaction(_) => {}
MessageData::Redaction(_) => {}
}
}
}
}
});
});
});
});
}
pub fn modals(app: &mut RealmApp, ctx: &Context) {
egui::Window::new("Info")
.open(&mut app.info_window_open)
.min_size((500.0, 200.0))
.show(ctx, |ui| {
egui::ScrollArea::vertical().show(ui, |ui| {
ui.label(format!("Saved username: {:?}", app.saved_username));
ui.label(format!("Saved token: {:?}", app.saved_token));
ui.label(format!("Saved auth address: {:?}", app.saved_auth_address));
ui.separator();
if let Some(servers) = &app.active_servers {
for server in servers {
ui.heading(&server.server_id);
ui.label(format!("{:?}", server));
}
}
ui.separator();
ui.label(format!("Current user: {:?}", app.current_user));
});
});
egui::Window::new("Signup")
.open(&mut app.signup_window_open)
.min_size((500.0, 200.0))
.show(ctx, |ui| {
ui.horizontal(|ui| {
ui.label("Domain: ");
ui.text_edit_singleline(&mut app.login_window_server_domain);
ui.label("Port: ");
if ui.text_edit_singleline(&mut app.login_window_server_port).changed() {
let re = Regex::new(r"[^0-9]+").unwrap();
app.login_window_server_port = re.replace_all(&app.login_window_server_port, "").to_string();
}
});
ui.horizontal(|ui| {
ui.label("Username: ");
ui.text_edit_singleline(&mut app.login_window_username);
});
ui.horizontal(|ui| {
ui.label("Email: ");
ui.text_edit_singleline(&mut app.login_window_email);
});
if ui.button("Create Account").clicked() {
let login_window_server_address = format!("{}:{}", app.login_window_server_domain, app.login_window_server_port);
let login_window_username = format!("@{}:{}", app.login_window_username, app.login_window_server_domain);
let login_window_email = app.login_window_email.clone();
let send_channel = app.login_start_channel.0.clone();
let _handle = tokio::spawn(async move {
let mut transport = tarpc::serde_transport::tcp::connect(login_window_server_address, Json::default);
transport.config_mut().max_frame_length(usize::MAX);
let result = transport.await;
let connection = match result {
Ok(connection) => connection,
Err(e) => {
tracing::error!("Failed to connect to server: {}", e);
return;
}
};
let client = RealmAuthClient::new(tarpc::client::Config::default(), connection).spawn();
let result = client.create_account_flow(context::current(), login_window_username, login_window_email).await;
match result {
Ok(r) => send_channel.send(r).unwrap(),
Err(_) => send_channel.send(Err(RPCError)).unwrap(),
};
});
//ui.close_menu()
}
});
egui::Window::new("Login")
.open(&mut app.login_window_open)
.min_size((500.0, 200.0))
.show(ctx, |ui| {
ui.horizontal(|ui| {
ui.label("Domain: ");
ui.text_edit_singleline(&mut app.login_window_server_domain);
ui.label("Port: ");
if ui.text_edit_singleline(&mut app.login_window_server_port).changed() {
let re = Regex::new(r"[^0-9]+").unwrap();
app.login_window_server_port = re.replace_all(&app.login_window_server_port, "").to_string();
}
});
ui.horizontal(|ui| {
ui.label("Username: ");
ui.text_edit_singleline(&mut app.login_window_username);
});
if ui.button("Send Login Code").clicked() {
let login_window_server_address = format!("{}:{}", app.login_window_server_domain, app.login_window_server_port);
let login_window_username = format!("@{}:{}", app.login_window_username, app.login_window_server_domain);
let send_channel = app.login_start_channel.0.clone();
let _handle = tokio::spawn(async move {
let mut transport = tarpc::serde_transport::tcp::connect(login_window_server_address, Json::default);
transport.config_mut().max_frame_length(usize::MAX);
let result = transport.await;
let connection = match result {
Ok(connection) => connection,
Err(e) => {
tracing::error!("Failed to connect to server: {}", e);
return;
}
};
let client = RealmAuthClient::new(tarpc::client::Config::default(), connection).spawn();
let result = client.create_login_flow(context::current(), Some(login_window_username), None).await;
match result {
Ok(r) => send_channel.send(r).unwrap(),
Err(_) => send_channel.send(Err(RPCError)).unwrap(),
};
});
//ui.close_menu()
}
});
egui::Window::new("Auth Code")
.open(&mut app.login_ready_for_code_input)
.min_size((500.0, 200.0))
.show(ctx, |ui| {
ui.horizontal(|ui| {
ui.label("Code: ");
if ui.text_edit_singleline(&mut app.login_window_code).changed() {
let re = Regex::new(r"[^0-9]+").unwrap();
app.login_window_code = re.replace_all(&app.login_window_code, "").to_string();
}
});
if ui.button("Login").clicked() {
let login_window_server_address = format!("{}:{}", app.login_window_server_domain, app.login_window_server_port);
let login_window_code = app.login_window_code.clone();
let login_window_username = format!("@{}:{}", app.login_window_username, app.login_window_server_domain);
let send_channel = app.login_ending_channel.0.clone();
let _handle = tokio::spawn(async move {
let mut transport = tarpc::serde_transport::tcp::connect(login_window_server_address, Json::default);
transport.config_mut().max_frame_length(usize::MAX);
let result = transport.await;
let connection = match result {
Ok(connection) => connection,
Err(e) => {
tracing::error!("Failed to connect to server: {}", e);
return;
}
};
let client = RealmAuthClient::new(tarpc::client::Config::default(), connection).spawn();
let result = client.finish_login_flow(context::current(), login_window_username, login_window_code.parse::<u32>().unwrap()).await;
match result {
Ok(r) => {
send_channel.send(r).unwrap();
}
Err(e) => {
send_channel.send(Err(RPCError)).unwrap();
}
}
});
//ui.close_menu()
}
});
egui::Window::new("Add Server")
.open(&mut app.server_window_open)
.min_size((500.0, 200.0))
.show(ctx, |ui| {
ui.horizontal(|ui| {
ui.label("Domain: ");
ui.text_edit_singleline(&mut app.server_window_domain);
});
ui.horizontal(|ui| {
ui.label("Port: ");
if ui.text_edit_singleline(&mut app.server_window_port).changed() {
let re = Regex::new(r"[^0-9]+").unwrap();
app.login_window_code = re.replace_all(&app.server_window_port, "").to_string();
}
});
if app.current_user.is_some() && ui.button("Add Server").clicked() {
let domain = app.server_window_domain.clone();
let port = app.server_window_port.clone();
let auth_address = app.current_user.clone().unwrap().auth_address;
let auth_username = app.current_user.clone().unwrap().username;
let auth_token = app.current_user.clone().unwrap().token;
let send_channel = app.add_server_channel.0.clone();
let _handle = tokio::spawn(async move {
let mut transport = tarpc::serde_transport::tcp::connect(auth_address, Json::default);
transport.config_mut().max_frame_length(usize::MAX);
let result = transport.await;
let connection = match result {
Ok(connection) => connection,
Err(e) => {
tracing::error!("Failed to connect to server: {}", e);
return;
}
};
let client = RealmAuthClient::new(tarpc::client::Config::default(), connection).spawn();
let result = client.add_server(
context::current(), auth_username, auth_token, domain.clone(), port.parse::<u16>().unwrap()).await;
match result {
Ok(r) => {
match r {
Ok(_) => {
info!("Server added successfully!");
send_channel.send(Ok(format!("{}:{}", domain, port))).unwrap();
},
Err(e) => error!("Error adding server: {:?}", e),
}
},
Err(_) => { send_channel.send(Err(RPCError)).unwrap(); },
};
});
}
});
egui::Window::new("Add Room")
.open(&mut app.room_window_open)
.min_size((500.0, 200.0))
.show(ctx, |ui| {
ui.horizontal(|ui| {
ui.label("Name: ");
ui.text_edit_singleline(&mut app.room_window_name);
});
ui.checkbox(&mut app.room_window_admin_only_send, "Only admins can send");
ui.checkbox(&mut app.room_window_admin_only_view, "Only admins can view");
if ui.button("Add Room").clicked() {
for server in app.active_servers.clone().unwrap() {
if server.server_id.eq(&app.selected_serverid) {
let token = app.current_user.as_ref().unwrap().token.clone();
let roomid = app.room_window_name.clone();
let admin_only_send = app.room_window_admin_only_send;
let admin_only_view = app.room_window_admin_only_view;
let userid = app.current_user.as_ref().unwrap().username.clone();
let send_channel = app.add_room_channel.0.clone();
let _handle = tokio::spawn(async move {
let result = server.tarpc_conn.create_room(
context::current(),
stoken(&token, &server.server_id, &server.domain, server.port),
userid,
Room {
id: 0,
roomid,
admin_only_send,
admin_only_view,
}
).await;
match result {
Ok(r) => {
match r {
Ok(_) => { send_channel.send(Ok(server)).unwrap(); }
Err(e) => { send_channel.send(Err(e)).unwrap(); }
}
}
Err(_) => {
send_channel.send(Err(RPCError)).unwrap();
}
}
});
}
}
}
});
}

View File

@@ -1 +0,0 @@
pub mod gui;

View File

@@ -0,0 +1,6 @@
#Fri Jun 21 00:57:31 EDT 2024
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

View File

@@ -1,22 +0,0 @@
[package]
name = "realm_server"
version = "0.1.0"
edition = "2021"
[dependencies]
anyhow = "1.0.89"
futures = "0.3.30"
tarpc = { version = "0.34.0", features = ["full"] }
tokio = { version = "1.40.0", features = ["macros", "net", "rt-multi-thread"] }
tracing = "0.1.40"
tracing-subscriber = "0.3.18"
serde = { version = "1.0.210", features = ["derive"] }
emojis = "0.6.3"
chrono = { version = "0.4.38", features = ["serde"] }
sqlx = { version = "0.8.2", features = [ "runtime-tokio", "tls-rustls", "sqlite", "chrono" ] }
dotenvy = "0.15.7"
moka = { version = "0.12.8", features = ["future"] }
futures-util = "0.3.30"
realm_auth = { path = "../auth" }
realm_shared = { path = "../shared" }

View File

@@ -1,4 +0,0 @@
DATABASE_URL=sqlite:server.db
DOMAIN=
SERVER_ID=
PORT=5051

View File

@@ -1,31 +0,0 @@
-- Add migration script here
CREATE TABLE IF NOT EXISTS room (
id INTEGER PRIMARY KEY,
roomid VARCHAR(255) NOT NULL,
admin_only_send BOOL NOT NULL,
admin_only_view BOOL NOT NULL
);
CREATE TABLE IF NOT EXISTS user (
id INTEGER PRIMARY KEY,
userid VARCHAR(255) NOT NULL,
name VARCHAR(255) NOT NULL,
owner BOOL NOT NULL,
admin BOOL NOT NULL
);
CREATE TABLE IF NOT EXISTS message (
id INTEGER PRIMARY KEY,
timestamp DATETIME NOT NULL,
user INT NOT NULL,
room INT NOT NULL,
msg_type VARCHAR CHECK( msg_type IN ('text', 'attachment', 'reply', 'edit', 'reaction', 'redaction')) NOT NULL,
msg_text TEXT,
referencing_id INTEGER,
emoji TEXT
);
CREATE TABLE IF NOT EXISTS banned (
id INTEGER PRIMARY KEY,
userid VARCHAR(255) NOT NULL
);

View File

@@ -1,15 +0,0 @@
use crate::types::{Message, Room, User};
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
pub enum Event {
// UserJoined(User),
// UserLeft(User),
None,
NewMessage(Message),
NewRoom(Room),
DeleteRoom(String),
// KickedUser(KickedUser),
// BannedUser(BannedUser),
// PromotedUser(PromotedUser),
// DemotedUser(DemotedUser),
}

View File

@@ -1,3 +0,0 @@
pub mod server;
pub mod types;
pub mod events;

View File

@@ -1,110 +0,0 @@
use std::env;
use std::future::Future;
use std::net::{IpAddr, Ipv6Addr, SocketAddr};
use std::sync::{Arc};
use dotenvy::dotenv;
use futures::future::{self};
use futures::StreamExt;
use sqlx::migrate::MigrateDatabase;
use sqlx::{migrate, Sqlite, SqlitePool};
use tarpc::{
server::{Channel},
tokio_serde::formats::Json,
};
use tarpc::server::incoming::Incoming;
use tarpc::server::BaseChannel;
use tokio::sync::Mutex;
use tracing::{info, subscriber, warn};
use tracing::instrument::WithSubscriber;
use realm_server::events::*;
use realm_server::server::RealmChatServer;
use realm_server::types::{RealmChat};
async fn spawn(fut: impl Future<Output = ()> + Send + 'static) {
tokio::spawn(fut);
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
dotenv().ok();
let subscriber = tracing_subscriber::fmt()
.compact()
.with_file(true)
.with_line_number(true)
.with_thread_ids(true)
.with_target(false)
.finish();
subscriber::set_global_default(subscriber)?;
let database_url: &str = &env::var("DATABASE_URL").expect("DATABASE_URL must be set");
if !Sqlite::database_exists(database_url).await.unwrap_or(false) {
info!("Creating database {}", database_url);
match Sqlite::create_database(database_url).await {
Ok(_) => info!("Create db success"),
Err(error) => panic!("error: {}", error),
}
} else {
warn!("Database already exists");
} // TODO: Do in Docker with Sqlx-cli
let db_pool = SqlitePool::connect(database_url).await?;
info!("Running migrations...");
migrate!().run(&db_pool).await?; // TODO: Do in Docker with Sqlx-cli
info!("Migrations complete!");
let port = env::var("PORT").expect("PORT must be set").parse::<u16>()?;
let server_addr = (IpAddr::V4("0.0.0.0".parse()?), port);
// let (handler, listener) = node::split::<()>();
// handler.network().listen(Transport::FramedTcp, "0.0.0.0:"+(port-1))?;
// info!("Listening on port {}", port-1);
//
// // Read incoming network events.
// listener.for_each(|event| match event.network() {
// NetEvent::Connected(_, _) => unreachable!(), // Used for explicit connections.
// NetEvent::Accepted(_endpoint, _listener) => println!("Client connected"), // Tcp or Ws
// NetEvent::Message(endpoint, data) => {
// println!("Received: {}", String::from_utf8_lossy(data));
// handler.network().send(endpoint, data);
// },
// NetEvent::Disconnected(_endpoint) => println!("Client disconnected"), //Tcp or Ws
// });
//
// inner_manager.init_server(
// ServerConfig::new(
// SocketAddr::from((IpAddr::V4("0.0.0.0".parse()?), port-1)).to_string(),
// 0, None, 8, 8))?;
//
// let manager = Arc::new(Mutex::new(NetIO {
// handler,
// listener,
// }));
// JSON transport is provided by the json_transport tarpc module. It makes it easy
// to start up a serde-powered json serialization strategy over TCP.
let mut listener = tarpc::serde_transport::tcp::listen(&server_addr, Json::default).await?;
info!("Listening on port {}", listener.local_addr().port());
listener.config_mut().max_frame_length(usize::MAX);
listener
// Ignore accept errors.
.filter_map(|r| future::ready(r.ok()))
.map(BaseChannel::with_defaults)
// Limit channels to 1 per IP.
.max_channels_per_key(1024, |t| t.transport().peer_addr().unwrap().ip())
// serve is generated by the service attribute. It takes as input any type implementing
// the generated World trait.
.map(|channel| {
let server = RealmChatServer::new(env::var("SERVER_ID").expect("SERVER_ID must be set"), channel.transport().peer_addr().unwrap(), db_pool.clone());
channel.execute(server.serve()).for_each(spawn)
})
// Max 10 channels.
.buffer_unordered(10240)
.for_each(|_| async {})
.await;
Ok(())
}

View File

@@ -1,669 +0,0 @@
use std::env;
use std::net::SocketAddr;
use std::sync::{Arc};
use std::time::Duration;
use chrono::{DateTime, Utc};
use moka::future::Cache;
use sqlx::{FromRow, Pool, query_as, Sqlite};
use sqlx::query;
use tarpc::context::Context;
use tarpc::tokio_serde::formats::Json;
use tokio::sync::Mutex;
use tracing::error;
use realm_auth::types::RealmAuthClient;
use realm_shared::types::ErrorCode::*;
use realm_shared::types::ErrorCode;
use crate::events::*;
use crate::types::{Attachment, Edit, FromRows, Message, MessageData, Reaction, RealmChat, Redaction, Reply, ReplyChain, Room, ServerInfo, User};
#[derive(Clone)]
pub struct RealmChatServer {
pub server_id: String,
pub domain: String,
pub port: u16,
pub socket: SocketAddr,
pub db_pool: Pool<Sqlite>,
pub typing_users: Vec<(String, String)>, //NOTE: user.userid, room.roomid
pub cache: Cache<String, String>,
//pub events: Arc<Mutex<Vec<(u32, Event)>>>,
}
const FETCH_MESSAGE: &str = "SELECT message.*,
room.id AS 'room_id', room.roomid AS 'room_roomid', room.admin_only_send AS 'room_admin_only_send', room.admin_only_view AS 'room_admin_only_view',
user.id AS 'user_id', user.userid AS 'user_userid', user.name AS 'user_name', user.owner AS 'user_owner', user.admin AS 'user_admin'
FROM message INNER JOIN room ON message.room = room.id INNER JOIN user ON message.user = user.id";
impl RealmChatServer {
pub fn new(server_id: String, socket: SocketAddr, db_pool: Pool<Sqlite>) -> RealmChatServer {
RealmChatServer {
server_id,
port: env::var("PORT").unwrap().parse::<u16>().unwrap(),
domain: env::var("DOMAIN").expect("DOMAIN must be set"),
socket,
db_pool,
typing_users: Vec::new(),
cache: Cache::builder()
.max_capacity(10_000)
.time_to_idle(Duration::from_secs(5*60))
.time_to_live(Duration::from_secs(60*60))
.build(),
//events: Arc::new(Mutex::new(Vec::new())),
}
}
async fn is_stoken_valid(&self, userid: &str, stoken: &str) -> bool {
match self.cache.get(stoken).await {
None => {
// if !self.is_user_in_server(userid).await {
// return false;
// }
let user_domain = &userid[userid.find(':').unwrap()+1..];
let mut auth_transport = tarpc::serde_transport::tcp::connect((user_domain, 5052), Json::default);
auth_transport.config_mut().max_frame_length(usize::MAX);
let connected = match auth_transport.await {
Ok(out) => Some(out),
Err(_) => None
};
if connected.is_none() {
return false;
}
let auth_client = RealmAuthClient::new(tarpc::client::Config::default(), connected.unwrap()).spawn();
let result = auth_client.server_token_validation(
tarpc::context::current(), stoken.to_string(), userid.to_string(), self.server_id.clone(), self.domain.clone(), self.port)
.await;
match result {
Ok(valid) => {
if valid {
self.cache.insert(stoken.to_string(), userid.to_string()).await;
return true
}
false
}
Err(_) => {
error!("Error validating server token for user, {}, with stoken {}", userid, stoken);
false
}
}
}
Some(cached_username) => cached_username.eq(userid),
}
}
pub async fn internal_is_user_admin(&self, userid: &str) -> bool {
let result = query!("SELECT admin FROM user WHERE userid = ?", userid).fetch_one(&self.db_pool).await;
match result {
Ok(record) => record.admin,
Err(_) => false
}
}
pub async fn internal_is_user_owner(&self, userid: &str) -> bool {
let result = query!("SELECT owner FROM user WHERE userid = ?", userid).fetch_one(&self.db_pool).await;
match result {
Ok(record) => record.owner,
Err(_) => false
}
}
async fn is_user_in_server(&self, userid: &str) -> bool {
let result = query!("SELECT EXISTS (SELECT 1 FROM user WHERE userid = ?) AS does_exist", userid).fetch_one(&self.db_pool).await;
match result {
Ok(record) => record.does_exist != 0,
Err(_) => false
}
}
async fn inner_get_all_direct_replies(&self, userid: &str, head: i64) -> Result<Vec<Message>, ErrorCode> {
let is_admin = self.internal_is_user_admin(userid).await;
let result = sqlx::query(&format!("{}{}", FETCH_MESSAGE, "AND message.referencing_id = ?"))
.bind(is_admin)
.bind(head)
.fetch_all(&self.db_pool).await;
match result {
Ok(rows) => Ok(Message::from_rows(rows).unwrap()),
Err(_) => Err(MessageNotFound),
}
}
async fn inner_get_reply_chain(&self, userid: &str, head: Message, depth: u8) -> Result<ReplyChain, ErrorCode> {
if depth > 8 {
return Err(DepthTooLarge)
}
let direct_replies = self.inner_get_all_direct_replies(userid, head.id).await?;
let replies = if direct_replies.is_empty() || depth == 0 {
None
} else {
let mut chains = Vec::new();
for reply in direct_replies {
chains.push(Box::pin(self.inner_get_reply_chain(userid, reply, depth - 1)).await?);
}
Some(chains)
};
let chain = ReplyChain {
message: head,
replies,
};
Ok(chain)
}
async fn inner_get_room(&self, userid: &str, roomid: &str) -> Result<Room, ErrorCode> {
let is_admin = self.internal_is_user_admin(&userid).await;
let result = query_as!(
Room, "SELECT * FROM room WHERE roomid = ? AND admin_only_view = ? OR false", is_admin, roomid).fetch_one(&self.db_pool).await;
match result {
Ok(room) => Ok(room),
Err(_) => Err(RoomNotFound),
}
}
async fn inner_get_user(&self, userid: &str) -> Result<User, ErrorCode> {
let result = query_as!(User, "SELECT * FROM user WHERE userid = ?", userid).fetch_one(&self.db_pool).await;
match result {
Ok(user) => Ok(user),
Err(_) => Err(UserNotFound),
}
}
async fn inner_get_all_users(&self) -> Result<Vec<User>, ErrorCode> {
let result = query_as!(User, "SELECT * FROM user").fetch_all(&self.db_pool).await;
match result {
Ok(users) => Ok(users),
Err(_) => Err(Error),
}
}
async fn inner_get_message(&self, userid: &str, id: i64) -> Result<Message, ErrorCode> {
let is_admin = self.internal_is_user_admin(&userid).await;
let result = sqlx::query(&format!("{}{}", FETCH_MESSAGE, "AND message.id = ?"))
.bind(is_admin)
.bind(id)
.fetch_one(&self.db_pool).await;
match result {
Ok(row) => Ok(Message::from_row(&row).unwrap()),
Err(_) =>Err(MessageNotFound),
}
}
}
impl RealmChat for RealmChatServer {
async fn test(self, _: Context, name: String) -> String {
format!("Hello, {name}!")
}
async fn get_info(self, _: Context) -> ServerInfo {
ServerInfo {
server_id: self.server_id.clone(),
}
}
async fn is_user_admin(self, _: Context, userid: String) -> bool {
self.internal_is_user_admin(&userid).await
}
async fn is_user_owner(self, _: Context, userid: String) -> bool {
self.internal_is_user_owner(&userid).await
}
async fn poll_events_since(self, _: Context, index: u32) -> Vec<(u32, Event)> {
//self.events.lock().await.iter().filter(|(i, _)| i > &index).map(|(i, e)| (*i, e.clone())).collect()
// let mut events_to_send = Vec::new();
//
// for (i, event) in self.events.lock().await.clone() {
// if i > index {
// events_to_send.push((i, event));
// }
// }
//
// events_to_send
Vec::new()
}
async fn join_server(self, _: Context, stoken: String, userid: String) -> Result<User, ErrorCode> {
if !self.is_stoken_valid(&userid, &stoken).await {
return Err(Unauthorized)
}
if self.is_user_in_server(&userid).await {
return Err(AlreadyJoinedServer)
}
let is_owner = {
let all_users = self.inner_get_all_users().await?;
all_users.is_empty()
};
//TOOD: name support
let result = query!("INSERT INTO user (userid, name, owner, admin) VALUES (?,?,?,?)", userid, "userid", is_owner, is_owner).execute(&self.db_pool).await;
match result {
Ok(_) => {
let new_user = self.inner_get_user(&userid).await?;
// let result = self.packet_manager.lock().await.broadcast(UserJoinedEvent {
// user: new_user.clone(),
// });
// if result.is_err() {
// error!("Error broadcasting UserJoinedEvent!");
// }
Ok(new_user)
},
Err(_) => Err(MalformedDBResponse),
}
}
async fn leave_server(self, _: Context, stoken: String, userid: String) -> Result<(), ErrorCode> {
if !self.is_stoken_valid(&userid, &stoken).await {
return Err(Unauthorized)
}
if !self.is_user_in_server(&userid).await {
return Err(NotInServer)
}
let user = self.inner_get_user(&userid).await?;
let result = query!("DELETE FROM user WHERE userid = ?", userid).execute(&self.db_pool).await;
match result {
Ok(_) => {
// let result = self.packet_manager.lock().await.broadcast(UserLeftEvent {
// user,
// });
//
// if result.is_err() {
// error!("Error broadcasting UserLeftEvent!");
// }
Ok(())
},
Err(_) => Err(MalformedDBResponse),
}
}
async fn send_message(self, _: Context, stoken: String, mut message: Message) -> Result<Message, ErrorCode> {
// if !self.is_stoken_valid(&message.user.userid, &stoken).await { // Check sender userid
// return Err(Unauthorized)
// }
//
// // Assert all the data in message is correct
// message.user = self.inner_get_user(&message.user.userid).await?;
// match &message.data { // Check that the sender is the owner of the referencing msg
// MessageData::Edit(e) => {
// let ref_msg = self.inner_get_message(&message.user.userid, e.referencing_id).await?;
// if !ref_msg.user.userid.eq(&message.user.userid) {
// return Err(Unauthorized)
// }
// }
// MessageData::Redaction(r)=> {
// let ref_msg = self.inner_get_message(&message.user.userid, r.referencing_id).await?;
// if !ref_msg.user.userid.eq(&message.user.userid) || !self.internal_is_user_admin(&message.user.userid).await {
// return Err(Unauthorized)
// }
// }
// _ => {}
// }
// let is_admin = self.internal_is_user_admin(&message.user.userid).await;
// let admin_only_send = query!(
// "SELECT admin_only_send FROM room WHERE roomid = ?",
// message.room.roomid).fetch_one(&self.db_pool).await;
// if let Ok(record) = admin_only_send {
// if record.admin_only_send && !is_admin {
// return Err(Unauthorized)
// }
// } else {
// return Err(RoomNotFound)
// }
//
// message.room = self.inner_get_room(&message.user.userid, &message.room.roomid).await?;
let result = match &message.data {
MessageData::Text(text) => {
query!("INSERT INTO message (timestamp, user, room, msg_type, msg_text) VALUES (?, ?, ?, 'text', ?)",
message.timestamp, message.user.id, message.room.id, text)
.execute(&self.db_pool).await
}
MessageData::Attachment(attachment) => { todo!() }
MessageData::Reply(reply) => {
query!("INSERT INTO message (timestamp, user, room, msg_type, msg_text, referencing_id) VALUES (?, ?, ?, 'reply', ?, ?)",
message.timestamp, message.user.id, message.room.id, reply.text, reply.referencing_id)
.execute(&self.db_pool).await
}
MessageData::Edit(edit) => {
query!("INSERT INTO message (timestamp, user, room, msg_type, msg_text, referencing_id) VALUES (?, ?, ?, 'edit', ?, ?)",
message.timestamp, message.user.id, message.room.id, edit.text, edit.referencing_id)
.execute(&self.db_pool).await
}
MessageData::Reaction(reaction) => {
query!("INSERT INTO message (timestamp, user, room, msg_type, emoji, referencing_id) VALUES (?, ?, ?, 'reaction', ?, ?)",
message.timestamp, message.user.id, message.room.id, reaction.emoji, reaction.referencing_id)
.execute(&self.db_pool).await
}
MessageData::Redaction(redaction) => {
query!("INSERT INTO message (timestamp, user, room, msg_type, referencing_id) VALUES (?, ?, ?, 'redaction', ?)",
message.timestamp, message.user.id, message.room.id, redaction.referencing_id)
.execute(&self.db_pool).await
}
};
match result {
Ok(_) => {
// let result = self.packet_manager.lock().await.broadcast(NewMessageEvent {
// message: message.clone(),
// });
//
// if result.is_err() {
// error!("Error broadcasting NewMessageEvent!");
// }
// self.events.lock().await.push(
// (self.events.lock().await.len() as u32 + 1,
// Event::NewMessage(message.clone())));
Ok(message)
},
Err(_) => Err(Error),
}
}
async fn start_typing(self, _: Context, stoken: String, userid: String, roomid: String) -> ErrorCode {
todo!()
}
async fn stop_typing(self, _: Context, stoken: String, userid: String, roomid: String) -> ErrorCode {
todo!()
}
async fn keep_typing(self, _: Context, stoken: String, userid: String, roomid: String) -> ErrorCode {
todo!()
}
async fn get_message(self, _: Context, stoken: String, userid: String, id: i64) -> Result<Message, ErrorCode> {
if !self.is_stoken_valid(&userid, &stoken).await {
return Err(Unauthorized)
}
let is_admin = self.internal_is_user_admin(&userid).await;
let result = sqlx::query(&format!("{}{}", FETCH_MESSAGE, "AND message.id = ?"))
.bind(is_admin)
.bind(id)
.fetch_one(&self.db_pool).await;
match result {
Ok(row) => {
Ok(Message::from_row(&row).unwrap())
},
Err(_) => {
Err(MessageNotFound)
},
}
}
async fn get_messages_since(self, _: Context, stoken: String, userid: String, id: i64) -> Result<Vec<Message>, ErrorCode> {
if !self.is_stoken_valid(&userid, &stoken).await {
return Err(Unauthorized)
}
let is_admin = self.internal_is_user_admin(&userid).await;
let result = sqlx::query(&format!("{}{}", FETCH_MESSAGE, " AND message.id > ?"))
//.bind(is_admin)
.bind(id)
.fetch_all(&self.db_pool).await;
match result {
Ok(rows) => Ok(Message::from_rows(rows).unwrap()),
Err(_) => Err(MalformedDBResponse)
}
}
async fn get_all_direct_replies(self, _: Context, stoken: String, userid: String, head: i64) -> Result<Vec<Message>, ErrorCode> {
if !self.is_stoken_valid(&userid, &stoken).await {
return Err(Unauthorized)
}
self.inner_get_all_direct_replies(&userid, head).await
}
async fn get_reply_chain(self, _: Context, stoken: String, userid: String, head: Message, depth: u8) -> Result<ReplyChain, ErrorCode> {
if !self.is_stoken_valid(&userid, &stoken).await {
return Err(Unauthorized)
}
self.inner_get_reply_chain(&stoken, head, depth).await
}
async fn get_rooms(self, _: Context, stoken: String, userid: String) -> Result<Vec<Room>, ErrorCode> {
if !self.is_stoken_valid(&userid, &stoken).await {
return Err(Unauthorized)
}
let is_admin = self.internal_is_user_admin(&userid).await;
let result = query_as!(
Room, "SELECT * FROM room WHERE admin_only_view LIKE false or ?", is_admin).fetch_all(&self.db_pool).await;
match result {
Ok(rooms) => Ok(rooms),
Err(_) => Err(Error),
}
}
async fn get_room(self, _: Context, stoken: String, userid: String, roomid: String) -> Result<Room, ErrorCode> {
if !self.is_stoken_valid(&userid, &stoken).await {
return Err(Unauthorized)
}
self.inner_get_room(&userid, &roomid).await
}
async fn get_user(self, _: Context, userid: String) -> Result<User, ErrorCode> {
self.inner_get_user(&userid).await
}
async fn get_users(self, _: Context) -> Result<Vec<User>, ErrorCode> {
self.inner_get_all_users().await
}
async fn create_room(self, _: Context, stoken: String, userid: String, room: Room) -> Result<Room, ErrorCode> {
if !self.is_stoken_valid(&userid, &stoken).await {
return Err(Unauthorized)
}
if !self.internal_is_user_admin(&userid).await {
return Err(Unauthorized)
}
let result = query!("INSERT INTO room (roomid, admin_only_send, admin_only_view) VALUES (?,?,?)",
room.roomid, room.admin_only_send, room.admin_only_view)
.execute(&self.db_pool).await;
match result {
Ok(_) => {
// let result = self.packet_manager.lock().await.broadcast(NewRoomEvent {
// room: room.clone(),
// });
//
// if result.is_err() {
// error!("Error broadcasting NewRoomEvent!");
// }
// self.events.lock().await.push(
// (self.events.lock().await.len() as u32 + 1,
// Event::NewRoom(room.clone())));
Ok(room)
}
Err(_) => Err(MalformedDBResponse)
}
}
async fn delete_room(self, _: Context, stoken: String, userid: String, roomid: String) -> Result<(), ErrorCode> {
if !self.is_stoken_valid(&userid, &stoken).await {
return Err(Unauthorized)
}
if !self.internal_is_user_admin(&userid).await {
return Err(Unauthorized)
}
let result = query!("DELETE FROM room WHERE roomid = ?", roomid).execute(&self.db_pool).await;
match result {
Ok(_) => {
// let result = self.packet_manager.lock().await.broadcast(DeleteRoomEvent {
// roomid,
// });
//
// if result.is_err() {
// error!("Error broadcasting DeleteRoomEvent!");
// }
// self.events.lock().await.push(
// (self.events.lock().await.len() as u32 + 1,
// Event::DeleteRoom(roomid.clone())));
Ok(())
}
Err(_) => Err(MalformedDBResponse)
}
}
async fn promote_user(self, _: Context, stoken: String, admin_userid: String, userid: String) -> Result<(), ErrorCode> {
if !self.is_stoken_valid(&admin_userid, &stoken).await {
return Err(Unauthorized)
}
if !self.internal_is_user_owner(&admin_userid).await {
return Err(Unauthorized)
}
let result = query!("UPDATE user SET admin = true WHERE userid = ?", userid).execute(&self.db_pool).await;
match result {
Ok(_) => {
// let result = self.packet_manager.lock().await.broadcast(PromotedUserEvent {
// userid,
// });
//
// if result.is_err() {
// error!("Error broadcasting PromotedUserEvent!");
// }
Ok(())
}
Err(_) => Err(MalformedDBResponse)
}
}
async fn demote_user(self, _: Context, stoken: String, owner_userid: String, userid: String) -> Result<(), ErrorCode> {
if !self.is_stoken_valid(&owner_userid, &stoken).await {
return Err(Unauthorized)
}
if !self.internal_is_user_owner(&owner_userid).await {
return Err(Unauthorized)
}
let result = query!("UPDATE user SET admin = false WHERE userid = ?", userid).execute(&self.db_pool).await;
match result {
Ok(_) => {
// let result = self.packet_manager.lock().await.broadcast(DemotedUserEvent {
// userid,
// });
//
// if result.is_err() {
// error!("Error broadcasting DemotedUserEvent!");
// }
Ok(())
}
Err(_) => Err(MalformedDBResponse)
}
}
async fn kick_user(self, _: Context, stoken: String, admin_userid: String, userid: String) -> Result<(), ErrorCode> {
if !self.is_stoken_valid(&admin_userid, &stoken).await {
return Err(Unauthorized)
}
if !self.internal_is_user_admin(&admin_userid).await {
return Err(Unauthorized)
}
let result = query!("DELETE FROM user WHERE userid = ?", userid).execute(&self.db_pool).await;
match result {
Ok(_) => {
// let result = self.packet_manager.lock().await.broadcast(KickedUserEvent {
// userid,
// });
//
// if result.is_err() {
// error!("Error broadcasting KickedUserEvent!");
// }
Ok(())
}
Err(_) => Err(MalformedDBResponse)
}
}
async fn ban_user(self, _: Context, stoken: String, admin_userid: String, userid: String) -> Result<(), ErrorCode> {
if !self.is_stoken_valid(&admin_userid, &stoken).await {
return Err(Unauthorized)
}
if !self.internal_is_user_admin(&admin_userid).await {
return Err(Unauthorized)
}
query!("DELETE FROM user WHERE userid = ?", userid).execute(&self.db_pool).await.unwrap();
let result = query!("INSERT INTO banned (userid) VALUES (?)", userid).execute(&self.db_pool).await;
match result {
Ok(_) => {
// let result = self.packet_manager.lock().await.broadcast(BannedUserEvent {
// userid,
// });
//
// if result.is_err() {
// error!("Error broadcasting BannedUserEvent!");
// }
Ok(())
}
Err(_) => Err(MalformedDBResponse)
}
}
async fn pardon_user(self, _: Context, stoken: String, admin_userid: String, userid: String) -> Result<(), ErrorCode> {
if !self.internal_is_user_admin(&admin_userid).await {
return Err(Unauthorized)
}
let result = query!("DELETE FROM banned WHERE userid = ?", userid).execute(&self.db_pool).await;
match result {
Ok(_) => Ok(()),
Err(_) => Err(MalformedDBResponse)
}
}
}

View File

@@ -1,181 +0,0 @@
use chrono::{DateTime, Utc};
use sqlx::{FromRow, Row};
use sqlx::sqlite::SqliteRow;
use tarpc::serde::{Deserialize, Serialize};
use realm_shared::types::ErrorCode;
use crate::events::Event;
use crate::types::MessageData::*;
#[tarpc::service]
pub trait RealmChat {
async fn test(name: String) -> String;
async fn get_info() -> ServerInfo;
async fn is_user_admin(stoken: String) -> bool;
async fn is_user_owner(stoken: String) -> bool;
async fn poll_events_since(index: u32) -> Vec<(u32, Event)>;
async fn join_server(stoken: String, userid: String) -> Result<User, ErrorCode>;
async fn leave_server(stoken: String, userid: String) -> Result<(), ErrorCode>;
//NOTE: Any user authorized as themselves
async fn send_message(stoken: String, message: Message) -> Result<Message, ErrorCode>;
async fn start_typing(stoken: String, userid: String, roomid: String) -> ErrorCode;
async fn stop_typing(stoken: String, userid: String, roomid: String) -> ErrorCode;
async fn keep_typing(stoken: String, userid: String, roomid: String) -> ErrorCode; //NOTE: If a keep alive hasn't been received in 5 seconds, stop typing
//NOTE: Any user can call, if they are in the server
async fn get_message(stoken: String, userid: String, id: i64) -> Result<Message, ErrorCode>;
async fn get_messages_since(stoken: String, userid: String, id: i64) -> Result<Vec<Message>, ErrorCode>;
async fn get_all_direct_replies(stoken: String, userid: String, head: i64) -> Result<Vec<Message>, ErrorCode>;
async fn get_reply_chain(stoken: String, userid: String, head: Message, depth: u8) -> Result<ReplyChain, ErrorCode>;
async fn get_rooms(stoken: String, userid: String) -> Result<Vec<Room>, ErrorCode>;
async fn get_room(stoken: String, userid: String, roomid: String) -> Result<Room, ErrorCode>;
async fn get_user(userid: String) -> Result<User, ErrorCode>;
async fn get_users() -> Result<Vec<User>, ErrorCode>;
async fn create_room(stoken: String, userid: String, room: Room) -> Result<Room, ErrorCode>;
async fn delete_room(stoken: String, userid: String, roomid: String) -> Result<(), ErrorCode>;
async fn promote_user(stoken: String, admin_userid: String, userid: String) -> Result<(), ErrorCode>;
async fn demote_user(stoken: String, owner_userid: String, userid: String) -> Result<(), ErrorCode>;
async fn kick_user(stoken: String, admin_userid: String, userid: String) -> Result<(), ErrorCode>;
async fn ban_user(stoken: String, admin_userid: String, userid: String) -> Result<(), ErrorCode>;
async fn pardon_user(stoken: String, admin_userid: String, userid: String) -> Result<(), ErrorCode>;
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ServerInfo {
pub server_id: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
pub struct Message {
pub id: i64,
pub timestamp: DateTime<Utc>,
pub user: User,
pub room: Room,
#[sqlx(flatten)]
pub data: MessageData,
}
pub trait FromRows<R: Row>: Sized {
fn from_rows(rows: Vec<R>) -> Result<Vec<Self>, sqlx::Error>;
}
impl FromRows<SqliteRow> for Message {
fn from_rows(rows: Vec<SqliteRow>) -> sqlx::Result<Vec<Self>> {
let mut messages = Vec::new();
for row in rows {
messages.push(Message::from_row(&row)?);
}
Ok(messages)
}
}
impl FromRow<'_, SqliteRow> for Message {
fn from_row(row: &SqliteRow) -> sqlx::Result<Self> {
Ok(Self {
id: row.try_get("id")?,
timestamp: row.try_get("timestamp")?,
user: User {
id: row.try_get("user_id")?,
userid: row.try_get("user_userid")?,
name: row.try_get("user_name")?,
owner: row.try_get("user_owner")?,
admin: row.try_get("user_admin")?,
},
room: Room {
id: row.try_get("room_id")?,
roomid: row.try_get("room_roomid")?,
admin_only_send: row.try_get("room_admin_only_send")?,
admin_only_view: row.try_get("room_admin_only_view")?,
},
data: match row.try_get("msg_type")? {
"text" => Text(row.try_get("msg_text")?),
"attachment" => Attachment(Attachment {
}),
"reply" => Reply(Reply {
referencing_id: row.try_get("referencing_id")?,
text: row.try_get("msg_text")?,
}),
"edit" => Edit(Edit {
referencing_id: row.try_get("referencing_id")?,
text: row.try_get("msg_text")?,
}),
"reaction" => Reaction(Reaction {
referencing_id: row.try_get("referencing_id")?,
emoji: row.try_get("emoji")?,
}),
"redaction" => Redaction(Redaction {
referencing_id: row.try_get("referencing_id")?,
}),
_ => { panic!() }
},
})
}
}
//TODO: Maybe have multipart messages
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum MessageData {
Text(String),
Attachment(Attachment),
Reply(Reply),
Edit(Edit), //NOTE: Have to be the owner of the referencing_guid
Reaction(Reaction),
Redaction(Redaction), //NOTE: Have to be the owner of the referencing_guid
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Attachment {
//TODO
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Reply {
pub referencing_id: i64,
pub text: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Edit {
pub referencing_id: i64,
pub text: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Reaction {
pub referencing_id: i64,
pub emoji: String
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Redaction {
pub referencing_id: i64,
}
#[derive(Debug, Clone, Serialize, Deserialize, FromRow, PartialEq)]
pub struct User {
pub id: i64,
pub userid: String,
pub name: String,
pub owner: bool,
pub admin: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize, FromRow, PartialEq)]
pub struct Room {
pub id: i64,
pub roomid: String,
pub admin_only_send: bool,
pub admin_only_view: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReplyChain {
pub message: Message,
pub replies: Option<Vec<ReplyChain>>,
}

View File

@@ -1,9 +0,0 @@
[package]
name = "realm_shared"
version = "0.1.0"
edition = "2021"
[dependencies]
serde = { version = "1.0.203", features = ["derive"] }
sha3 = "0.10.8"
hex = "0.4.3"

View File

@@ -1,9 +0,0 @@
use sha3::digest::Update;
use sha3::{Digest, Sha3_256};
pub mod types;
pub fn stoken(token: &str, serverid: &str, domain: &str, port: u16) -> String {
let hash = Sha3_256::new().chain(format!("{}{}{}{}", token, serverid, domain, port)).finalize();
hex::encode(hash)
}

View File

@@ -1,27 +0,0 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum ErrorCode {
Error,
Unauthorized,
EmailTaken,
UsernameTaken,
InvalidLoginCode,
InvalidImage,
InvalidUsername,
InvalidEmail,
InvalidToken,
UnableToConnectToMail,
UnableToSendMail,
AlreadyJoinedServer,
NotInServer,
MessageNotFound,
RoomNotFound,
UserNotFound,
DepthTooLarge,
MalformedDBResponse,
RPCError,
UnableToConnectToServer,
}