first commit

This commit is contained in:
2025-08-25 22:41:07 +02:00
commit 0bef2b69c2
25 changed files with 6007 additions and 0 deletions

1
back/.env Normal file
View File

@@ -0,0 +1 @@
DATABASE_URL="mysql://root:root@127.0.0.1:3306/liscord_db"

1
back/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/target

1062
back/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

12
back/Cargo.toml Normal file
View File

@@ -0,0 +1,12 @@
[package]
name = "back"
version = "0.1.0"
edition = "2024"
[dependencies]
axum = "0.8.4"
diesel = { version = "2.2.0", features = ["mysql", "serde_json"] }
dotenvy = "0.15"
serde = { version = "1.0.219", features = ["derive"] }
serde_json = "1.0.143"
tokio = { version = "1.47.1", features = ["rt-multi-thread"] }

9
back/diesel.toml Normal file
View File

@@ -0,0 +1,9 @@
# For documentation on how to configure this file,
# see https://diesel.rs/guides/configuring-diesel-cli
[print_schema]
file = "src/schema.rs"
custom_type_derives = ["diesel::query_builder::QueryId", "Clone"]
[migrations_directory]
dir = "/home/clement/projects/liscord/back/migrations"

View File

@@ -0,0 +1,13 @@
services:
db:
image: mariadb:latest
volumes:
- /liscord:/var/lib/mysql/data
ports:
- "3306:3306"
environment:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: liscord_db
volumes:
db_data:

0
back/migrations/.keep Normal file
View File

View File

@@ -0,0 +1,6 @@
DROP TABLE IF EXISTS user_roles;
DROP TABLE IF EXISTS server_members;
DROP TABLE IF EXISTS messages;
DROP TABLE IF EXISTS channels;
DROP TABLE IF EXISTS servers;
DROP TABLE IF EXISTS users;

View File

@@ -0,0 +1,73 @@
-- Your SQL goes here
-- Table des utilisateurs
CREATE TABLE users (
user_id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) NOT NULL UNIQUE,
password_hash VARCHAR(255) NOT NULL,
email VARCHAR(100) NOT NULL UNIQUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Table des serveurs
CREATE TABLE servers (
server_id INT AUTO_INCREMENT PRIMARY KEY,
server_name VARCHAR(100) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Table des canaux
CREATE TABLE channels (
channel_id INT AUTO_INCREMENT PRIMARY KEY,
server_id INT NOT NULL,
channel_name VARCHAR(100) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (server_id) REFERENCES servers(server_id)
);
-- Table des messages
CREATE TABLE messages (
message_id INT AUTO_INCREMENT PRIMARY KEY,
channel_id INT NOT NULL,
user_id INT NOT NULL,
content TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (channel_id) REFERENCES channels(channel_id),
FOREIGN KEY (user_id) REFERENCES users(user_id)
);
-- Table des membres de serveur
CREATE TABLE server_members (
server_member_id INT AUTO_INCREMENT PRIMARY KEY,
server_id INT NOT NULL,
user_id INT NOT NULL,
joined_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (server_id) REFERENCES servers(server_id),
FOREIGN KEY (user_id) REFERENCES users(user_id)
);
-- Table des membres de canal
CREATE TABLE channel_members (
channel_member_id INT AUTO_INCREMENT PRIMARY KEY,
channel_id INT NOT NULL,
user_id INT NOT NULL,
joined_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (channel_id) REFERENCES channels(channel_id),
FOREIGN KEY (user_id) REFERENCES users(user_id)
);
-- Table des rôles
CREATE TABLE roles (
role_id INT AUTO_INCREMENT PRIMARY KEY,
server_id INT NOT NULL,
role_name VARCHAR(50) NOT NULL,
FOREIGN KEY (server_id) REFERENCES servers(server_id)
);
-- Table des relations utilisateur-rôle
CREATE TABLE user_roles (
user_role_id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
role_id INT NOT NULL,
FOREIGN KEY (user_id) REFERENCES users(user_id),
FOREIGN KEY (role_id) REFERENCES roles(role_id)
);

View File

@@ -0,0 +1,24 @@
use back::*;
use std::env::args;
fn main() {
let id = args()
.nth(1)
.expect("This action requires an id")
.parse::<i32>()
.expect("Invalid id");
let new_email = args().nth(2).expect("This action requires an email");
if !new_email.contains("@") {
panic!("Email invalid");
}
let connection = &mut establish_connection();
if let Ok(user) = change_user_email(id, new_email, connection) {
debug(format!("Email changed for user {}", user.username));
} else {
debug("Problem while trying to change email");
}
}

View File

@@ -0,0 +1,40 @@
use back::{models::NewUser, *};
use std::io::stdin;
fn main() {
let connection = &mut establish_connection();
let mut username = String::new();
let mut password = String::new();
let mut email = String::new();
println!("Username: ");
stdin().read_line(&mut username).unwrap();
let username = username.trim_end();
println!("Email: ",);
stdin().read_line(&mut email).unwrap();
let email = email.trim_end();
println!("Password: ",);
stdin().read_line(&mut password).unwrap();
let password = password.trim_end();
let new_user = NewUser {
username: username.to_owned(),
email: email.to_owned(),
password_hash: password.to_owned(),
};
if let Ok(user) = create_user(connection, &new_user) {
debug(format!("\nSaved user {username} with id {}", user.user_id));
} else {
debug("Saving failed");
}
}
// #[cfg(not(windows))]
// const EOF: &str = "CTRL+D";
// #[cfg(windows)]
// const EOF: &str = "CTRL+Z";

View File

@@ -0,0 +1,15 @@
use std::env::args;
use back::*;
fn main() {
let target_username = args().nth(1).expect("Expected a username");
let connection = &mut establish_connection();
if let Ok(username) = delete_user(target_username, connection) {
debug(format!("User {username} deleted"));
} else {
debug("Deletion failed");
}
}

View File

@@ -0,0 +1,19 @@
use back::establish_connection;
use back::models::*;
use diesel::prelude::*;
fn main() {
use back::schema::users::dsl::*;
let connection = &mut establish_connection();
let results = users
.limit(5)
.select(User::as_select())
.load(connection)
.expect("Error loading posts");
println!("Displaying {} posts", results.len());
for user in results {
println!("{}, {}", user.username, user.email);
}
}

70
back/src/lib.rs Normal file
View File

@@ -0,0 +1,70 @@
use diesel::prelude::*;
use dotenvy::dotenv;
use std::{env, fmt::Display};
use crate::models::{NewUser, User};
pub mod models;
pub mod schema;
// use crate::crud_macros::make_crud;
type Error = diesel::result::Error;
pub fn debug(s: impl Display) {
#[cfg(debug_assertions)]
{
println!("{s}");
}
}
pub fn establish_connection() -> MysqlConnection {
dotenv().ok();
let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set");
MysqlConnection::establish(&database_url)
.unwrap_or_else(|_| panic!("Error connecting to {database_url}"))
}
pub fn create_user(conn: &mut MysqlConnection, new_user: &NewUser) -> Result<User, Error> {
use crate::schema::users;
conn.transaction(|conn| {
diesel::insert_into(users::table)
.values(new_user)
.execute(conn)?;
users::table
.order(users::user_id.desc())
.select(User::as_select())
.first(conn)
})
}
pub fn delete_user(
target_username: String,
connection: &mut MysqlConnection,
) -> Result<String, Error> {
use crate::schema::users::dsl::*;
let nb_del = diesel::delete(users.filter(username.eq(&target_username))).execute(connection)?;
if nb_del > 0 {
Ok(target_username)
} else {
Err(diesel::result::Error::NotFound)
}
}
pub fn change_user_email(
id: i32,
new_email: String,
connection: &mut MysqlConnection,
) -> Result<User, Error> {
use crate::schema::users::dsl::*;
connection.transaction(|connection| {
let user = users.find(id).select(User::as_select()).first(connection)?;
diesel::update(users.find(id))
.set(email.eq(&new_email))
.execute(connection)?;
Ok(user)
})
}

32
back/src/main.rs Normal file
View File

@@ -0,0 +1,32 @@
use axum::{
Json, Router,
response::IntoResponse,
routing::{get, post},
};
use back::{create_user, establish_connection, models::NewUser};
#[tokio::main]
async fn main() {
let app = Router::new()
.route("/", get(hello))
.route("/create_user", post(create_user_api));
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}
async fn hello() -> &'static str {
"Hello !"
}
async fn create_user_api(Json(payload): Json<NewUser>) -> impl IntoResponse {
let mut conn = establish_connection();
match create_user(&mut conn, &payload) {
Ok(user) => (axum::http::StatusCode::CREATED, axum::Json(user)).into_response(),
Err(e) => (
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
format!("Failed to create user: {e}"),
)
.into_response(),
}
}

50
back/src/models.rs Normal file
View File

@@ -0,0 +1,50 @@
use crate::schema::*;
use diesel::prelude::*;
use serde::{Deserialize, Serialize};
#[derive(Queryable, Selectable, Serialize, Deserialize)]
#[diesel(table_name = users)]
#[diesel(check_for_backend(diesel::mysql::Mysql))]
pub struct User {
pub user_id: i32,
pub username: String,
pub password_hash: String,
pub email: String,
}
#[derive(Insertable, Serialize, Deserialize)]
#[diesel(table_name = users)]
pub struct NewUser {
pub username: String,
pub password_hash: String,
pub email: String,
}
#[derive(Queryable, Selectable, Serialize, Deserialize)]
#[diesel(table_name = servers)]
#[diesel(check_for_backend(diesel::mysql::Mysql))]
pub struct Server {
pub server_id: i32,
pub server_name: String,
}
#[derive(Insertable, Serialize, Deserialize)]
#[diesel(table_name = servers)]
pub struct NewServer {
pub server_name: String,
}
#[derive(Queryable, Serialize, Deserialize)]
#[diesel(table_name = channels)]
pub struct Channel {
pub channel_id: i32,
pub server_id: i32,
pub channel_name: String,
}
#[derive(Insertable, Serialize, Deserialize)]
#[diesel(table_name = channels)]
pub struct NewChannel {
pub server_id: i32,
pub channel_name: String,
}

100
back/src/schema.rs Normal file
View File

@@ -0,0 +1,100 @@
// @generated automatically by Diesel CLI.
diesel::table! {
channel_members (channel_member_id) {
channel_member_id -> Integer,
channel_id -> Integer,
user_id -> Integer,
joined_at -> Nullable<Timestamp>,
}
}
diesel::table! {
channels (channel_id) {
channel_id -> Integer,
server_id -> Integer,
#[max_length = 100]
channel_name -> Varchar,
created_at -> Nullable<Timestamp>,
}
}
diesel::table! {
messages (message_id) {
message_id -> Integer,
channel_id -> Integer,
user_id -> Integer,
content -> Text,
created_at -> Nullable<Timestamp>,
}
}
diesel::table! {
roles (role_id) {
role_id -> Integer,
server_id -> Integer,
#[max_length = 50]
role_name -> Varchar,
}
}
diesel::table! {
server_members (server_member_id) {
server_member_id -> Integer,
server_id -> Integer,
user_id -> Integer,
joined_at -> Nullable<Timestamp>,
}
}
diesel::table! {
servers (server_id) {
server_id -> Integer,
#[max_length = 100]
server_name -> Varchar,
created_at -> Nullable<Timestamp>,
}
}
diesel::table! {
user_roles (user_role_id) {
user_role_id -> Integer,
user_id -> Integer,
role_id -> Integer,
}
}
diesel::table! {
users (user_id) {
user_id -> Integer,
#[max_length = 50]
username -> Varchar,
#[max_length = 255]
password_hash -> Varchar,
#[max_length = 100]
email -> Varchar,
created_at -> Nullable<Timestamp>,
}
}
diesel::joinable!(channel_members -> channels (channel_id));
diesel::joinable!(channel_members -> users (user_id));
diesel::joinable!(channels -> servers (server_id));
diesel::joinable!(messages -> channels (channel_id));
diesel::joinable!(messages -> users (user_id));
diesel::joinable!(roles -> servers (server_id));
diesel::joinable!(server_members -> servers (server_id));
diesel::joinable!(server_members -> users (user_id));
diesel::joinable!(user_roles -> roles (role_id));
diesel::joinable!(user_roles -> users (user_id));
diesel::allow_tables_to_appear_in_same_query!(
channel_members,
channels,
messages,
roles,
server_members,
servers,
user_roles,
users,
);

1
front/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/target

4259
front/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

11
front/Cargo.toml Normal file
View File

@@ -0,0 +1,11 @@
[package]
name = "front"
version = "0.1.0"
edition = "2024"
[dependencies]
iced = { version = "0.13.1", features = ["debug", "image"] }
[[bin]]
name = "liscord"
path = "src/main.rs"

BIN
front/assets/liscord.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 248 KiB

15
front/src/channels.rs Normal file
View File

@@ -0,0 +1,15 @@
#[allow(dead_code)]
#[derive(Clone, Default)]
pub struct Channel {
pub server_id: i32,
pub channel_name: String,
}
impl Channel {
fn new(server_id: i32, channel_name: &str) -> Self {
Channel {
server_id,
channel_name: channel_name.to_string(),
}
}
}

98
front/src/main.rs Normal file
View File

@@ -0,0 +1,98 @@
use iced::Length::{self, Fill, FillPortion};
use iced::widget::container;
use iced::{
Background, Color, Element, Task, Theme,
widget::{Image, column, container::Style, row, text},
};
mod channels;
mod servers;
use servers::{User, get_servers_buttons};
fn main() -> Result<(), iced::Error> {
iced::application(Liscord::title, Liscord::update, Liscord::view)
.theme(Liscord::theme)
.run_with(Liscord::new)
}
struct Liscord {
user: Option<User>,
selected_server: Option<i32>,
}
#[derive(Debug, Clone)]
enum Event {
ServerPressed(i32),
}
impl Liscord {
fn new() -> (Self, Task<Event>) {
(
Liscord {
user: Some(User::default()),
selected_server: None,
},
Task::none(),
)
}
fn title(&self) -> String {
"Liscord".to_string()
}
fn update(&mut self, event: Event) -> Task<Event> {
match event {
Event::ServerPressed(server_id) => {
self.selected_server = Some(server_id);
// println!("Server Pressed ! Id: {server_id}")
}
}
Task::none()
}
fn view(&self) -> Element<'_, Event> {
let app_body = {
let server_bar = {
let mut content = column![text("Servers")];
if let Some(user) = &self.user {
content = content.push(get_servers_buttons(user, self))
}
container(content)
.padding(10)
.width(iced::Length::Fixed(300.))
.height(Fill)
.style(|_theme| Style {
background: Some(Background::Color(Color::from_rgb(0.0, 0.0, 1.0))),
..Default::default()
})
};
let message_content = container(text("Messages"))
.width(iced::Length::Fill)
.height(Fill);
row![server_bar, message_content]
}
.height(FillPortion(19));
let app_header = {
let liscord_logo = Image::new("assets/liscord.png");
let liscord_text = text("Liscord");
let centered_text = container(liscord_text)
.width(Fill)
.center_x(Length::Fill)
.center_y(Length::Fill);
row![liscord_logo, centered_text].width(Fill).height(Fill)
}
.height(FillPortion(1));
column![app_header, app_body].into()
}
fn theme(&self) -> Theme {
Theme::Dark
}
}

93
front/src/servers.rs Normal file
View File

@@ -0,0 +1,93 @@
use iced::{
Border,
widget::{Button, Column, container},
};
use iced::{Length, Shadow};
use crate::{Event, Liscord};
mod colors {
use iced::Color;
pub const BUTTON_COLOR: Color = Color::from_rgb(0.2, 0.2, 0.2);
pub const SELECTED_BUTTON_COLOR: Color = Color::from_rgb(0.2, 0.2, 1.);
}
#[allow(dead_code)]
#[derive(Default, Clone)]
pub struct Server {
pub server_id: i32,
pub server_name: String,
is_hovered: bool,
}
impl Server {
fn new(id: i32, name: &str) -> Self {
Server {
server_id: id,
server_name: name.to_string(),
..Default::default()
}
}
}
impl PartialEq for Server {
fn eq(&self, other: &Self) -> bool {
self.server_id == other.server_id
}
}
#[allow(dead_code)]
#[derive(Default)]
pub struct User {
pub user_id: i32,
pub username: String,
pub password_hash: String,
pub email: String,
}
pub fn get_servers(_user: &User) -> Vec<Server> {
let friend_serv0 = Server::new(0, "Friends0");
let friend_serv1 = Server::new(1, "Friends1");
let friend_serv2 = Server::new(2, "Friends2");
vec![friend_serv0, friend_serv1, friend_serv2]
}
pub fn get_servers_buttons<'a>(user: &User, instance: &'a Liscord) -> Column<'a, Event> {
let servers = get_servers(user);
let mut col = Column::new();
for server in servers {
col = col.push(server_button(server, instance));
}
col
}
fn server_button<'a>(server: Server, instance: &'a Liscord) -> Button<'a, Event> {
Button::new(
container(iced::widget::text(server.server_name).size(20))
.center_x(Length::Fill)
.center_y(Length::Fill),
)
.padding(10)
.width(iced::Length::Fill)
.height(Length::Fixed(100.0))
.style(move |_theme, _status| iced::widget::button::Style {
background: Some(iced::Background::Color(
if Some(server.server_id) == instance.selected_server {
colors::SELECTED_BUTTON_COLOR
} else {
colors::BUTTON_COLOR
},
)),
border: Border::default().rounded(10),
shadow: Shadow {
offset: iced::Vector { x: 3., y: 3. },
..Default::default()
},
..Default::default()
})
.on_press(Event::ServerPressed(server.server_id))
}

3
init.sh Executable file
View File

@@ -0,0 +1,3 @@
cd back/docker
docker compose up
cd ../..