Compare commits
1 Commits
master
...
issues-and
Author | SHA1 | Date | |
---|---|---|---|
|
adea6d4b4a |
@ -82,9 +82,9 @@ fabric.properties
|
||||
|
||||
/tmp/
|
||||
|
||||
crates/web/target/
|
||||
crates/web/tmp/
|
||||
crates/web/build/
|
||||
/jirs-client/target/
|
||||
/jirs-client/tmp/
|
||||
/jirs-client/build/
|
||||
|
||||
crates/bitque-server/target/
|
||||
crates/bitque-server/tmp/
|
||||
/jirs-server/target/
|
||||
/jirs-server/tmp/
|
||||
|
12
.env
12
.env
@ -1,7 +1,7 @@
|
||||
DEBUG=true
|
||||
RUST_LOG=info
|
||||
BITQUE_CLIENT_PORT=80
|
||||
BITQUE_CLIENT_BIND=bitque.lvh.me
|
||||
DATABASE_URL=postgres://postgres@localhost:5432/bitque
|
||||
BITQUE_SERVER_PORT=5000
|
||||
BITQUE_SERVER_BIND=0.0.0.0
|
||||
RUST_LOG=debug
|
||||
JIRS_CLIENT_PORT=7000
|
||||
JIRS_CLIENT_BIND=0.0.0.0
|
||||
DATABASE_URL=postgres://postgres@localhost:5432/jirs
|
||||
JIRS_SERVER_PORT=5000
|
||||
JIRS_SERVER_BIND=0.0.0.0
|
||||
|
33
.gitignore
vendored
33
.gitignore
vendored
@ -1,12 +1,25 @@
|
||||
/target
|
||||
/crates/bitque-client/pkg
|
||||
/crates/bitque-client/tmp
|
||||
/crates/bitque-client/build
|
||||
/crates/bitque-server/target
|
||||
/crates/bitque-cli/target
|
||||
/crates/bitque-bat/bat
|
||||
/crates/highlight/bitque-highlight/build
|
||||
/crates/bitque-client/src/location.rs
|
||||
/uploads
|
||||
/config
|
||||
|
||||
mail.toml
|
||||
mail.test.toml
|
||||
web.toml
|
||||
web.test.toml
|
||||
db.toml
|
||||
db.test.toml
|
||||
fs.toml
|
||||
fs.test.toml
|
||||
highlight.toml
|
||||
highlight.test.toml
|
||||
|
||||
pkg
|
||||
jirs-client/pkg
|
||||
jirs-client/tmp
|
||||
jirs-client/build
|
||||
tmp
|
||||
jirs-server/target
|
||||
jirs-cli/target
|
||||
jirs-bat/bat
|
||||
|
||||
highlight/jirs-highlight/build
|
||||
uploads
|
||||
config
|
||||
|
@ -8,7 +8,7 @@ Use nothing other than standard rust tests.
|
||||
|
||||
## Submitting changes
|
||||
|
||||
Please send a [GitHub Pull Request to bitque](https://github.com/Eraden/hirs/pull/new/master) with a clear list of what you've done (read more about [pull requests](http://help.github.com/pull-requests/)). When you send a pull request, we will love you forever if you include RSpec examples. We can always use more test coverage. Please follow our coding conventions (below) and make sure all of your commits are atomic (one feature per commit).
|
||||
Please send a [GitHub Pull Request to jirs](https://github.com/Eraden/hirs/pull/new/master) with a clear list of what you've done (read more about [pull requests](http://help.github.com/pull-requests/)). When you send a pull request, we will love you forever if you include RSpec examples. We can always use more test coverage. Please follow our coding conventions (below) and make sure all of your commits are atomic (one feature per commit).
|
||||
|
||||
Always write a clear log message for your commits. One-line messages are fine for small changes, but bigger changes should look like this:
|
||||
|
||||
@ -23,7 +23,7 @@ Start reading our code and you'll get the hang of it. We optimize for readabilit
|
||||
* We ALWAYS run `cargo fmt` before commit
|
||||
* We ALWAYS run `cargo clippy` before commit
|
||||
* We avoid local variables and prefer functions in theirs place
|
||||
* We prefer Rust over JavaScript
|
||||
* We prefer rust over JavaScript
|
||||
* We avoid putting logic in view
|
||||
* This is open source software. Consider the people who will read your code, and make it look nice for them. It's sort of like driving a car: Perhaps you love doing donuts when you're alone, but with passengers the goal is to make the ride as smooth as possible.
|
||||
|
||||
|
4416
Cargo.lock
generated
4416
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
57
Cargo.toml
57
Cargo.toml
@ -1,51 +1,30 @@
|
||||
#[package]
|
||||
#name = "bitque"
|
||||
#name = "jirs"
|
||||
#version = "0.1.0"
|
||||
#authors = ["Adrian Wozniak <adrian.wozniak@ita-prog.pl>"]
|
||||
#edition = "2018"
|
||||
#description = "JIRS (Simplified JIRA in Rust)"
|
||||
#repository = "https://gitlab.com/adrian.wozniak/bitque"
|
||||
#repository = "https://gitlab.com/adrian.wozniak/jirs"
|
||||
#license = "MPL-2.0"
|
||||
#license-file = "./LICENSE"
|
||||
|
||||
[workspace]
|
||||
members = [
|
||||
"./crates/bitque-cli",
|
||||
"./crates/bitque-server",
|
||||
"./crates/bitque-config",
|
||||
"./crates/bitque-data",
|
||||
"./crates/derive_enum_iter",
|
||||
"./crates/derive_enum_primitive",
|
||||
"./crates/derive_enum_sql",
|
||||
"./crates/derive_db_execute",
|
||||
"./crates/highlight-actor",
|
||||
"./crates/database-actor",
|
||||
"./crates/web-actor",
|
||||
"./crates/websocket-actor",
|
||||
"./crates/mail-actor",
|
||||
"./crates/cloud-storage-actor",
|
||||
"./crates/filesystem-actor",
|
||||
"./jirs-cli",
|
||||
"./jirs-server",
|
||||
"./shared/jirs-config",
|
||||
"./shared/jirs-data",
|
||||
"./derive/derive_enum_iter",
|
||||
"./derive/derive_enum_primitive",
|
||||
"./derive/derive_enum_sql",
|
||||
"./derive/derive_db_execute",
|
||||
"./actors/highlight-actor",
|
||||
"./actors/database-actor",
|
||||
"./actors/web-actor",
|
||||
"./actors/websocket-actor",
|
||||
"./actors/mail-actor",
|
||||
"./actors/amazon-actor",
|
||||
"./actors/filesystem-actor",
|
||||
# Client
|
||||
"./crates/web"
|
||||
"./jirs-client"
|
||||
]
|
||||
exclude = [
|
||||
"crates/bitque-cli",
|
||||
"crates/web",
|
||||
]
|
||||
|
||||
[workspace.dependencies]
|
||||
bitque-cli = { path = "./crates/bitque-cli" }
|
||||
bitque-server = { path = "./crates/bitque-server" }
|
||||
bitque-config = { path = "./crates/bitque-config" }
|
||||
bitque-data = { path = "./crates/bitque-data" }
|
||||
derive_enum_iter = { path = "./crates/derive_enum_iter" }
|
||||
derive_enum_primitive = { path = "./crates/derive_enum_primitive" }
|
||||
derive_enum_sql = { path = "./crates/derive_enum_sql" }
|
||||
derive_db_execute = { path = "./crates/derive_db_execute" }
|
||||
highlight-actor = { path = "./crates/highlight-actor" }
|
||||
database-actor = { path = "./crates/database-actor" }
|
||||
web-actor = { path = "./crates/web-actor" }
|
||||
websocket-actor = { path = "./crates/websocket-actor" }
|
||||
mail-actor = { path = "./crates/mail-actor" }
|
||||
cloud-storage-actor = { path = "./crates/cloud-storage-actor" }
|
||||
filesystem-actor = { path = "./crates/filesystem-actor" }
|
||||
|
15
Dockerfile.build
Normal file
15
Dockerfile.build
Normal file
@ -0,0 +1,15 @@
|
||||
FROM ubuntu:18.04
|
||||
|
||||
WORKDIR /app/
|
||||
|
||||
RUN apt-get update && apt-get install -y curl git openssl libpq-dev gcc openssl1.0 make cmake
|
||||
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- --default-toolchain nightly -y
|
||||
RUN . $HOME/.cargo/env && \
|
||||
rustup toolchain install nightly && rustup default nightly
|
||||
|
||||
RUN ls -al /app
|
||||
CMD . $HOME/.cargo/env && \
|
||||
cd ./jirs-server && \
|
||||
rm -Rf ./target/debug/jirs_server && \
|
||||
cargo build --bin jirs_server --release --no-default-features --features local-storage && \
|
||||
cp /app/target/release/jirs_server /app/build/
|
53
README.md
53
README.md
@ -1,13 +1,11 @@
|
||||
# A simplified Jira clone built with seed.rs and actix
|
||||
|
||||
![JIRS](https://raw.githubusercontent.com/Eraden/bitque/master/web/static/project-icon.svg)
|
||||
![JIRS](https://raw.githubusercontent.com/Eraden/jirs/master/jirs-client/static/project-icon.svg)
|
||||
|
||||
Server: [![builds.sr.ht status](https://builds.sr.ht/~tsumanu/bitque/server.yml.svg)](https://builds.sr.ht/~tsumanu/bitque/server.yml?)
|
||||
Client: [![builds.sr.ht status](https://builds.sr.ht/~tsumanu/bitque/client.yml.svg)](https://builds.sr.ht/~tsumanu/bitque/client.yml?)
|
||||
Server: [![builds.sr.ht status](https://builds.sr.ht/~tsumanu/jirs/server.yml.svg)](https://builds.sr.ht/~tsumanu/jirs/server.yml?)
|
||||
Client: [![builds.sr.ht status](https://builds.sr.ht/~tsumanu/jirs/client.yml.svg)](https://builds.sr.ht/~tsumanu/jirs/client.yml?)
|
||||
|
||||
Main repo: https://git.sr.ht/~tsumanu/bitque
|
||||
|
||||
Demo: https://bitque.ita-prog.pl
|
||||
https://git.sr.ht/~tsumanu/jirs
|
||||
|
||||
## Features
|
||||
|
||||
@ -60,14 +58,14 @@ Demo: https://bitque.ita-prog.pl
|
||||
* [X] Add Epic
|
||||
* [X] Edit Epic
|
||||
* [X] Delete Epic
|
||||
* [X] Epic `starts` and `ends` date
|
||||
* [ ] Epic `starts` and `ends` date
|
||||
* [X] Grouping by Epic
|
||||
* [X] Basic Rich Text Editor
|
||||
* [ ] Basic Rich Text Editor
|
||||
* [ ] Insert Code in Rich Text Editor
|
||||
* [X] Code syntax
|
||||
* [X] Personal settings to choose MDE (Markdown Editor) or RTE
|
||||
* [X] Issues and filters view
|
||||
* [X] Issues and filters working filters
|
||||
* [ ] Personal settings to choose MDE (Markdown Editor) or RTE
|
||||
* [ ] Issues and filters view
|
||||
* [ ] Issues and filters working filters
|
||||
|
||||
## How to run it
|
||||
|
||||
@ -101,7 +99,7 @@ This requires additional configuration.
|
||||
|
||||
```toml
|
||||
[filesystem]
|
||||
store_path = "/var/bitque/uploads"
|
||||
store_path = "/var/jirs/uploads"
|
||||
client_path = "/img"
|
||||
```
|
||||
|
||||
@ -128,7 +126,7 @@ region_name = "eu-central-1"
|
||||
```toml
|
||||
# db.toml
|
||||
concurrency = 2
|
||||
database_url = "postgres://postgres@localhost:5432/bitque"
|
||||
database_url = "postgres://postgres@localhost:5432/jirs"
|
||||
```
|
||||
|
||||
#### Mail Service
|
||||
@ -141,15 +139,15 @@ concurrency = 2
|
||||
user = "apikey"
|
||||
pass = "YOUR-TOKEN"
|
||||
host = "smtp.sendgrid.net"
|
||||
from = "contact@bitque.pl"
|
||||
from = "contact@jirs.pl"
|
||||
```
|
||||
|
||||
### Local variables
|
||||
|
||||
Within `bitque` directory place `.env` file with following content
|
||||
Within `jirs` directory place `.env` file with following content
|
||||
|
||||
```dotenv
|
||||
DATABASE_URL=postgres://postgres@localhost:5432/bitque
|
||||
DATABASE_URL=postgres://postgres@localhost:5432/jirs
|
||||
RUST_LOG=actix_web=info,diesel=info
|
||||
JIRS_CLIENT_PORT=7000
|
||||
JIRS_CLIENT_BIND=0.0.0.0
|
||||
@ -170,23 +168,24 @@ Requirements:
|
||||
|
||||
```bash
|
||||
cargo install diesel_cli --no-default-features --features postgres
|
||||
export DATABASE_URL=postgres://postgres@localhost/bitque
|
||||
export DATABASE_URL=postgres://postgres@localhost/jirs
|
||||
diesel setup
|
||||
diesel migration run
|
||||
|
||||
cargo run --bin bitque_server
|
||||
cargo run --bin jirs_server
|
||||
```
|
||||
|
||||
### Frontend
|
||||
|
||||
```bash
|
||||
curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
|
||||
cd bitque_client
|
||||
./web/scripts/prod.sh
|
||||
cd jirs_client
|
||||
yarn
|
||||
./scripts/prod.sh
|
||||
```
|
||||
|
||||
```bash
|
||||
sudo ln -s ./bitque.nginx /etc/nginx/sites-enabled/
|
||||
sudo ln -s ./jirs.nginx /etc/nginx/sites-enabled/
|
||||
sudo nginx -s reload
|
||||
```
|
||||
|
||||
@ -204,10 +203,10 @@ Custom element glued with WASM
|
||||
* `lang` does not have callback and it's used only on `connectedCallback`
|
||||
|
||||
```html
|
||||
<bitque-code-view lang="Rust" file-path="/some/path.rs">
|
||||
<jirs-code-view lang="Rust" file-path="/some/path.rs">
|
||||
struct Foo {
|
||||
}
|
||||
</bitque-code-view>
|
||||
</jirs-code-view>
|
||||
```
|
||||
|
||||
### Supported languages
|
||||
@ -349,11 +348,3 @@ struct Foo {
|
||||
* lrc
|
||||
* reStructuredText
|
||||
* srt
|
||||
|
||||
## Utils
|
||||
|
||||
```bash
|
||||
cargo install --locked --git https://github.com/dcchut/cargo-derivefmt --bin cargo-derivefmt
|
||||
cargo install carto-sort
|
||||
cargo install cargo-llvm-cov
|
||||
```
|
||||
|
51
actors/amazon-actor/Cargo.toml
Normal file
51
actors/amazon-actor/Cargo.toml
Normal file
@ -0,0 +1,51 @@
|
||||
[package]
|
||||
name = "amazon-actor"
|
||||
version = "0.1.0"
|
||||
authors = ["Adrian Wozniak <adrian.wozniak@ita-prog.pl>"]
|
||||
edition = "2018"
|
||||
description = "JIRS (Simplified JIRA in Rust) shared data types"
|
||||
repository = "https://gitlab.com/adrian.wozniak/jirs"
|
||||
license = "MPL-2.0"
|
||||
#license-file = "../LICENSE"
|
||||
|
||||
[lib]
|
||||
name = "amazon_actor"
|
||||
path = "./src/lib.rs"
|
||||
|
||||
[dependencies]
|
||||
serde = "*"
|
||||
|
||||
actix = { version = "0.10.0" }
|
||||
actix-service = { version = "*" }
|
||||
actix-rt = "1"
|
||||
actix-web-actors = "*"
|
||||
|
||||
bytes = { version = "0.5.6" }
|
||||
|
||||
futures = { version = "0.3.8" }
|
||||
openssl-sys = { version = "*", features = ["vendored"] }
|
||||
libc = { version = "0.2.0", default-features = false }
|
||||
|
||||
log = "0.4"
|
||||
pretty_env_logger = "0.4"
|
||||
env_logger = "0.7"
|
||||
|
||||
uuid = { version = "0.8.1", features = ["serde", "v4", "v5"] }
|
||||
|
||||
[dependencies.jirs-config]
|
||||
path = "../../shared/jirs-config"
|
||||
features = ["mail", "web", "local-storage"]
|
||||
|
||||
# Amazon S3
|
||||
[dependencies.rusoto_s3]
|
||||
version = "0.45.0"
|
||||
|
||||
[dependencies.rusoto_core]
|
||||
version = "0.45.0"
|
||||
|
||||
[dependencies.rusoto_signature]
|
||||
version = "0.45.0"
|
||||
|
||||
[dependencies.tokio]
|
||||
version = "0.2.23"
|
||||
features = ["tcp", "time", "rt-core", "fs"]
|
87
actors/amazon-actor/src/lib.rs
Normal file
87
actors/amazon-actor/src/lib.rs
Normal file
@ -0,0 +1,87 @@
|
||||
use {
|
||||
actix,
|
||||
rusoto_s3::{PutObjectRequest, S3Client, S3},
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum AmazonError {
|
||||
UploadFailed,
|
||||
}
|
||||
|
||||
pub struct AmazonExecutor;
|
||||
|
||||
impl Default for AmazonExecutor {
|
||||
fn default() -> Self {
|
||||
Self {}
|
||||
}
|
||||
}
|
||||
|
||||
impl actix::Actor for AmazonExecutor {
|
||||
type Context = actix::SyncContext<Self>;
|
||||
}
|
||||
|
||||
#[derive(actix::Message)]
|
||||
#[rtype(result = "Result<String, AmazonError>")]
|
||||
pub struct S3PutObject {
|
||||
pub source: tokio::sync::broadcast::Receiver<bytes::Bytes>,
|
||||
pub file_name: String,
|
||||
}
|
||||
|
||||
impl actix::Handler<S3PutObject> for AmazonExecutor {
|
||||
type Result = Result<String, AmazonError>;
|
||||
|
||||
fn handle(&mut self, msg: S3PutObject, _ctx: &mut Self::Context) -> Self::Result {
|
||||
let S3PutObject {
|
||||
mut source,
|
||||
file_name,
|
||||
} = msg;
|
||||
jirs_config::amazon::config().set_variables();
|
||||
|
||||
tokio::runtime::Runtime::new()
|
||||
.expect("Failed to start amazon agent")
|
||||
.block_on(async {
|
||||
let s3 = jirs_config::amazon::config();
|
||||
log::debug!("{:?}", s3);
|
||||
|
||||
// TODO: Unable to upload as stream because there is no size_hint
|
||||
// use futures::stream::*;
|
||||
// let stream = source
|
||||
// .into_stream()
|
||||
// .map_err(|_e| std::io::Error::from_raw_os_error(1));
|
||||
|
||||
let mut v: Vec<u8> = vec![];
|
||||
use bytes::Buf;
|
||||
while let Ok(b) = source.recv().await {
|
||||
v.extend_from_slice(b.bytes())
|
||||
}
|
||||
|
||||
let client = S3Client::new(s3.region());
|
||||
let put_object = PutObjectRequest {
|
||||
bucket: s3.bucket.clone(),
|
||||
key: file_name.clone(),
|
||||
// body: Some(rusoto_signature::ByteStream::new(stream)),
|
||||
body: Some(v.into()),
|
||||
..Default::default()
|
||||
};
|
||||
let id = match client.put_object(put_object).await {
|
||||
Ok(obj) => obj,
|
||||
Err(e) => {
|
||||
log::error!("{}", e);
|
||||
return Err(AmazonError::UploadFailed);
|
||||
}
|
||||
};
|
||||
log::debug!("{:?}", id);
|
||||
Ok(aws_s3_url(file_name.as_str()))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn aws_s3_url(key: &str) -> String {
|
||||
let config = jirs_config::amazon::config();
|
||||
format!(
|
||||
"https://{bucket}.s3.{region}.amazonaws.com/{key}",
|
||||
bucket = config.bucket,
|
||||
region = config.region_name,
|
||||
key = key
|
||||
)
|
||||
}
|
62
actors/database-actor/Cargo.toml
Normal file
62
actors/database-actor/Cargo.toml
Normal file
@ -0,0 +1,62 @@
|
||||
[package]
|
||||
name = "database-actor"
|
||||
version = "0.1.0"
|
||||
authors = ["Adrian Wozniak <adrian.wozniak@ita-prog.pl>"]
|
||||
edition = "2018"
|
||||
description = "JIRS (Simplified JIRA in Rust) shared data types"
|
||||
repository = "https://gitlab.com/adrian.wozniak/jirs"
|
||||
license = "MPL-2.0"
|
||||
#license-file = "../LICENSE"
|
||||
|
||||
[lib]
|
||||
name = "database_actor"
|
||||
path = "./src/lib.rs"
|
||||
|
||||
[dependencies]
|
||||
serde = "*"
|
||||
bincode = "*"
|
||||
toml = { version = "*" }
|
||||
|
||||
actix = { version = "0.10.0" }
|
||||
actix-web = { version = "*" }
|
||||
|
||||
futures = { version = "0.3.8" }
|
||||
openssl-sys = { version = "*", features = ["vendored"] }
|
||||
libc = { version = "0.2.0", default-features = false }
|
||||
|
||||
pq-sys = { version = ">=0.3.0, <0.5.0" }
|
||||
r2d2 = { version = ">= 0.8, < 0.9" }
|
||||
|
||||
dotenv = { version = "*" }
|
||||
|
||||
byteorder = "1.0"
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
time = { version = "0.1" }
|
||||
url = { version = "2.1.0" }
|
||||
percent-encoding = { version = "2.1.0" }
|
||||
uuid = { version = "0.8.1", features = ["serde", "v4", "v5"] }
|
||||
ipnetwork = { version = ">=0.12.2, <0.17.0" }
|
||||
num-bigint = { version = ">=0.1.41, <0.3" }
|
||||
num-traits = { version = "0.2" }
|
||||
num-integer = { version = "0.1.32" }
|
||||
bigdecimal = { version = ">= 0.0.10, <= 0.1.0" }
|
||||
bitflags = { version = "1.0" }
|
||||
|
||||
log = "0.4"
|
||||
pretty_env_logger = "0.4"
|
||||
env_logger = "0.7"
|
||||
|
||||
[dependencies.jirs-config]
|
||||
path = "../../shared/jirs-config"
|
||||
features = ["database"]
|
||||
|
||||
[dependencies.jirs-data]
|
||||
path = "../../shared/jirs-data"
|
||||
features = ["backend"]
|
||||
|
||||
[dependencies.derive_db_execute]
|
||||
path = "../../derive/derive_db_execute"
|
||||
|
||||
[dependencies.diesel]
|
||||
version = "1.4.5"
|
||||
features = ["unstable", "postgres", "numeric", "extras", "uuidv07"]
|
@ -1,8 +1,8 @@
|
||||
use bitque_data::User;
|
||||
use diesel::prelude::*;
|
||||
|
||||
use crate::db_find;
|
||||
use crate::tokens::FindAccessToken;
|
||||
use {
|
||||
crate::{db_find, tokens::FindAccessToken},
|
||||
diesel::prelude::*,
|
||||
jirs_data::User,
|
||||
};
|
||||
|
||||
db_find! {
|
||||
AuthorizeUser,
|
@ -1,7 +1,8 @@
|
||||
use bitque_data::{Comment, CommentId, IssueId, UserId};
|
||||
use diesel::prelude::*;
|
||||
|
||||
use crate::{db_create, db_delete, db_load, db_update};
|
||||
use {
|
||||
crate::{db_create, db_delete, db_load, db_update},
|
||||
diesel::prelude::*,
|
||||
jirs_data::{Comment, CommentId, IssueId, UserId},
|
||||
};
|
||||
|
||||
db_load! {
|
||||
LoadIssueComments,
|
@ -1,9 +1,11 @@
|
||||
use bitque_data::{DescriptionString, EndsAt, Epic, EpicId, ProjectId, StartsAt};
|
||||
use diesel::prelude::*;
|
||||
use {
|
||||
crate::{db_create, db_delete, db_load, db_update},
|
||||
derive_db_execute::Execute,
|
||||
diesel::prelude::*,
|
||||
jirs_data::{DescriptionString, Epic, EpicId, ProjectId},
|
||||
};
|
||||
|
||||
use crate::{db_create, db_delete, db_load, db_update};
|
||||
|
||||
#[derive(derive_db_execute::Execute)]
|
||||
#[derive(Execute)]
|
||||
#[db_exec(schema = "epics", result = "Epic", find = "epics.find(msg.epic_id)")]
|
||||
pub struct FindEpic {
|
||||
pub epic_id: EpicId,
|
||||
@ -34,7 +36,7 @@ db_create! {
|
||||
}
|
||||
|
||||
db_update! {
|
||||
UpdateEpicName,
|
||||
UpdateEpic,
|
||||
msg => epics => diesel::update(
|
||||
epics
|
||||
.filter(project_id.eq(msg.project_id))
|
||||
@ -46,32 +48,6 @@ db_update! {
|
||||
name => String
|
||||
}
|
||||
|
||||
db_update! {
|
||||
UpdateEpicStartsAt,
|
||||
msg => epics => diesel::update(
|
||||
epics
|
||||
.filter(project_id.eq(msg.project_id))
|
||||
.find(msg.epic_id),
|
||||
).set(starts_at.eq(msg.starts_at)),
|
||||
Epic,
|
||||
epic_id => i32,
|
||||
project_id => i32,
|
||||
starts_at => Option<StartsAt>
|
||||
}
|
||||
|
||||
db_update! {
|
||||
UpdateEpicEndsAt,
|
||||
msg => epics => diesel::update(
|
||||
epics
|
||||
.filter(project_id.eq(msg.project_id))
|
||||
.find(msg.epic_id),
|
||||
).set(ends_at.eq(msg.ends_at)),
|
||||
Epic,
|
||||
epic_id => i32,
|
||||
project_id => i32,
|
||||
ends_at => Option<EndsAt>
|
||||
}
|
||||
|
||||
db_delete! {
|
||||
DeleteEpic,
|
||||
msg => epics => diesel::delete(
|
@ -1,4 +1,4 @@
|
||||
use bitque_data::{EmailString, UsernameString};
|
||||
use jirs_data::{EmailString, UsernameString};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum OperationError {
|
||||
@ -20,7 +20,6 @@ pub enum ResourceKind {
|
||||
Project,
|
||||
Token,
|
||||
UserProject,
|
||||
UserSetting,
|
||||
User,
|
||||
Comment,
|
||||
}
|
171
actors/database-actor/src/invitations.rs
Normal file
171
actors/database-actor/src/invitations.rs
Normal file
@ -0,0 +1,171 @@
|
||||
use {
|
||||
crate::{
|
||||
db_create, db_delete, db_find, db_load, db_pool, db_update,
|
||||
tokens::CreateBindToken,
|
||||
users::{LookupUser, Register},
|
||||
DbExecutor, DbPooledConn, InvitationError,
|
||||
},
|
||||
actix::{Handler, Message},
|
||||
diesel::prelude::*,
|
||||
jirs_data::{
|
||||
EmailString, Invitation, InvitationId, InvitationState, InvitationToken, ProjectId, Token,
|
||||
User, UserId, UserRole, UsernameString,
|
||||
},
|
||||
};
|
||||
|
||||
db_find! {
|
||||
FindByBindToken,
|
||||
msg => invitations => invitations.filter(bind_token.eq(msg.token)),
|
||||
Invitation,
|
||||
token => InvitationToken
|
||||
}
|
||||
|
||||
db_load! {
|
||||
ListInvitation,
|
||||
msg => invitations => invitations
|
||||
.filter(invited_by_id.eq(msg.user_id))
|
||||
.filter(state.ne(InvitationState::Accepted))
|
||||
.order_by(state.asc())
|
||||
.then_order_by(updated_at.desc()),
|
||||
Invitation,
|
||||
user_id => UserId
|
||||
}
|
||||
|
||||
db_create! {
|
||||
CreateInvitation,
|
||||
msg => invitations => diesel::insert_into(invitations).values((
|
||||
name.eq(msg.name),
|
||||
email.eq(msg.email),
|
||||
state.eq(InvitationState::Sent),
|
||||
project_id.eq(msg.project_id),
|
||||
invited_by_id.eq(msg.user_id),
|
||||
role.eq(msg.role),
|
||||
)),
|
||||
Invitation,
|
||||
user_id => UserId,
|
||||
project_id => ProjectId,
|
||||
email => EmailString,
|
||||
name => UsernameString,
|
||||
role => UserRole
|
||||
}
|
||||
|
||||
db_delete! {
|
||||
DeleteInvitation,
|
||||
msg => invitations => diesel::delete(invitations).filter(id.eq(msg.id)),
|
||||
Invitation,
|
||||
id => InvitationId
|
||||
}
|
||||
|
||||
db_update! {
|
||||
UpdateInvitationState,
|
||||
msg => invitations => diesel::update(invitations)
|
||||
.set((
|
||||
state.eq(msg.state),
|
||||
updated_at.eq(chrono::Utc::now().naive_utc()),
|
||||
))
|
||||
.filter(id.eq(msg.id)),
|
||||
Invitation,
|
||||
id => InvitationId,
|
||||
state => InvitationState
|
||||
}
|
||||
|
||||
pub struct RevokeInvitation {
|
||||
pub id: InvitationId,
|
||||
}
|
||||
|
||||
impl Message for RevokeInvitation {
|
||||
type Result = Result<(), crate::DatabaseError>;
|
||||
}
|
||||
|
||||
impl Handler<RevokeInvitation> for DbExecutor {
|
||||
type Result = Result<(), crate::DatabaseError>;
|
||||
|
||||
fn handle(&mut self, msg: RevokeInvitation, _ctx: &mut Self::Context) -> Self::Result {
|
||||
let conn = db_pool!(self);
|
||||
UpdateInvitationState {
|
||||
id: msg.id,
|
||||
state: InvitationState::Revoked,
|
||||
}
|
||||
.execute(conn)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct AcceptInvitation {
|
||||
pub invitation_token: InvitationToken,
|
||||
}
|
||||
|
||||
impl AcceptInvitation {
|
||||
pub fn execute(self, conn: &DbPooledConn) -> Result<Token, crate::DatabaseError> {
|
||||
crate::Guard::new(conn)?.run::<Token, _>(|_guard| {
|
||||
let invitation = crate::invitations::FindByBindToken {
|
||||
token: self.invitation_token,
|
||||
}
|
||||
.execute(conn)?;
|
||||
|
||||
if invitation.state == InvitationState::Revoked {
|
||||
return Err(crate::DatabaseError::Invitation(
|
||||
InvitationError::InvitationRevoked,
|
||||
));
|
||||
}
|
||||
|
||||
crate::invitations::UpdateInvitationState {
|
||||
id: invitation.id,
|
||||
state: InvitationState::Accepted,
|
||||
}
|
||||
.execute(conn)?;
|
||||
|
||||
UpdateInvitationState {
|
||||
id: invitation.id,
|
||||
state: InvitationState::Accepted,
|
||||
}
|
||||
.execute(conn)?;
|
||||
|
||||
match {
|
||||
Register {
|
||||
name: invitation.name.clone(),
|
||||
email: invitation.email.clone(),
|
||||
project_id: Some(invitation.project_id),
|
||||
role: UserRole::User,
|
||||
}
|
||||
.execute(conn)
|
||||
} {
|
||||
Ok(_) => (),
|
||||
Err(crate::DatabaseError::User(crate::UserError::InvalidPair(..))) => (),
|
||||
Err(e) => return Err(e),
|
||||
};
|
||||
|
||||
let user: User = LookupUser {
|
||||
name: invitation.name.clone(),
|
||||
email: invitation.email.clone(),
|
||||
}
|
||||
.execute(conn)?;
|
||||
CreateBindToken { user_id: user.id }.execute(conn)?;
|
||||
|
||||
crate::user_projects::CreateUserProject {
|
||||
user_id: user.id,
|
||||
project_id: invitation.project_id,
|
||||
is_current: false,
|
||||
is_default: false,
|
||||
role: invitation.role,
|
||||
}
|
||||
.execute(conn)?;
|
||||
|
||||
crate::tokens::FindUserId { user_id: user.id }.execute(conn)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Message for AcceptInvitation {
|
||||
type Result = Result<Token, crate::DatabaseError>;
|
||||
}
|
||||
|
||||
impl Handler<AcceptInvitation> for DbExecutor {
|
||||
type Result = Result<Token, crate::DatabaseError>;
|
||||
|
||||
fn handle(&mut self, msg: AcceptInvitation, _ctx: &mut Self::Context) -> Self::Result {
|
||||
let conn = db_pool!(self);
|
||||
|
||||
msg.execute(conn)
|
||||
}
|
||||
}
|
@ -1,8 +1,8 @@
|
||||
use bitque_data::{IssueAssignee, IssueId, UserId};
|
||||
use diesel::dsl::not;
|
||||
use diesel::prelude::*;
|
||||
|
||||
use crate::{db_create, db_delete, db_load, db_load_field};
|
||||
use {
|
||||
crate::{db_create, db_delete, db_load, db_load_field},
|
||||
diesel::{expression::dsl::not, prelude::*},
|
||||
jirs_data::{IssueAssignee, IssueId, UserId},
|
||||
};
|
||||
|
||||
db_create! {
|
||||
AsignMultiple,
|
@ -1,7 +1,8 @@
|
||||
use bitque_data::{IssueStatus, IssueStatusId, Position, ProjectId, TitleString};
|
||||
use diesel::prelude::*;
|
||||
|
||||
use crate::{db_create, db_delete, db_load, db_update};
|
||||
use {
|
||||
crate::{db_create, db_delete, db_load, db_update},
|
||||
diesel::prelude::*,
|
||||
jirs_data::{IssueStatus, IssueStatusId, Position, ProjectId, TitleString},
|
||||
};
|
||||
|
||||
db_load! {
|
||||
LoadIssueStatuses,
|
@ -1,10 +1,11 @@
|
||||
use bitque_data::{IssueId, IssuePriority, IssueStatusId, IssueType, ProjectId, UserId};
|
||||
use diesel::dsl::sql;
|
||||
use diesel::prelude::*;
|
||||
use {
|
||||
crate::models::Issue,
|
||||
derive_db_execute::Execute,
|
||||
diesel::{expression::sql_literal::sql, prelude::*},
|
||||
jirs_data::{IssueId, IssuePriority, IssueStatusId, IssueType, ProjectId, UserId},
|
||||
};
|
||||
|
||||
use crate::models::Issue;
|
||||
|
||||
#[derive(Default, derive_db_execute::Execute)]
|
||||
#[derive(Default, Execute)]
|
||||
#[db_exec(
|
||||
result = "Issue",
|
||||
schema = "issues",
|
||||
@ -14,7 +15,7 @@ pub struct LoadIssue {
|
||||
pub issue_id: IssueId,
|
||||
}
|
||||
|
||||
#[derive(derive_db_execute::Execute)]
|
||||
#[derive(Execute)]
|
||||
#[db_exec(
|
||||
result = "Issue",
|
||||
schema = "issues",
|
||||
@ -24,28 +25,28 @@ pub struct LoadProjectIssues {
|
||||
pub project_id: ProjectId,
|
||||
}
|
||||
|
||||
#[derive(Default, derive_db_execute::Execute)]
|
||||
#[derive(Default, Execute)]
|
||||
#[db_exec(result = "Issue", schema = "issues")]
|
||||
pub struct UpdateIssue {
|
||||
pub issue_id: bitque_data::IssueId,
|
||||
pub issue_id: jirs_data::IssueId,
|
||||
pub title: Option<String>,
|
||||
pub issue_type: Option<IssueType>,
|
||||
pub priority: Option<IssuePriority>,
|
||||
pub list_position: Option<bitque_data::ListPosition>,
|
||||
pub list_position: Option<jirs_data::ListPosition>,
|
||||
pub description: Option<String>,
|
||||
pub description_text: Option<String>,
|
||||
pub estimate: Option<i32>,
|
||||
pub time_spent: Option<i32>,
|
||||
pub time_remaining: Option<i32>,
|
||||
pub project_id: Option<bitque_data::ProjectId>,
|
||||
pub user_ids: Option<Vec<bitque_data::UserId>>,
|
||||
pub reporter_id: Option<bitque_data::UserId>,
|
||||
pub issue_status_id: Option<bitque_data::IssueStatusId>,
|
||||
pub epic_id: Option<Option<bitque_data::EpicId>>,
|
||||
pub project_id: Option<jirs_data::ProjectId>,
|
||||
pub user_ids: Option<Vec<jirs_data::UserId>>,
|
||||
pub reporter_id: Option<jirs_data::UserId>,
|
||||
pub issue_status_id: Option<jirs_data::IssueStatusId>,
|
||||
pub epic_id: Option<Option<jirs_data::EpicId>>,
|
||||
}
|
||||
|
||||
impl UpdateIssue {
|
||||
fn execute(self, conn: &mut crate::DbPooledConn) -> Result<Issue, crate::DatabaseError> {
|
||||
fn execute(self, conn: &crate::DbPooledConn) -> Result<Issue, crate::DatabaseError> {
|
||||
let msg = self;
|
||||
use crate::schema::issues::dsl::*;
|
||||
if let Some(user_ids) = msg.user_ids {
|
||||
@ -87,7 +88,7 @@ impl UpdateIssue {
|
||||
))
|
||||
.get_result(conn)
|
||||
.map_err(|e| {
|
||||
::tracing::debug!("{:?}", e);
|
||||
log::debug!("{:?}", e);
|
||||
crate::DatabaseError::GenericFailure(
|
||||
crate::OperationError::Create,
|
||||
crate::ResourceKind::Issue,
|
||||
@ -96,7 +97,7 @@ impl UpdateIssue {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(derive_db_execute::Execute)]
|
||||
#[derive(Execute)]
|
||||
#[db_exec(
|
||||
result = "Issue",
|
||||
schema = "issues",
|
||||
@ -111,11 +112,12 @@ pub struct DeleteIssue {
|
||||
}
|
||||
|
||||
mod inner {
|
||||
use bitque_data::{IssuePriority, IssueStatusId, IssueType};
|
||||
use derive_db_execute::Execute;
|
||||
use diesel::prelude::*;
|
||||
|
||||
use crate::models::Issue;
|
||||
use {
|
||||
crate::models::Issue,
|
||||
derive_db_execute::Execute,
|
||||
diesel::prelude::*,
|
||||
jirs_data::{IssuePriority, IssueStatusId, IssueType},
|
||||
};
|
||||
|
||||
#[derive(Default, Execute)]
|
||||
#[db_exec(
|
||||
@ -152,13 +154,13 @@ mod inner {
|
||||
pub estimate: Option<i32>,
|
||||
pub time_spent: Option<i32>,
|
||||
pub time_remaining: Option<i32>,
|
||||
pub project_id: bitque_data::ProjectId,
|
||||
pub reporter_id: bitque_data::UserId,
|
||||
pub epic_id: Option<bitque_data::EpicId>,
|
||||
pub project_id: jirs_data::ProjectId,
|
||||
pub reporter_id: jirs_data::UserId,
|
||||
pub epic_id: Option<jirs_data::EpicId>,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(derive_db_execute::Execute)]
|
||||
#[derive(Execute)]
|
||||
#[db_exec(result = "Issue", schema = "issues")]
|
||||
pub struct CreateIssue {
|
||||
pub title: String,
|
||||
@ -170,39 +172,31 @@ pub struct CreateIssue {
|
||||
pub estimate: Option<i32>,
|
||||
pub time_spent: Option<i32>,
|
||||
pub time_remaining: Option<i32>,
|
||||
pub project_id: ProjectId,
|
||||
pub reporter_id: UserId,
|
||||
pub user_ids: Vec<UserId>,
|
||||
pub epic_id: Option<bitque_data::EpicId>,
|
||||
pub project_id: jirs_data::ProjectId,
|
||||
pub reporter_id: jirs_data::UserId,
|
||||
pub user_ids: Vec<jirs_data::UserId>,
|
||||
pub epic_id: Option<jirs_data::EpicId>,
|
||||
}
|
||||
|
||||
impl CreateIssue {
|
||||
fn execute(self, conn: &mut crate::DbPooledConn) -> Result<Issue, crate::DatabaseError> {
|
||||
fn execute(self, conn: &crate::DbPooledConn) -> Result<Issue, crate::DatabaseError> {
|
||||
use crate::schema::issues::dsl::*;
|
||||
let msg = self;
|
||||
|
||||
let pos = issues
|
||||
.select(sql::<diesel::sql_types::Integer>(
|
||||
"COALESCE(max(list_position), 0) + 1",
|
||||
))
|
||||
.select(sql("COALESCE(max(list_position), 0) + 1"))
|
||||
.get_result::<i32>(conn)
|
||||
.map_err(|e| {
|
||||
::tracing::error!("resolve new issue position failed {}", e);
|
||||
log::error!("resolve new issue position failed {}", e);
|
||||
crate::DatabaseError::Issue(crate::IssueError::BadListPosition)
|
||||
})?;
|
||||
let i_s_id: IssueStatusId = if msg.issue_status_id == 0 {
|
||||
crate::issue_statuses::LoadIssueStatuses {
|
||||
project_id: msg.project_id,
|
||||
}
|
||||
.execute(conn)
|
||||
.map_err(|e| {
|
||||
::tracing::error!("Failed to find issue status. {:?}", e);
|
||||
e
|
||||
})?
|
||||
.execute(conn)?
|
||||
.first()
|
||||
.ok_or(crate::DatabaseError::Issue(
|
||||
crate::IssueError::NoIssueStatuses,
|
||||
))?
|
||||
.ok_or_else(|| crate::DatabaseError::Issue(crate::IssueError::NoIssueStatuses))?
|
||||
.id
|
||||
} else {
|
||||
msg.issue_status_id
|
||||
@ -228,24 +222,13 @@ impl CreateIssue {
|
||||
reporter_id: msg.reporter_id,
|
||||
epic_id: msg.epic_id,
|
||||
}
|
||||
.execute(conn)
|
||||
.map_err(|e| {
|
||||
::tracing::error!("Failed to insert issue. {:?}", e);
|
||||
e
|
||||
})?;
|
||||
if !assign_users.is_empty() {
|
||||
crate::issue_assignees::AsignMultiple {
|
||||
issue_id: issue.id,
|
||||
user_ids: assign_users,
|
||||
}
|
||||
.execute(conn)
|
||||
.map_err(|e| {
|
||||
::tracing::error!("Failed to apply multiple assignee to issue. {:?}", e);
|
||||
e
|
||||
})?;
|
||||
}
|
||||
.execute(conn)?;
|
||||
crate::issue_assignees::AsignMultiple {
|
||||
issue_id: issue.id,
|
||||
user_ids: assign_users,
|
||||
};
|
||||
issues.find(issue.id).get_result(conn).map_err(|e| {
|
||||
::tracing::error!("{:?}", e);
|
||||
log::error!("{:?}", e);
|
||||
crate::DatabaseError::GenericFailure(
|
||||
crate::OperationError::Create,
|
||||
crate::ResourceKind::Issue,
|
109
actors/database-actor/src/lib.rs
Normal file
109
actors/database-actor/src/lib.rs
Normal file
@ -0,0 +1,109 @@
|
||||
#![recursion_limit = "256"]
|
||||
|
||||
#[macro_use]
|
||||
extern crate diesel;
|
||||
|
||||
pub use errors::*;
|
||||
use {
|
||||
actix::{Actor, SyncContext},
|
||||
diesel::pg::PgConnection,
|
||||
diesel::r2d2::{self, ConnectionManager},
|
||||
};
|
||||
|
||||
pub mod authorize_user;
|
||||
pub mod comments;
|
||||
pub mod epics;
|
||||
pub mod errors;
|
||||
pub mod invitations;
|
||||
pub mod issue_assignees;
|
||||
pub mod issue_statuses;
|
||||
pub mod issues;
|
||||
pub mod messages;
|
||||
pub mod models;
|
||||
pub mod prelude;
|
||||
pub mod projects;
|
||||
pub mod schema;
|
||||
pub mod tokens;
|
||||
pub mod user_projects;
|
||||
pub mod users;
|
||||
|
||||
pub type DbPool = r2d2::Pool<ConnectionManager<PgConnection>>;
|
||||
pub type DbPooledConn = r2d2::PooledConnection<ConnectionManager<PgConnection>>;
|
||||
|
||||
pub struct DbExecutor {
|
||||
pub pool: DbPool,
|
||||
pub config: jirs_config::database::Configuration,
|
||||
}
|
||||
|
||||
impl Actor for DbExecutor {
|
||||
type Context = SyncContext<Self>;
|
||||
}
|
||||
|
||||
impl Default for DbExecutor {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
pool: build_pool(),
|
||||
config: jirs_config::database::Configuration::read(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn build_pool() -> DbPool {
|
||||
dotenv::dotenv().ok();
|
||||
let config = jirs_config::database::Configuration::read();
|
||||
|
||||
let manager = ConnectionManager::<PgConnection>::new(config.database_url);
|
||||
r2d2::Pool::builder()
|
||||
.max_size(config.concurrency as u32)
|
||||
.build(manager)
|
||||
.unwrap_or_else(|e| panic!("Failed to create pool. {}", e))
|
||||
}
|
||||
|
||||
pub trait SyncQuery {
|
||||
type Result;
|
||||
|
||||
fn handle(&self, pool: &DbPool) -> Self::Result;
|
||||
}
|
||||
|
||||
pub struct Guard<'l> {
|
||||
conn: &'l crate::DbPooledConn,
|
||||
tm: &'l diesel::connection::AnsiTransactionManager,
|
||||
}
|
||||
|
||||
impl<'l> Guard<'l> {
|
||||
pub fn new(conn: &'l DbPooledConn) -> Result<Self, crate::DatabaseError> {
|
||||
use diesel::{connection::TransactionManager, prelude::*};
|
||||
let tm = conn.transaction_manager();
|
||||
tm.begin_transaction(conn).map_err(|e| {
|
||||
log::error!("{:?}", e);
|
||||
crate::DatabaseError::DatabaseConnectionLost
|
||||
})?;
|
||||
Ok(Self { conn, tm })
|
||||
}
|
||||
|
||||
pub fn run<R, F: FnOnce(&Guard) -> Result<R, crate::DatabaseError>>(
|
||||
&self,
|
||||
f: F,
|
||||
) -> Result<R, crate::DatabaseError> {
|
||||
use diesel::connection::TransactionManager;
|
||||
|
||||
let r = f(self);
|
||||
match r {
|
||||
Ok(r) => {
|
||||
self.tm.commit_transaction(self.conn).map_err(|e| {
|
||||
log::error!("{:?}", e);
|
||||
crate::DatabaseError::DatabaseConnectionLost
|
||||
})?;
|
||||
Ok(r)
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("{:?}", e);
|
||||
self.tm.rollback_transaction(self.conn).map_err(|e| {
|
||||
log::error!("{:?}", e);
|
||||
crate::DatabaseError::DatabaseConnectionLost
|
||||
})?;
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,8 +1,11 @@
|
||||
use bitque_data::{BindToken, Message, MessageId, MessageType, User, UserId};
|
||||
use diesel::prelude::*;
|
||||
|
||||
use crate::users::{FindUser, LookupUser};
|
||||
use crate::{db_create, db_delete, db_load};
|
||||
use {
|
||||
crate::{
|
||||
db_create, db_delete, db_load,
|
||||
users::{FindUser, LookupUser},
|
||||
},
|
||||
diesel::prelude::*,
|
||||
jirs_data::{BindToken, Message, MessageId, MessageType, User, UserId},
|
||||
};
|
||||
|
||||
db_load! {
|
||||
LoadMessages,
|
@ -1,13 +1,15 @@
|
||||
use bitque_data::{
|
||||
EpicId, InvitationState, IssuePriority, IssueStatusId, IssueType, ProjectCategory, ProjectId,
|
||||
TimeTracking, UserId,
|
||||
use {
|
||||
crate::schema::*,
|
||||
chrono::NaiveDateTime,
|
||||
jirs_data::{
|
||||
EpicId, InvitationState, IssuePriority, IssueStatusId, IssueType, ProjectCategory,
|
||||
ProjectId, TimeTracking, UserId,
|
||||
},
|
||||
serde::{Deserialize, Serialize},
|
||||
uuid::Uuid,
|
||||
};
|
||||
use chrono::NaiveDateTime;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::schema::*;
|
||||
|
||||
#[derive(Debug, Deserialize, Queryable, Serialize)]
|
||||
#[derive(Serialize, Debug, Deserialize, Queryable)]
|
||||
pub struct Issue {
|
||||
pub id: i32,
|
||||
pub title: String,
|
||||
@ -27,9 +29,9 @@ pub struct Issue {
|
||||
pub epic_id: Option<EpicId>,
|
||||
}
|
||||
|
||||
impl Into<bitque_data::Issue> for Issue {
|
||||
fn into(self) -> bitque_data::Issue {
|
||||
bitque_data::Issue {
|
||||
impl Into<jirs_data::Issue> for Issue {
|
||||
fn into(self) -> jirs_data::Issue {
|
||||
jirs_data::Issue {
|
||||
id: self.id,
|
||||
title: self.title,
|
||||
issue_type: self.issue_type,
|
||||
@ -52,8 +54,8 @@ impl Into<bitque_data::Issue> for Issue {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Insertable, Serialize)]
|
||||
#[diesel(table_name = issues)]
|
||||
#[derive(Debug, Serialize, Deserialize, Insertable)]
|
||||
#[table_name = "issues"]
|
||||
pub struct CreateIssueForm {
|
||||
pub title: String,
|
||||
pub issue_type: IssueType,
|
||||
@ -70,15 +72,15 @@ pub struct CreateIssueForm {
|
||||
pub epic_id: Option<EpicId>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Insertable, Serialize)]
|
||||
#[diesel(table_name = issue_assignees)]
|
||||
#[derive(Debug, Serialize, Deserialize, Insertable)]
|
||||
#[table_name = "issue_assignees"]
|
||||
pub struct CreateIssueAssigneeForm {
|
||||
pub issue_id: i32,
|
||||
pub user_id: i32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Insertable, Serialize)]
|
||||
#[diesel(table_name = projects)]
|
||||
#[derive(Debug, Serialize, Deserialize, Insertable)]
|
||||
#[table_name = "projects"]
|
||||
pub struct UpdateProjectForm {
|
||||
pub name: Option<String>,
|
||||
pub url: Option<String>,
|
||||
@ -87,8 +89,8 @@ pub struct UpdateProjectForm {
|
||||
pub time_tracking: Option<TimeTracking>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Insertable, Serialize)]
|
||||
#[diesel(table_name = projects)]
|
||||
#[derive(Debug, Serialize, Deserialize, Insertable)]
|
||||
#[table_name = "projects"]
|
||||
pub struct CreateProjectForm {
|
||||
pub name: String,
|
||||
pub url: String,
|
||||
@ -96,16 +98,25 @@ pub struct CreateProjectForm {
|
||||
pub category: ProjectCategory,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Insertable, Serialize)]
|
||||
#[diesel(table_name = users)]
|
||||
#[derive(Debug, Serialize, Deserialize, Insertable)]
|
||||
#[table_name = "users"]
|
||||
pub struct UserForm {
|
||||
pub name: String,
|
||||
pub email: String,
|
||||
pub avatar_url: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Insertable, Serialize)]
|
||||
#[diesel(table_name = invitations)]
|
||||
#[derive(Debug, Serialize, Deserialize, Insertable)]
|
||||
#[table_name = "tokens"]
|
||||
pub struct TokenForm {
|
||||
pub user_id: i32,
|
||||
pub access_token: Uuid,
|
||||
pub refresh_token: Uuid,
|
||||
pub bind_token: Option<Uuid>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Insertable)]
|
||||
#[table_name = "invitations"]
|
||||
pub struct InvitationForm {
|
||||
pub name: String,
|
||||
pub email: String,
|
@ -1,14 +1,14 @@
|
||||
#[macro_export]
|
||||
macro_rules! db_pool {
|
||||
($self: expr) => {
|
||||
$self.pool.get().map_err(|e| {
|
||||
::tracing::error!("{:?}", e);
|
||||
&$self.pool.get().map_err(|e| {
|
||||
log::error!("{:?}", e);
|
||||
$crate::DatabaseError::DatabaseConnectionLost
|
||||
})?
|
||||
};
|
||||
($self: expr, $pool: expr) => {
|
||||
$pool.get().map_err(|e| {
|
||||
::tracing::error!("{:?}", e);
|
||||
&$pool.get().map_err(|e| {
|
||||
log::error!("{:?}", e);
|
||||
$crate::DatabaseError::DatabaseConnectionLost
|
||||
})?
|
||||
};
|
||||
@ -18,7 +18,7 @@ macro_rules! db_pool {
|
||||
macro_rules! q {
|
||||
($q: expr) => {{
|
||||
let q = $q;
|
||||
::tracing::debug!(
|
||||
log::debug!(
|
||||
"{}",
|
||||
diesel::debug_query::<diesel::pg::Pg, _>(&q).to_string()
|
||||
);
|
||||
@ -34,13 +34,13 @@ macro_rules! db_find {
|
||||
}
|
||||
|
||||
impl $action {
|
||||
pub fn execute(self, $conn: &mut $crate::DbPooledConn) -> Result<$resource, crate::DatabaseError> {
|
||||
pub fn execute(self, $conn: &$crate::DbPooledConn) -> Result<$resource, crate::DatabaseError> {
|
||||
use crate::schema:: $schema ::dsl::*;
|
||||
let $self = self;
|
||||
$crate::q!($q)
|
||||
.first($conn)
|
||||
.map_err(|e| {
|
||||
::tracing::error!("{:?}", e);
|
||||
log::error!("{:?}", e);
|
||||
$crate::DatabaseError::GenericFailure(
|
||||
$crate::OperationError::LoadCollection,
|
||||
$crate::ResourceKind::$resource,
|
||||
@ -57,8 +57,8 @@ macro_rules! db_find {
|
||||
type Result = Result<$resource, $crate::DatabaseError>;
|
||||
|
||||
fn handle(&mut self, msg: $action, _ctx: &mut Self::Context) -> Self::Result {
|
||||
let mut $conn = $crate::db_pool!(self);
|
||||
msg.execute(&mut $conn)
|
||||
let $conn = $crate::db_pool!(self);
|
||||
msg.execute($conn)
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -75,13 +75,13 @@ macro_rules! db_load {
|
||||
}
|
||||
|
||||
impl $action {
|
||||
pub fn execute(self, conn: &mut $crate::DbPooledConn) -> Result<Vec<$resource>, $crate::DatabaseError> {
|
||||
pub fn execute(self, conn: &$crate::DbPooledConn) -> Result<Vec<$resource>, $crate::DatabaseError> {
|
||||
use crate::schema:: $schema ::dsl::*;
|
||||
let $self = self;
|
||||
$crate::q!($q)
|
||||
.load(conn)
|
||||
.map_err(|e| {
|
||||
::tracing::error!("{:?}", e);
|
||||
log::error!("{:?}", e);
|
||||
$crate::DatabaseError::GenericFailure(
|
||||
$crate::OperationError::LoadCollection,
|
||||
$crate::ResourceKind::$resource,
|
||||
@ -98,9 +98,9 @@ macro_rules! db_load {
|
||||
type Result = Result<Vec<$resource>, $crate::DatabaseError>;
|
||||
|
||||
fn handle(&mut self, msg: $action, _ctx: &mut Self::Context) -> Self::Result {
|
||||
let mut conn = $crate::db_pool!(self);
|
||||
let conn = $crate::db_pool!(self);
|
||||
|
||||
msg.execute(&mut conn)
|
||||
msg.execute(conn)
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -114,13 +114,13 @@ macro_rules! db_load_field {
|
||||
}
|
||||
|
||||
impl $action {
|
||||
pub fn execute(self, conn: &mut $crate::DbPooledConn) -> Result<Vec<$return_type>, $crate::DatabaseError> {
|
||||
pub fn execute(self, conn: &$crate::DbPooledConn) -> Result<Vec<$return_type>, $crate::DatabaseError> {
|
||||
use crate::schema:: $schema ::dsl::*;
|
||||
let $self = self;
|
||||
$crate::q!($q)
|
||||
.load(conn)
|
||||
.map_err(|e| {
|
||||
::tracing::error!("{:?}", e);
|
||||
log::error!("{:?}", e);
|
||||
$crate::DatabaseError::GenericFailure(
|
||||
$crate::OperationError::LoadCollection,
|
||||
$crate::ResourceKind::$resource,
|
||||
@ -137,9 +137,9 @@ macro_rules! db_load_field {
|
||||
type Result = Result<Vec<$return_type>, $crate::DatabaseError>;
|
||||
|
||||
fn handle(&mut self, msg: $action, _ctx: &mut Self::Context) -> Self::Result {
|
||||
let mut conn = $crate::db_pool!(self);
|
||||
let conn = $crate::db_pool!(self);
|
||||
|
||||
msg.execute(&mut conn)
|
||||
msg.execute(conn)
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -151,34 +151,25 @@ macro_rules! db_create {
|
||||
};
|
||||
($action: ident, $self: ident => $conn: ident => $schema: ident => $q: expr, $resource: ident, $($field: ident => $ty: ty),+) => {
|
||||
pub struct $action {
|
||||
$(pub $field : $ty),+
|
||||
$(pub $field : $ty),+
|
||||
}
|
||||
|
||||
impl $action {
|
||||
pub fn execute(self, conn: &mut $crate::DbPooledConn) -> Result<$resource, crate::DatabaseError> {
|
||||
let mut res = Err(crate::DatabaseError::DatabaseConnectionLost);
|
||||
conn.transaction(|conn| {
|
||||
res = self.exec_with_transaction(conn);
|
||||
if res.is_err() {
|
||||
Err(diesel::NotFound)
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}).ok();
|
||||
res
|
||||
}
|
||||
|
||||
fn exec_with_transaction(self, $conn: &mut $crate::DbPooledConn) -> Result<$resource, crate::DatabaseError> {
|
||||
use crate::schema:: $schema ::dsl::*;
|
||||
let $self = self;
|
||||
$crate::q!($q).get_result::<$resource>($conn).map_err(|e| {
|
||||
::tracing::error!("{:?}", e);
|
||||
pub fn execute(self, $conn: &$crate::DbPooledConn) -> Result<$resource, crate::DatabaseError> {
|
||||
crate::Guard::new($conn)?.run(|_guard| {
|
||||
use crate::schema:: $schema ::dsl::*;
|
||||
let $self = self;
|
||||
$crate::q!($q)
|
||||
.get_result::<$resource>($conn)
|
||||
.map_err(|e| {
|
||||
log::error!("{:?}", e);
|
||||
$crate::DatabaseError::GenericFailure(
|
||||
$crate::OperationError::Create,
|
||||
$crate::ResourceKind::$resource,
|
||||
)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl actix::Message for $action {
|
||||
@ -189,9 +180,9 @@ macro_rules! db_create {
|
||||
type Result = Result<$resource, $crate::DatabaseError>;
|
||||
|
||||
fn handle(&mut self, msg: $action, _ctx: &mut Self::Context) -> Self::Result {
|
||||
let mut $conn = $crate::db_pool!(self);
|
||||
let $conn = $crate::db_pool!(self);
|
||||
|
||||
msg.execute(&mut $conn)
|
||||
msg.execute($conn)
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -208,13 +199,13 @@ macro_rules! db_update {
|
||||
}
|
||||
|
||||
impl $action {
|
||||
pub fn execute(self, $conn: &mut $crate::DbPooledConn) -> Result<$resource, crate::DatabaseError> {
|
||||
pub fn execute(self, $conn: &$crate::DbPooledConn) -> Result<$resource, crate::DatabaseError> {
|
||||
use crate::schema:: $schema ::dsl::*;
|
||||
let $self = self;
|
||||
$crate::q!($q)
|
||||
.get_result::<$resource>($conn)
|
||||
.map_err(|e| {
|
||||
::tracing::error!("{:?}", e);
|
||||
log::error!("{:?}", e);
|
||||
$crate::DatabaseError::GenericFailure(
|
||||
$crate::OperationError::Update,
|
||||
$crate::ResourceKind::$resource,
|
||||
@ -231,9 +222,9 @@ macro_rules! db_update {
|
||||
type Result = Result<$resource, $crate::DatabaseError>;
|
||||
|
||||
fn handle(&mut self, msg: $action, _ctx: &mut Self::Context) -> Self::Result {
|
||||
let mut $conn = $crate::db_pool!(self);
|
||||
let $conn = $crate::db_pool!(self);
|
||||
|
||||
msg.execute ( &mut $conn )
|
||||
msg.execute ( $conn )
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -250,13 +241,13 @@ macro_rules! db_delete {
|
||||
}
|
||||
|
||||
impl $action {
|
||||
pub fn execute(self, $conn: &mut $crate::DbPooledConn) -> Result<usize, $crate::DatabaseError> {
|
||||
pub fn execute(self, $conn: &$crate::DbPooledConn) -> Result<usize, $crate::DatabaseError> {
|
||||
use $crate::schema:: $schema ::dsl::*;
|
||||
let $self = self;
|
||||
$crate::q!($q)
|
||||
.execute($conn)
|
||||
.map_err(|e| {
|
||||
::tracing::error!("{:?}", e);
|
||||
log::error!("{:?}", e);
|
||||
$crate::DatabaseError::GenericFailure(
|
||||
$crate::OperationError::Delete,
|
||||
$crate::ResourceKind::$resource,
|
||||
@ -273,9 +264,9 @@ macro_rules! db_delete {
|
||||
type Result = Result<usize, $crate::DatabaseError>;
|
||||
|
||||
fn handle(&mut self, msg: $action, _ctx: &mut Self::Context) -> Self::Result {
|
||||
let mut $conn = $crate::db_pool!(self);
|
||||
let $conn = $crate::db_pool!(self);
|
||||
|
||||
msg.execute(&mut $conn)
|
||||
msg.execute($conn)
|
||||
}
|
||||
}
|
||||
};
|
@ -1,7 +1,8 @@
|
||||
use bitque_data::{NameString, Project, ProjectCategory, ProjectId, TimeTracking, UserId};
|
||||
use diesel::prelude::*;
|
||||
|
||||
use crate::{db_create, db_find, db_load, db_update};
|
||||
use {
|
||||
crate::{db_create, db_find, db_load, db_update},
|
||||
diesel::prelude::*,
|
||||
jirs_data::{NameString, Project, ProjectCategory, ProjectId, TimeTracking, UserId},
|
||||
};
|
||||
|
||||
db_find! {
|
||||
LoadCurrentProject,
|
||||
@ -11,10 +12,11 @@ db_find! {
|
||||
}
|
||||
|
||||
mod inner {
|
||||
use bitque_data::{NameString, Project, ProjectCategory, TimeTracking};
|
||||
use diesel::prelude::*;
|
||||
|
||||
use crate::db_create;
|
||||
use {
|
||||
crate::db_create,
|
||||
diesel::prelude::*,
|
||||
jirs_data::{NameString, Project, ProjectCategory, TimeTracking},
|
||||
};
|
||||
|
||||
db_create! {
|
||||
CreateProject,
|
@ -1,13 +1,13 @@
|
||||
diff --git a/bitque-server/src/schema.rs b/bitque-server/src/schema.rs
|
||||
diff --git a/jirs-server/src/schema.rs b/jirs-server/src/schema.rs
|
||||
index 00d1c0b..5b82ccf 100644
|
||||
--- a/bitque-server/src/schema.rs
|
||||
+++ b/bitque-server/src/schema.rs
|
||||
--- a/jirs-server/src/schema.rs
|
||||
+++ b/jirs-server/src/schema.rs
|
||||
@@ -1,6 +1,8 @@
|
||||
+#![allow(unused_imports, dead_code)]
|
||||
+
|
||||
table! {
|
||||
use diesel::sql_types::*;
|
||||
use bitque_data::*;
|
||||
use jirs_data::*;
|
||||
|
||||
/// Representation of the `comments` table.
|
||||
///
|
699
actors/database-actor/src/schema.rs
Normal file
699
actors/database-actor/src/schema.rs
Normal file
@ -0,0 +1,699 @@
|
||||
#![allow(unused_imports, dead_code)]
|
||||
|
||||
table! {
|
||||
use diesel::sql_types::*;
|
||||
use jirs_data::*;
|
||||
|
||||
/// Representation of the `comments` table.
|
||||
///
|
||||
/// (Automatically generated by Diesel.)
|
||||
comments (id) {
|
||||
/// The `id` column of the `comments` table.
|
||||
///
|
||||
/// Its SQL type is `Int4`.
|
||||
///
|
||||
/// (Automatically generated by Diesel.)
|
||||
id -> Int4,
|
||||
/// The `body` column of the `comments` table.
|
||||
///
|
||||
/// Its SQL type is `Text`.
|
||||
///
|
||||
/// (Automatically generated by Diesel.)
|
||||
body -> Text,
|
||||
/// The `user_id` column of the `comments` table.
|
||||
///
|
||||
/// Its SQL type is `Int4`.
|
||||
///
|
||||
/// (Automatically generated by Diesel.)
|
||||
user_id -> Int4,
|
||||
/// The `issue_id` column of the `comments` table.
|
||||
///
|
||||
/// Its SQL type is `Int4`.
|
||||
///
|
||||
/// (Automatically generated by Diesel.)
|
||||
issue_id -> Int4,
|
||||
/// The `created_at` column of the `comments` table.
|
||||
///
|
||||
/// Its SQL type is `Timestamp`.
|
||||
///
|
||||
/// (Automatically generated by Diesel.)
|
||||
created_at -> Timestamp,
|
||||
/// The `updated_at` column of the `comments` table.
|
||||
///
|
||||
/// Its SQL type is `Timestamp`.
|
||||
///
|
||||
/// (Automatically generated by Diesel.)
|
||||
updated_at -> Timestamp,
|
||||
}
|
||||
}
|
||||
|
||||
table! {
|
||||
use diesel::sql_types::*;
|
||||
use jirs_data::*;
|
||||
|
||||
/// Representation of the `epics` table.
|
||||
///
|
||||
/// (Automatically generated by Diesel.)
|
||||
epics (id) {
|
||||
/// The `id` column of the `epics` table.
|
||||
///
|
||||
/// Its SQL type is `Int4`.
|
||||
///
|
||||
/// (Automatically generated by Diesel.)
|
||||
id -> Int4,
|
||||
/// The `name` column of the `epics` table.
|
||||
///
|
||||
/// Its SQL type is `Text`.
|
||||
///
|
||||
/// (Automatically generated by Diesel.)
|
||||
name -> Text,
|
||||
/// The `user_id` column of the `epics` table.
|
||||
///
|
||||
/// Its SQL type is `Int4`.
|
||||
///
|
||||
/// (Automatically generated by Diesel.)
|
||||
user_id -> Int4,
|
||||
/// The `project_id` column of the `epics` table.
|
||||
///
|
||||
/// Its SQL type is `Int4`.
|
||||
///
|
||||
/// (Automatically generated by Diesel.)
|
||||
project_id -> Int4,
|
||||
/// The `created_at` column of the `epics` table.
|
||||
///
|
||||
/// Its SQL type is `Timestamp`.
|
||||
///
|
||||
/// (Automatically generated by Diesel.)
|
||||
created_at -> Timestamp,
|
||||
/// The `updated_at` column of the `epics` table.
|
||||
///
|
||||
/// Its SQL type is `Timestamp`.
|
||||
///
|
||||
/// (Automatically generated by Diesel.)
|
||||
updated_at -> Timestamp,
|
||||
/// The `starts_at` column of the `epics` table.
|
||||
///
|
||||
/// Its SQL type is `Nullable<Timestamp>`.
|
||||
///
|
||||
/// (Automatically generated by Diesel.)
|
||||
starts_at -> Nullable<Timestamp>,
|
||||
/// The `ends_at` column of the `epics` table.
|
||||
///
|
||||
/// Its SQL type is `Nullable<Timestamp>`.
|
||||
///
|
||||
/// (Automatically generated by Diesel.)
|
||||
ends_at -> Nullable<Timestamp>,
|
||||
/// The `description` column of the `epics` table.
|
||||
///
|
||||
/// Its SQL type is `Nullable<Text>`.
|
||||
///
|
||||
/// (Automatically generated by Diesel.)
|
||||
description -> Nullable<Text>,
|
||||
/// The `description_html` column of the `epics` table.
|
||||
///
|
||||
/// Its SQL type is `Nullable<Text>`.
|
||||
///
|
||||
/// (Automatically generated by Diesel.)
|
||||
description_html -> Nullable<Text>,
|
||||
}
|
||||
}
|
||||
|
||||
table! {
|
||||
use diesel::sql_types::*;
|
||||
use jirs_data::*;
|
||||
|
||||
/// Representation of the `invitations` table.
|
||||
///
|
||||
/// (Automatically generated by Diesel.)
|
||||
invitations (id) {
|
||||
/// The `id` column of the `invitations` table.
|
||||
///
|
||||
/// Its SQL type is `Int4`.
|
||||
///
|
||||
/// (Automatically generated by Diesel.)
|
||||
id -> Int4,
|
||||
/// The `name` column of the `invitations` table.
|
||||
///
|
||||
/// Its SQL type is `Text`.
|
||||
///
|
||||
/// (Automatically generated by Diesel.)
|
||||
name -> Text,
|
||||
/// The `email` column of the `invitations` table.
|
||||
///
|
||||
/// Its SQL type is `Text`.
|
||||
///
|
||||
/// (Automatically generated by Diesel.)
|
||||
email -> Text,
|
||||
/// The `state` column of the `invitations` table.
|
||||
///
|
||||
/// Its SQL type is `InvitationStateType`.
|
||||
///
|
||||
/// (Automatically generated by Diesel.)
|
||||
state -> InvitationStateType,
|
||||
/// The `project_id` column of the `invitations` table.
|
||||
///
|
||||
/// Its SQL type is `Int4`.
|
||||
///
|
||||
/// (Automatically generated by Diesel.)
|
||||
project_id -> Int4,
|
||||
/// The `invited_by_id` column of the `invitations` table.
|
||||
///
|
||||
/// Its SQL type is `Int4`.
|
||||
///
|
||||
/// (Automatically generated by Diesel.)
|
||||
invited_by_id -> Int4,
|
||||
/// The `created_at` column of the `invitations` table.
|
||||
///
|
||||
/// Its SQL type is `Timestamp`.
|
||||
///
|
||||
/// (Automatically generated by Diesel.)
|
||||
created_at -> Timestamp,
|
||||
/// The `updated_at` column of the `invitations` table.
|
||||
///
|
||||
/// Its SQL type is `Timestamp`.
|
||||
///
|
||||
/// (Automatically generated by Diesel.)
|
||||
updated_at -> Timestamp,
|
||||
/// The `bind_token` column of the `invitations` table.
|
||||
///
|
||||
/// Its SQL type is `Uuid`.
|
||||
///
|
||||
/// (Automatically generated by Diesel.)
|
||||
bind_token -> Uuid,
|
||||
/// The `role` column of the `invitations` table.
|
||||
///
|
||||
/// Its SQL type is `UserRoleType`.
|
||||
///
|
||||
/// (Automatically generated by Diesel.)
|
||||
role -> UserRoleType,
|
||||
}
|
||||
}
|
||||
|
||||
table! {
|
||||
use diesel::sql_types::*;
|
||||
use jirs_data::*;
|
||||
|
||||
/// Representation of the `issue_assignees` table.
|
||||
///
|
||||
/// (Automatically generated by Diesel.)
|
||||
issue_assignees (id) {
|
||||
/// The `id` column of the `issue_assignees` table.
|
||||
///
|
||||
/// Its SQL type is `Int4`.
|
||||
///
|
||||
/// (Automatically generated by Diesel.)
|
||||
id -> Int4,
|
||||
/// The `issue_id` column of the `issue_assignees` table.
|
||||
///
|
||||
/// Its SQL type is `Int4`.
|
||||
///
|
||||
/// (Automatically generated by Diesel.)
|
||||
issue_id -> Int4,
|
||||
/// The `user_id` column of the `issue_assignees` table.
|
||||
///
|
||||
/// Its SQL type is `Int4`.
|
||||
///
|
||||
/// (Automatically generated by Diesel.)
|
||||
user_id -> Int4,
|
||||
/// The `created_at` column of the `issue_assignees` table.
|
||||
///
|
||||
/// Its SQL type is `Timestamp`.
|
||||
///
|
||||
/// (Automatically generated by Diesel.)
|
||||
created_at -> Timestamp,
|
||||
/// The `updated_at` column of the `issue_assignees` table.
|
||||
///
|
||||
/// Its SQL type is `Timestamp`.
|
||||
///
|
||||
/// (Automatically generated by Diesel.)
|
||||
updated_at -> Timestamp,
|
||||
}
|
||||
}
|
||||
|
||||
table! {
|
||||
use diesel::sql_types::*;
|
||||
use jirs_data::*;
|
||||
|
||||
/// Representation of the `issue_statuses` table.
|
||||
///
|
||||
/// (Automatically generated by Diesel.)
|
||||
issue_statuses (id) {
|
||||
/// The `id` column of the `issue_statuses` table.
|
||||
///
|
||||
/// Its SQL type is `Int4`.
|
||||
///
|
||||
/// (Automatically generated by Diesel.)
|
||||
id -> Int4,
|
||||
/// The `name` column of the `issue_statuses` table.
|
||||
///
|
||||
/// Its SQL type is `Varchar`.
|
||||
///
|
||||
/// (Automatically generated by Diesel.)
|
||||
name -> Varchar,
|
||||
/// The `position` column of the `issue_statuses` table.
|
||||
///
|
||||
/// Its SQL type is `Int4`.
|
||||
///
|
||||
/// (Automatically generated by Diesel.)
|
||||
position -> Int4,
|
||||
/// The `project_id` column of the `issue_statuses` table.
|
||||
///
|
||||
/// Its SQL type is `Int4`.
|
||||
///
|
||||
/// (Automatically generated by Diesel.)
|
||||
project_id -> Int4,
|
||||
/// The `created_at` column of the `issue_statuses` table.
|
||||
///
|
||||
/// Its SQL type is `Timestamp`.
|
||||
///
|
||||
/// (Automatically generated by Diesel.)
|
||||
created_at -> Timestamp,
|
||||
/// The `updated_at` column of the `issue_statuses` table.
|
||||
///
|
||||
/// Its SQL type is `Timestamp`.
|
||||
///
|
||||
/// (Automatically generated by Diesel.)
|
||||
updated_at -> Timestamp,
|
||||
}
|
||||
}
|
||||
|
||||
table! {
|
||||
use diesel::sql_types::*;
|
||||
use jirs_data::*;
|
||||
|
||||
/// Representation of the `issues` table.
|
||||
///
|
||||
/// (Automatically generated by Diesel.)
|
||||
issues (id) {
|
||||
/// The `id` column of the `issues` table.
|
||||
///
|
||||
/// Its SQL type is `Int4`.
|
||||
///
|
||||
/// (Automatically generated by Diesel.)
|
||||
id -> Int4,
|
||||
/// The `title` column of the `issues` table.
|
||||
///
|
||||
/// Its SQL type is `Text`.
|
||||
///
|
||||
/// (Automatically generated by Diesel.)
|
||||
title -> Text,
|
||||
/// The `issue_type` column of the `issues` table.
|
||||
///
|
||||
/// Its SQL type is `IssueTypeType`.
|
||||
///
|
||||
/// (Automatically generated by Diesel.)
|
||||
issue_type -> IssueTypeType,
|
||||
/// The `priority` column of the `issues` table.
|
||||
///
|
||||
/// Its SQL type is `IssuePriorityType`.
|
||||
///
|
||||
/// (Automatically generated by Diesel.)
|
||||
priority -> IssuePriorityType,
|
||||
/// The `list_position` column of the `issues` table.
|
||||
///
|
||||
/// Its SQL type is `Int4`.
|
||||
///
|
||||
/// (Automatically generated by Diesel.)
|
||||
list_position -> Int4,
|
||||
/// The `description` column of the `issues` table.
|
||||
///
|
||||
/// Its SQL type is `Nullable<Text>`.
|
||||
///
|
||||
/// (Automatically generated by Diesel.)
|
||||
description -> Nullable<Text>,
|
||||
/// The `description_text` column of the `issues` table.
|
||||
///
|
||||
/// Its SQL type is `Nullable<Text>`.
|
||||
///
|
||||
/// (Automatically generated by Diesel.)
|
||||
description_text -> Nullable<Text>,
|
||||
/// The `estimate` column of the `issues` table.
|
||||
///
|
||||
/// Its SQL type is `Nullable<Int4>`.
|
||||
///
|
||||
/// (Automatically generated by Diesel.)
|
||||
estimate -> Nullable<Int4>,
|
||||
/// The `time_spent` column of the `issues` table.
|
||||
///
|
||||
/// Its SQL type is `Nullable<Int4>`.
|
||||
///
|
||||
/// (Automatically generated by Diesel.)
|
||||
time_spent -> Nullable<Int4>,
|
||||
/// The `time_remaining` column of the `issues` table.
|
||||
///
|
||||
/// Its SQL type is `Nullable<Int4>`.
|
||||
///
|
||||
/// (Automatically generated by Diesel.)
|
||||
time_remaining -> Nullable<Int4>,
|
||||
/// The `reporter_id` column of the `issues` table.
|
||||
///
|
||||
/// Its SQL type is `Int4`.
|
||||
///
|
||||
/// (Automatically generated by Diesel.)
|
||||
reporter_id -> Int4,
|
||||
/// The `project_id` column of the `issues` table.
|
||||
///
|
||||
/// Its SQL type is `Int4`.
|
||||
///
|
||||
/// (Automatically generated by Diesel.)
|
||||
project_id -> Int4,
|
||||
/// The `created_at` column of the `issues` table.
|
||||
///
|
||||
/// Its SQL type is `Timestamp`.
|
||||
///
|
||||
/// (Automatically generated by Diesel.)
|
||||
created_at -> Timestamp,
|
||||
/// The `updated_at` column of the `issues` table.
|
||||
///
|
||||
/// Its SQL type is `Timestamp`.
|
||||
///
|
||||
/// (Automatically generated by Diesel.)
|
||||
updated_at -> Timestamp,
|
||||
/// The `issue_status_id` column of the `issues` table.
|
||||
///
|
||||
/// Its SQL type is `Int4`.
|
||||
///
|
||||
/// (Automatically generated by Diesel.)
|
||||
issue_status_id -> Int4,
|
||||
/// The `epic_id` column of the `issues` table.
|
||||
///
|
||||
/// Its SQL type is `Nullable<Int4>`.
|
||||
///
|
||||
/// (Automatically generated by Diesel.)
|
||||
epic_id -> Nullable<Int4>,
|
||||
}
|
||||
}
|
||||
|
||||
table! {
|
||||
use diesel::sql_types::*;
|
||||
use jirs_data::*;
|
||||
|
||||
/// Representation of the `messages` table.
|
||||
///
|
||||
/// (Automatically generated by Diesel.)
|
||||
messages (id) {
|
||||
/// The `id` column of the `messages` table.
|
||||
///
|
||||
/// Its SQL type is `Int4`.
|
||||
///
|
||||
/// (Automatically generated by Diesel.)
|
||||
id -> Int4,
|
||||
/// The `receiver_id` column of the `messages` table.
|
||||
///
|
||||
/// Its SQL type is `Int4`.
|
||||
///
|
||||
/// (Automatically generated by Diesel.)
|
||||
receiver_id -> Int4,
|
||||
/// The `sender_id` column of the `messages` table.
|
||||
///
|
||||
/// Its SQL type is `Int4`.
|
||||
///
|
||||
/// (Automatically generated by Diesel.)
|
||||
sender_id -> Int4,
|
||||
/// The `summary` column of the `messages` table.
|
||||
///
|
||||
/// Its SQL type is `Text`.
|
||||
///
|
||||
/// (Automatically generated by Diesel.)
|
||||
summary -> Text,
|
||||
/// The `description` column of the `messages` table.
|
||||
///
|
||||
/// Its SQL type is `Text`.
|
||||
///
|
||||
/// (Automatically generated by Diesel.)
|
||||
description -> Text,
|
||||
/// The `message_type` column of the `messages` table.
|
||||
///
|
||||
/// Its SQL type is `MessageTypeType`.
|
||||
///
|
||||
/// (Automatically generated by Diesel.)
|
||||
message_type -> MessageTypeType,
|
||||
/// The `hyper_link` column of the `messages` table.
|
||||
///
|
||||
/// Its SQL type is `Text`.
|
||||
///
|
||||
/// (Automatically generated by Diesel.)
|
||||
hyper_link -> Text,
|
||||
/// The `created_at` column of the `messages` table.
|
||||
///
|
||||
/// Its SQL type is `Timestamp`.
|
||||
///
|
||||
/// (Automatically generated by Diesel.)
|
||||
created_at -> Timestamp,
|
||||
/// The `updated_at` column of the `messages` table.
|
||||
///
|
||||
/// Its SQL type is `Timestamp`.
|
||||
///
|
||||
/// (Automatically generated by Diesel.)
|
||||
updated_at -> Timestamp,
|
||||
}
|
||||
}
|
||||
|
||||
table! {
|
||||
use diesel::sql_types::*;
|
||||
use jirs_data::*;
|
||||
|
||||
/// Representation of the `projects` table.
|
||||
///
|
||||
/// (Automatically generated by Diesel.)
|
||||
projects (id) {
|
||||
/// The `id` column of the `projects` table.
|
||||
///
|
||||
/// Its SQL type is `Int4`.
|
||||
///
|
||||
/// (Automatically generated by Diesel.)
|
||||
id -> Int4,
|
||||
/// The `name` column of the `projects` table.
|
||||
///
|
||||
/// Its SQL type is `Text`.
|
||||
///
|
||||
/// (Automatically generated by Diesel.)
|
||||
name -> Text,
|
||||
/// The `url` column of the `projects` table.
|
||||
///
|
||||
/// Its SQL type is `Text`.
|
||||
///
|
||||
/// (Automatically generated by Diesel.)
|
||||
url -> Text,
|
||||
/// The `description` column of the `projects` table.
|
||||
///
|
||||
/// Its SQL type is `Text`.
|
||||
///
|
||||
/// (Automatically generated by Diesel.)
|
||||
description -> Text,
|
||||
/// The `category` column of the `projects` table.
|
||||
///
|
||||
/// Its SQL type is `ProjectCategoryType`.
|
||||
///
|
||||
/// (Automatically generated by Diesel.)
|
||||
category -> ProjectCategoryType,
|
||||
/// The `created_at` column of the `projects` table.
|
||||
///
|
||||
/// Its SQL type is `Timestamp`.
|
||||
///
|
||||
/// (Automatically generated by Diesel.)
|
||||
created_at -> Timestamp,
|
||||
/// The `updated_at` column of the `projects` table.
|
||||
///
|
||||
/// Its SQL type is `Timestamp`.
|
||||
///
|
||||
/// (Automatically generated by Diesel.)
|
||||
updated_at -> Timestamp,
|
||||
/// The `time_tracking` column of the `projects` table.
|
||||
///
|
||||
/// Its SQL type is `TimeTrackingType`.
|
||||
///
|
||||
/// (Automatically generated by Diesel.)
|
||||
time_tracking -> TimeTrackingType,
|
||||
}
|
||||
}
|
||||
|
||||
table! {
|
||||
use diesel::sql_types::*;
|
||||
use jirs_data::*;
|
||||
|
||||
/// Representation of the `tokens` table.
|
||||
///
|
||||
/// (Automatically generated by Diesel.)
|
||||
tokens (id) {
|
||||
/// The `id` column of the `tokens` table.
|
||||
///
|
||||
/// Its SQL type is `Int4`.
|
||||
///
|
||||
/// (Automatically generated by Diesel.)
|
||||
id -> Int4,
|
||||
/// The `user_id` column of the `tokens` table.
|
||||
///
|
||||
/// Its SQL type is `Int4`.
|
||||
///
|
||||
/// (Automatically generated by Diesel.)
|
||||
user_id -> Int4,
|
||||
/// The `access_token` column of the `tokens` table.
|
||||
///
|
||||
/// Its SQL type is `Uuid`.
|
||||
///
|
||||
/// (Automatically generated by Diesel.)
|
||||
access_token -> Uuid,
|
||||
/// The `refresh_token` column of the `tokens` table.
|
||||
///
|
||||
/// Its SQL type is `Uuid`.
|
||||
///
|
||||
/// (Automatically generated by Diesel.)
|
||||
refresh_token -> Uuid,
|
||||
/// The `created_at` column of the `tokens` table.
|
||||
///
|
||||
/// Its SQL type is `Timestamp`.
|
||||
///
|
||||
/// (Automatically generated by Diesel.)
|
||||
created_at -> Timestamp,
|
||||
/// The `updated_at` column of the `tokens` table.
|
||||
///
|
||||
/// Its SQL type is `Timestamp`.
|
||||
///
|
||||
/// (Automatically generated by Diesel.)
|
||||
updated_at -> Timestamp,
|
||||
/// The `bind_token` column of the `tokens` table.
|
||||
///
|
||||
/// Its SQL type is `Nullable<Uuid>`.
|
||||
///
|
||||
/// (Automatically generated by Diesel.)
|
||||
bind_token -> Nullable<Uuid>,
|
||||
}
|
||||
}
|
||||
|
||||
table! {
|
||||
use diesel::sql_types::*;
|
||||
use jirs_data::*;
|
||||
|
||||
/// Representation of the `user_projects` table.
|
||||
///
|
||||
/// (Automatically generated by Diesel.)
|
||||
user_projects (id) {
|
||||
/// The `id` column of the `user_projects` table.
|
||||
///
|
||||
/// Its SQL type is `Int4`.
|
||||
///
|
||||
/// (Automatically generated by Diesel.)
|
||||
id -> Int4,
|
||||
/// The `user_id` column of the `user_projects` table.
|
||||
///
|
||||
/// Its SQL type is `Int4`.
|
||||
///
|
||||
/// (Automatically generated by Diesel.)
|
||||
user_id -> Int4,
|
||||
/// The `project_id` column of the `user_projects` table.
|
||||
///
|
||||
/// Its SQL type is `Int4`.
|
||||
///
|
||||
/// (Automatically generated by Diesel.)
|
||||
project_id -> Int4,
|
||||
/// The `is_default` column of the `user_projects` table.
|
||||
///
|
||||
/// Its SQL type is `Bool`.
|
||||
///
|
||||
/// (Automatically generated by Diesel.)
|
||||
is_default -> Bool,
|
||||
/// The `is_current` column of the `user_projects` table.
|
||||
///
|
||||
/// Its SQL type is `Bool`.
|
||||
///
|
||||
/// (Automatically generated by Diesel.)
|
||||
is_current -> Bool,
|
||||
/// The `role` column of the `user_projects` table.
|
||||
///
|
||||
/// Its SQL type is `UserRoleType`.
|
||||
///
|
||||
/// (Automatically generated by Diesel.)
|
||||
role -> UserRoleType,
|
||||
/// The `created_at` column of the `user_projects` table.
|
||||
///
|
||||
/// Its SQL type is `Timestamp`.
|
||||
///
|
||||
/// (Automatically generated by Diesel.)
|
||||
created_at -> Timestamp,
|
||||
/// The `updated_at` column of the `user_projects` table.
|
||||
///
|
||||
/// Its SQL type is `Timestamp`.
|
||||
///
|
||||
/// (Automatically generated by Diesel.)
|
||||
updated_at -> Timestamp,
|
||||
}
|
||||
}
|
||||
|
||||
table! {
|
||||
use diesel::sql_types::*;
|
||||
use jirs_data::*;
|
||||
|
||||
/// Representation of the `users` table.
|
||||
///
|
||||
/// (Automatically generated by Diesel.)
|
||||
users (id) {
|
||||
/// The `id` column of the `users` table.
|
||||
///
|
||||
/// Its SQL type is `Int4`.
|
||||
///
|
||||
/// (Automatically generated by Diesel.)
|
||||
id -> Int4,
|
||||
/// The `name` column of the `users` table.
|
||||
///
|
||||
/// Its SQL type is `Text`.
|
||||
///
|
||||
/// (Automatically generated by Diesel.)
|
||||
name -> Text,
|
||||
/// The `email` column of the `users` table.
|
||||
///
|
||||
/// Its SQL type is `Text`.
|
||||
///
|
||||
/// (Automatically generated by Diesel.)
|
||||
email -> Text,
|
||||
/// The `avatar_url` column of the `users` table.
|
||||
///
|
||||
/// Its SQL type is `Nullable<Text>`.
|
||||
///
|
||||
/// (Automatically generated by Diesel.)
|
||||
avatar_url -> Nullable<Text>,
|
||||
/// The `created_at` column of the `users` table.
|
||||
///
|
||||
/// Its SQL type is `Timestamp`.
|
||||
///
|
||||
/// (Automatically generated by Diesel.)
|
||||
created_at -> Timestamp,
|
||||
/// The `updated_at` column of the `users` table.
|
||||
///
|
||||
/// Its SQL type is `Timestamp`.
|
||||
///
|
||||
/// (Automatically generated by Diesel.)
|
||||
updated_at -> Timestamp,
|
||||
}
|
||||
}
|
||||
|
||||
joinable!(comments -> issues (issue_id));
|
||||
joinable!(comments -> users (user_id));
|
||||
joinable!(epics -> projects (project_id));
|
||||
joinable!(epics -> users (user_id));
|
||||
joinable!(invitations -> projects (project_id));
|
||||
joinable!(invitations -> users (invited_by_id));
|
||||
joinable!(issue_assignees -> issues (issue_id));
|
||||
joinable!(issue_assignees -> users (user_id));
|
||||
joinable!(issue_statuses -> projects (project_id));
|
||||
joinable!(issues -> epics (epic_id));
|
||||
joinable!(issues -> issue_statuses (issue_status_id));
|
||||
joinable!(issues -> projects (project_id));
|
||||
joinable!(issues -> users (reporter_id));
|
||||
joinable!(tokens -> users (user_id));
|
||||
joinable!(user_projects -> projects (project_id));
|
||||
joinable!(user_projects -> users (user_id));
|
||||
|
||||
allow_tables_to_appear_in_same_query!(
|
||||
comments,
|
||||
epics,
|
||||
invitations,
|
||||
issue_assignees,
|
||||
issue_statuses,
|
||||
issues,
|
||||
messages,
|
||||
projects,
|
||||
tokens,
|
||||
user_projects,
|
||||
users,
|
||||
);
|
@ -1,7 +1,8 @@
|
||||
use bitque_data::{Token, UserId};
|
||||
use diesel::prelude::*;
|
||||
|
||||
use crate::{db_create, db_find, db_update};
|
||||
use {
|
||||
crate::{db_create, db_find, db_update},
|
||||
diesel::prelude::*,
|
||||
jirs_data::{Token, UserId},
|
||||
};
|
||||
|
||||
db_find! {
|
||||
FindUserId,
|
@ -1,7 +1,8 @@
|
||||
use bitque_data::{ProjectId, UserId, UserProject, UserProjectId, UserRole};
|
||||
use diesel::prelude::*;
|
||||
|
||||
use crate::{db_create, db_delete, db_find, db_load, db_update};
|
||||
use {
|
||||
crate::{db_create, db_delete, db_find, db_load, db_update},
|
||||
diesel::prelude::*,
|
||||
jirs_data::{ProjectId, UserId, UserProject, UserProjectId, UserRole},
|
||||
};
|
||||
|
||||
db_find! {
|
||||
CurrentUserProject,
|
||||
@ -26,10 +27,11 @@ db_load! {
|
||||
}
|
||||
|
||||
mod inner {
|
||||
use bitque_data::{UserId, UserProject, UserProjectId};
|
||||
use diesel::prelude::*;
|
||||
|
||||
use crate::db_update;
|
||||
use {
|
||||
crate::db_update,
|
||||
diesel::prelude::*,
|
||||
jirs_data::{UserId, UserProject, UserProjectId},
|
||||
};
|
||||
|
||||
db_update! {
|
||||
ChangeProjectIsCurrent,
|
@ -1,9 +1,11 @@
|
||||
use bitque_data::{EmailString, IssueId, ProjectId, User, UserId, UserRole, UsernameString};
|
||||
use diesel::prelude::*;
|
||||
|
||||
use crate::projects::CreateProject;
|
||||
use crate::user_projects::CreateUserProject;
|
||||
use crate::{db_create, db_find, db_load, db_update, q, DbPooledConn};
|
||||
use {
|
||||
crate::{
|
||||
db_create, db_find, db_load, db_update, projects::CreateProject, q,
|
||||
user_projects::CreateUserProject, DbPooledConn,
|
||||
},
|
||||
diesel::prelude::*,
|
||||
jirs_data::{EmailString, IssueId, ProjectId, User, UserId, UserRole, UsernameString},
|
||||
};
|
||||
|
||||
db_find! {
|
||||
FindUser,
|
||||
@ -120,7 +122,7 @@ db_load! {
|
||||
user_id => UserId
|
||||
}
|
||||
|
||||
fn count_matching_users(name: &str, email: &str, conn: &mut DbPooledConn) -> i64 {
|
||||
fn count_matching_users(name: &str, email: &str, conn: &DbPooledConn) -> i64 {
|
||||
use crate::schema::users::dsl;
|
||||
|
||||
q!(dsl::users
|
||||
@ -153,13 +155,13 @@ db_update! {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use bitque_data::{Project, ProjectCategory};
|
||||
use diesel::connection::TransactionManager;
|
||||
|
||||
use jirs_data::{Project, ProjectCategory};
|
||||
|
||||
use crate::build_pool;
|
||||
|
||||
use super::*;
|
||||
use crate::build_pool;
|
||||
use crate::schema::issues::dsl::issues;
|
||||
use crate::schema::tokens::dsl::tokens;
|
||||
use crate::schema::user_settings::dsl::user_settings;
|
||||
|
||||
#[test]
|
||||
fn check_collision() {
|
||||
@ -168,14 +170,13 @@ mod tests {
|
||||
use crate::schema::users::dsl::users;
|
||||
|
||||
let pool = build_pool();
|
||||
let mut conn = pool.get().unwrap();
|
||||
let conn = &mut conn;
|
||||
conn.begin_test_transaction().unwrap();
|
||||
let conn = &pool.get().unwrap();
|
||||
|
||||
let tm = conn.transaction_manager();
|
||||
|
||||
tm.begin_transaction(conn).unwrap();
|
||||
|
||||
diesel::delete(user_settings).execute(conn).unwrap();
|
||||
diesel::delete(user_projects).execute(conn).unwrap();
|
||||
diesel::delete(tokens).execute(conn).unwrap();
|
||||
diesel::delete(issues).execute(conn).unwrap();
|
||||
diesel::delete(users).execute(conn).unwrap();
|
||||
diesel::delete(projects).execute(conn).unwrap();
|
||||
|
||||
@ -221,6 +222,8 @@ mod tests {
|
||||
let res2 = count_matching_users("Bar", "foo@example.com", conn);
|
||||
let res3 = count_matching_users("Foo", "foo@example.com", conn);
|
||||
|
||||
tm.rollback_transaction(conn).unwrap();
|
||||
|
||||
assert_eq!(res1, 1);
|
||||
assert_eq!(res2, 1);
|
||||
assert_eq!(res3, 1);
|
36
actors/filesystem-actor/Cargo.toml
Normal file
36
actors/filesystem-actor/Cargo.toml
Normal file
@ -0,0 +1,36 @@
|
||||
[package]
|
||||
name = "filesystem-actor"
|
||||
version = "0.1.0"
|
||||
authors = ["Adrian Wozniak <adrian.wozniak@ita-prog.pl>"]
|
||||
edition = "2018"
|
||||
description = "JIRS (Simplified JIRA in Rust) shared data types"
|
||||
repository = "https://gitlab.com/adrian.wozniak/jirs"
|
||||
license = "MPL-2.0"
|
||||
#license-file = "../LICENSE"
|
||||
|
||||
[lib]
|
||||
name = "filesystem_actor"
|
||||
path = "./src/lib.rs"
|
||||
|
||||
[dependencies]
|
||||
actix = { version = "0.10.0" }
|
||||
|
||||
futures = { version = "0.3.8" }
|
||||
|
||||
log = "0.4"
|
||||
pretty_env_logger = "0.4"
|
||||
env_logger = "0.7"
|
||||
|
||||
bytes = { version = "0.5.6" }
|
||||
|
||||
# Local storage
|
||||
[dependencies.actix-files]
|
||||
version = "*"
|
||||
|
||||
[dependencies.jirs-config]
|
||||
path = "../../shared/jirs-config"
|
||||
features = ["local-storage"]
|
||||
|
||||
[dependencies.tokio]
|
||||
version = "0.2.23"
|
||||
features = ["dns"]
|
@ -1,9 +1,9 @@
|
||||
use std::io::Write;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use actix::SyncContext;
|
||||
use actix_files::{self, Files};
|
||||
use bitque_config::fs::Configuration;
|
||||
use {
|
||||
actix::SyncContext,
|
||||
actix_files::{self, Files},
|
||||
jirs_config::fs::Configuration,
|
||||
std::{io::Write, path::PathBuf},
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum FsError {
|
||||
@ -13,11 +13,11 @@ pub enum FsError {
|
||||
WriteFile,
|
||||
}
|
||||
|
||||
pub struct LocalStorageExecutor {
|
||||
pub struct FileSystemExecutor {
|
||||
config: Configuration,
|
||||
}
|
||||
|
||||
impl LocalStorageExecutor {
|
||||
impl FileSystemExecutor {
|
||||
pub fn client_path(&self) -> &str {
|
||||
self.config.client_path.as_str()
|
||||
}
|
||||
@ -27,7 +27,7 @@ impl LocalStorageExecutor {
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for LocalStorageExecutor {
|
||||
impl Default for FileSystemExecutor {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
config: Configuration::read(),
|
||||
@ -35,7 +35,7 @@ impl Default for LocalStorageExecutor {
|
||||
}
|
||||
}
|
||||
|
||||
impl actix::Actor for LocalStorageExecutor {
|
||||
impl actix::Actor for FileSystemExecutor {
|
||||
type Context = SyncContext<Self>;
|
||||
}
|
||||
|
||||
@ -46,14 +46,11 @@ pub struct CreateFile {
|
||||
pub file_name: String,
|
||||
}
|
||||
|
||||
impl actix::Handler<CreateFile> for LocalStorageExecutor {
|
||||
impl actix::Handler<CreateFile> for FileSystemExecutor {
|
||||
type Result = Result<usize, FsError>;
|
||||
|
||||
fn handle(&mut self, msg: CreateFile, _ctx: &mut Self::Context) -> Self::Result {
|
||||
let Configuration { store_path, .. } = &self.config;
|
||||
if std::fs::metadata(&store_path).is_err() {
|
||||
let _ = std::fs::create_dir_all(&store_path);
|
||||
}
|
||||
let CreateFile {
|
||||
mut source,
|
||||
file_name,
|
37
actors/highlight-actor/Cargo.toml
Normal file
37
actors/highlight-actor/Cargo.toml
Normal file
@ -0,0 +1,37 @@
|
||||
[package]
|
||||
name = "highlight-actor"
|
||||
version = "0.1.0"
|
||||
authors = ["Adrian Wozniak <adrian.wozniak@ita-prog.pl>"]
|
||||
edition = "2018"
|
||||
description = "JIRS (Simplified JIRA in Rust) shared data types"
|
||||
repository = "https://gitlab.com/adrian.wozniak/jirs"
|
||||
license = "MPL-2.0"
|
||||
#license-file = "../LICENSE"
|
||||
|
||||
[lib]
|
||||
name = "highlight_actor"
|
||||
path = "./src/lib.rs"
|
||||
|
||||
[dependencies]
|
||||
serde = "*"
|
||||
bincode = "*"
|
||||
toml = { version = "*" }
|
||||
|
||||
simsearch = { version = "0.2" }
|
||||
actix = { version = "0.10.0" }
|
||||
|
||||
flate2 = { version = "*" }
|
||||
syntect = { version = "*" }
|
||||
lazy_static = { version = "*" }
|
||||
|
||||
log = "0.4"
|
||||
pretty_env_logger = "0.4"
|
||||
env_logger = "0.7"
|
||||
|
||||
[dependencies.jirs-config]
|
||||
path = "../../shared/jirs-config"
|
||||
features = ["hi"]
|
||||
|
||||
[dependencies.jirs-data]
|
||||
path = "../../shared/jirs-data"
|
||||
features = ["backend"]
|
@ -1,11 +1,14 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use actix::{Actor, Handler, SyncContext};
|
||||
use bitque_data::HighlightedCode;
|
||||
use simsearch::SimSearch;
|
||||
use syntect::easy::HighlightLines;
|
||||
use syntect::highlighting::{Style, ThemeSet};
|
||||
use syntect::parsing::SyntaxSet;
|
||||
use {
|
||||
actix::{Actor, Handler, SyncContext},
|
||||
jirs_data::HighlightedCode,
|
||||
simsearch::SimSearch,
|
||||
std::sync::Arc,
|
||||
syntect::{
|
||||
easy::HighlightLines,
|
||||
highlighting::{Style, ThemeSet},
|
||||
parsing::SyntaxSet,
|
||||
},
|
||||
};
|
||||
|
||||
mod load;
|
||||
|
||||
@ -64,31 +67,22 @@ impl HighlightActor {
|
||||
.first()
|
||||
.and_then(|idx| self.syntax_set.syntaxes().get(*idx))
|
||||
.map(|st| st.name.as_str())
|
||||
.ok_or(HighlightError::UnknownLanguage)?;
|
||||
.ok_or_else(|| HighlightError::UnknownLanguage)?;
|
||||
|
||||
let set = self
|
||||
.syntax_set
|
||||
.as_ref()
|
||||
.find_syntax_by_name(lang)
|
||||
.ok_or(HighlightError::UnknownLanguage)?;
|
||||
.ok_or_else(|| HighlightError::UnknownLanguage)?;
|
||||
let theme: &syntect::highlighting::Theme = self
|
||||
.theme_set
|
||||
.as_ref()
|
||||
.themes
|
||||
.get("InspiredGitHub")
|
||||
.ok_or(HighlightError::UnknownTheme)?;
|
||||
.get("GitHub")
|
||||
.ok_or_else(|| HighlightError::UnknownTheme)?;
|
||||
|
||||
let mut hi = HighlightLines::new(set, theme);
|
||||
|
||||
let mut res = Vec::with_capacity(code.split_ascii_whitespace().count());
|
||||
for line in code.lines() {
|
||||
res.extend(
|
||||
hi.highlight_line(line, self.syntax_set.as_ref())
|
||||
.map_err(|_e| HighlightError::UnknownLanguage)?
|
||||
.iter(),
|
||||
);
|
||||
}
|
||||
Ok(res)
|
||||
Ok(hi.highlight(code, self.syntax_set.as_ref()))
|
||||
}
|
||||
}
|
||||
|
||||
@ -114,14 +108,14 @@ impl Handler<HighlightCode> for HighlightActor {
|
||||
.into_iter()
|
||||
.map(|(style, part)| {
|
||||
(
|
||||
bitque_data::Style {
|
||||
foreground: bitque_data::Color {
|
||||
jirs_data::Style {
|
||||
foreground: jirs_data::Color {
|
||||
r: style.foreground.r,
|
||||
g: style.foreground.g,
|
||||
b: style.foreground.b,
|
||||
a: style.foreground.a,
|
||||
},
|
||||
background: bitque_data::Color {
|
||||
background: jirs_data::Color {
|
||||
r: style.background.r,
|
||||
g: style.background.g,
|
||||
b: style.background.b,
|
||||
@ -137,7 +131,7 @@ impl Handler<HighlightCode> for HighlightActor {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, actix::Message)]
|
||||
#[derive(actix::Message)]
|
||||
#[rtype(result = "Result<String, HighlightError>")]
|
||||
pub struct TextHighlightCode {
|
||||
pub code: String,
|
25
actors/highlight-actor/src/load.rs
Normal file
25
actors/highlight-actor/src/load.rs
Normal file
@ -0,0 +1,25 @@
|
||||
use {
|
||||
bincode::{deserialize_from, Result},
|
||||
flate2::bufread::ZlibDecoder,
|
||||
serde::de::DeserializeOwned,
|
||||
std::io::BufRead,
|
||||
};
|
||||
|
||||
fn from_reader<T: DeserializeOwned, R: BufRead>(input: R) -> Result<T> {
|
||||
let mut decoder = ZlibDecoder::new(input);
|
||||
deserialize_from(&mut decoder)
|
||||
}
|
||||
|
||||
fn from_binary<T: DeserializeOwned>(v: &[u8]) -> T {
|
||||
from_reader(v).unwrap()
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn integrated_syntaxset() -> syntect::parsing::SyntaxSet {
|
||||
from_binary(include_bytes!("./syntaxes.bin"))
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn integrated_themeset() -> syntect::highlighting::ThemeSet {
|
||||
from_binary(include_bytes!("./themes.bin"))
|
||||
}
|
@ -3,8 +3,8 @@ name = "mail-actor"
|
||||
version = "0.1.0"
|
||||
authors = ["Adrian Wozniak <adrian.wozniak@ita-prog.pl>"]
|
||||
edition = "2018"
|
||||
description = "BITQUE (Simplified JIRA in Rust) shared data types"
|
||||
repository = "https://gitlab.com/adrian.wozniak/bitque"
|
||||
description = "JIRS (Simplified JIRA in Rust) shared data types"
|
||||
repository = "https://gitlab.com/adrian.wozniak/jirs"
|
||||
license = "MPL-2.0"
|
||||
#license-file = "../LICENSE"
|
||||
|
||||
@ -13,15 +13,26 @@ name = "mail_actor"
|
||||
path = "./src/lib.rs"
|
||||
|
||||
[dependencies]
|
||||
actix = { version = "0.13.0" }
|
||||
bitque-config = { workspace = true, features = ["mail", "web"] }
|
||||
dotenv = { version = "*" }
|
||||
futures = { version = "*" }
|
||||
lettre = { version = "0.10.0-rc.3" }
|
||||
lettre_email = { version = "*" }
|
||||
libc = { version = "0.2.0", default-features = false }
|
||||
openssl-sys = { version = "*", features = ["vendored"] }
|
||||
serde = { version = "*" }
|
||||
actix = { version = "0.10.0" }
|
||||
|
||||
serde = "*"
|
||||
toml = { version = "*" }
|
||||
uuid = { version = "1.3.0", features = ["serde", "v4", "v5"] }
|
||||
tracing = { version = "0" }
|
||||
|
||||
log = "0.4"
|
||||
pretty_env_logger = "0.4"
|
||||
env_logger = "0.7"
|
||||
|
||||
dotenv = { version = "*" }
|
||||
|
||||
uuid = { version = "0.8.1", features = ["serde", "v4", "v5"] }
|
||||
|
||||
futures = { version = "*" }
|
||||
openssl-sys = { version = "*", features = ["vendored"] }
|
||||
libc = { version = "0.2.0", default-features = false }
|
||||
|
||||
lettre = { version = "*" }
|
||||
lettre_email = { version = "*" }
|
||||
|
||||
[dependencies.jirs-config]
|
||||
path = "../../shared/jirs-config"
|
||||
features = ["mail", "web"]
|
60
actors/mail-actor/src/invite.rs
Normal file
60
actors/mail-actor/src/invite.rs
Normal file
@ -0,0 +1,60 @@
|
||||
use {
|
||||
crate::MailExecutor,
|
||||
actix::{Handler, Message},
|
||||
uuid::Uuid,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Invite {
|
||||
pub bind_token: Uuid,
|
||||
pub email: String,
|
||||
pub inviter_name: String,
|
||||
}
|
||||
|
||||
impl Message for Invite {
|
||||
type Result = Result<(), String>;
|
||||
}
|
||||
|
||||
impl Handler<Invite> for MailExecutor {
|
||||
type Result = Result<(), String>;
|
||||
|
||||
fn handle(&mut self, msg: Invite, _ctx: &mut Self::Context) -> Self::Result {
|
||||
use lettre::Transport;
|
||||
let transport = &mut self.transport;
|
||||
let from = self.config.from.as_str();
|
||||
let addr = jirs_config::web::Configuration::read().full_addr();
|
||||
|
||||
let html = format!(
|
||||
r#"
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head><meta charset="UTF-8"></head>
|
||||
<body>
|
||||
<h1>You have been invited to project by {inviter_name}!</h1>
|
||||
<p>
|
||||
</p>
|
||||
<p>
|
||||
Please click this link: <a href="{addr}/invite?token={bind_token}">{addr}/invite?token={bind_token}</a>
|
||||
</p>
|
||||
</body>
|
||||
</html>
|
||||
"#,
|
||||
bind_token = msg.bind_token,
|
||||
inviter_name = msg.inviter_name,
|
||||
addr = addr,
|
||||
);
|
||||
|
||||
let email = lettre_email::Email::builder()
|
||||
.from(from)
|
||||
.to(msg.email.as_str())
|
||||
.html(html.as_str())
|
||||
.subject("Invitation to JIRS project")
|
||||
.build()
|
||||
.map_err(|_| "Email is not valid".to_string())?;
|
||||
|
||||
transport
|
||||
.send(email.into())
|
||||
.map(|_| ())
|
||||
.map_err(|e| format!("Mailer: {}", e))
|
||||
}
|
||||
}
|
47
actors/mail-actor/src/lib.rs
Normal file
47
actors/mail-actor/src/lib.rs
Normal file
@ -0,0 +1,47 @@
|
||||
use actix::{Actor, SyncContext};
|
||||
|
||||
pub mod invite;
|
||||
pub mod welcome;
|
||||
|
||||
pub type MailTransport = lettre::SmtpTransport;
|
||||
|
||||
pub struct MailExecutor {
|
||||
pub transport: MailTransport,
|
||||
pub config: jirs_config::mail::Configuration,
|
||||
}
|
||||
|
||||
impl Actor for MailExecutor {
|
||||
type Context = SyncContext<Self>;
|
||||
}
|
||||
|
||||
impl Default for MailExecutor {
|
||||
fn default() -> Self {
|
||||
let config = jirs_config::mail::Configuration::read();
|
||||
Self {
|
||||
transport: mail_transport(&config),
|
||||
config,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn mail_client(config: &jirs_config::mail::Configuration) -> lettre::SmtpClient {
|
||||
let jirs_config::mail::Configuration {
|
||||
user: mail_user,
|
||||
pass: mail_pass,
|
||||
host: mail_host,
|
||||
..
|
||||
} = &config;
|
||||
|
||||
lettre::SmtpClient::new_simple(mail_host)
|
||||
.expect("Failed to init SMTP client")
|
||||
.credentials(lettre::smtp::authentication::Credentials::new(
|
||||
mail_user.clone(),
|
||||
mail_pass.clone(),
|
||||
))
|
||||
.connection_reuse(lettre::smtp::ConnectionReuseParameters::ReuseUnlimited)
|
||||
.smtp_utf8(true)
|
||||
}
|
||||
|
||||
fn mail_transport(config: &jirs_config::mail::Configuration) -> MailTransport {
|
||||
mail_client(config).transport()
|
||||
}
|
59
actors/mail-actor/src/welcome.rs
Normal file
59
actors/mail-actor/src/welcome.rs
Normal file
@ -0,0 +1,59 @@
|
||||
use {
|
||||
crate::MailExecutor,
|
||||
actix::{Handler, Message},
|
||||
uuid::Uuid,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Welcome {
|
||||
pub bind_token: Uuid,
|
||||
pub email: String,
|
||||
}
|
||||
|
||||
impl Message for Welcome {
|
||||
type Result = Result<(), String>;
|
||||
}
|
||||
|
||||
impl Handler<Welcome> for MailExecutor {
|
||||
type Result = Result<(), String>;
|
||||
|
||||
fn handle(&mut self, msg: Welcome, _ctx: &mut Self::Context) -> Self::Result {
|
||||
use lettre::Transport;
|
||||
let transport = &mut self.transport;
|
||||
let from = self.config.from.as_str();
|
||||
|
||||
let html = format!(
|
||||
r#"
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head><meta charset="UTF-8"></head>
|
||||
<body>
|
||||
<h1>Welcome in JIRS!</h1>
|
||||
<p>
|
||||
</p>
|
||||
<p>
|
||||
Please copy this code to sign-in single use token field: <pre><code>{bind_token}</code</pre>
|
||||
</p>
|
||||
<p>
|
||||
Notice: This token is single use and will be removed from system once you use it.
|
||||
</p>
|
||||
</body>
|
||||
</html>
|
||||
"#,
|
||||
bind_token = msg.bind_token,
|
||||
);
|
||||
|
||||
let email = lettre_email::Email::builder()
|
||||
.from(from)
|
||||
.to(msg.email.as_str())
|
||||
.html(html.as_str())
|
||||
.subject("Welcome to JIRS")
|
||||
.build()
|
||||
.map_err(|_| "Email is not valid".to_string())?;
|
||||
|
||||
transport
|
||||
.send(email.into())
|
||||
.map(|_| ())
|
||||
.map_err(|e| format!("Mailer: {}", e))
|
||||
}
|
||||
}
|
72
actors/web-actor/Cargo.toml
Normal file
72
actors/web-actor/Cargo.toml
Normal file
@ -0,0 +1,72 @@
|
||||
[package]
|
||||
name = "web-actor"
|
||||
version = "0.1.0"
|
||||
authors = ["Adrian Wozniak <adrian.wozniak@ita-prog.pl>"]
|
||||
edition = "2018"
|
||||
description = "JIRS (Simplified JIRA in Rust) shared data types"
|
||||
repository = "https://gitlab.com/adrian.wozniak/jirs"
|
||||
license = "MPL-2.0"
|
||||
#license-file = "../LICENSE"
|
||||
|
||||
[lib]
|
||||
name = "web_actor"
|
||||
path = "./src/lib.rs"
|
||||
|
||||
[features]
|
||||
local-storage = ["filesystem-actor"]
|
||||
aws-s3 = ["amazon-actor"]
|
||||
default = ["local-storage", "aws-s3"]
|
||||
|
||||
[dependencies]
|
||||
serde = "*"
|
||||
bincode = "*"
|
||||
toml = { version = "*" }
|
||||
|
||||
actix = { version = "0.10.0" }
|
||||
actix-web = { version = "*" }
|
||||
actix-cors = { version = "*" }
|
||||
actix-service = { version = "*" }
|
||||
actix-rt = "1"
|
||||
actix-web-actors = "*"
|
||||
actix-multipart = "*"
|
||||
|
||||
bytes = { version = "0.5.6" }
|
||||
|
||||
futures = { version = "0.3.8" }
|
||||
openssl-sys = { version = "*", features = ["vendored"] }
|
||||
libc = { version = "0.2.0", default-features = false }
|
||||
|
||||
log = "0.4"
|
||||
pretty_env_logger = "0.4"
|
||||
env_logger = "0.7"
|
||||
|
||||
uuid = { version = "0.8.1", features = ["serde", "v4", "v5"] }
|
||||
|
||||
[dependencies.jirs-config]
|
||||
path = "../../shared/jirs-config"
|
||||
features = ["mail", "web", "local-storage"]
|
||||
|
||||
[dependencies.jirs-data]
|
||||
path = "../../shared/jirs-data"
|
||||
features = ["backend"]
|
||||
|
||||
[dependencies.database-actor]
|
||||
path = "../database-actor"
|
||||
|
||||
[dependencies.mail-actor]
|
||||
path = "../mail-actor"
|
||||
|
||||
[dependencies.websocket-actor]
|
||||
path = "../websocket-actor"
|
||||
|
||||
[dependencies.filesystem-actor]
|
||||
path = "../filesystem-actor"
|
||||
optional = true
|
||||
|
||||
[dependencies.amazon-actor]
|
||||
path = "../amazon-actor"
|
||||
optional = true
|
||||
|
||||
[dependencies.tokio]
|
||||
version = "0.2.23"
|
||||
features = ["dns"]
|
129
actors/web-actor/src/avatar.rs
Normal file
129
actors/web-actor/src/avatar.rs
Normal file
@ -0,0 +1,129 @@
|
||||
use std::io::Write;
|
||||
|
||||
#[cfg(feature = "local-storage")]
|
||||
use filesystem_actor;
|
||||
use {
|
||||
actix::Addr,
|
||||
actix_multipart::{Field, Multipart},
|
||||
actix_web::{http::header::ContentDisposition, post, web, web::Data, Error, HttpResponse},
|
||||
database_actor::{
|
||||
authorize_user::AuthorizeUser, user_projects::CurrentUserProject, users::UpdateAvatarUrl,
|
||||
DbExecutor,
|
||||
},
|
||||
futures::{executor::block_on, StreamExt, TryStreamExt},
|
||||
jirs_data::{User, UserId, WsMsg},
|
||||
websocket_actor::server::{InnerMsg::BroadcastToChannel, WsServer},
|
||||
};
|
||||
|
||||
#[post("/")]
|
||||
pub async fn upload(
|
||||
mut payload: Multipart,
|
||||
db: Data<Addr<DbExecutor>>,
|
||||
ws: Data<Addr<WsServer>>,
|
||||
fs: Data<Addr<filesystem_actor::FileSystemExecutor>>,
|
||||
amazon: Data<Addr<amazon_actor::AmazonExecutor>>,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
let mut user_id: Option<UserId> = None;
|
||||
let mut avatar_url: Option<String> = None;
|
||||
|
||||
while let Ok(Some(field)) = payload.try_next().await {
|
||||
let disposition: ContentDisposition = match field.content_disposition() {
|
||||
Some(d) => d,
|
||||
_ => continue,
|
||||
};
|
||||
if !disposition.is_form_data() {
|
||||
return Ok(HttpResponse::BadRequest().finish());
|
||||
}
|
||||
match disposition.get_name() {
|
||||
Some("token") => {
|
||||
user_id = Some(handle_token(field, db.clone()).await?);
|
||||
}
|
||||
Some("avatar") => {
|
||||
let id = user_id.ok_or_else(|| HttpResponse::Unauthorized().finish())?;
|
||||
avatar_url = Some(
|
||||
crate::handlers::upload_avatar_image::handle_image(
|
||||
id,
|
||||
field,
|
||||
disposition,
|
||||
fs.clone(),
|
||||
amazon.clone(),
|
||||
)
|
||||
.await?,
|
||||
);
|
||||
}
|
||||
_ => continue,
|
||||
};
|
||||
}
|
||||
let user_id = match user_id {
|
||||
Some(id) => id,
|
||||
_ => return Ok(HttpResponse::Unauthorized().finish()),
|
||||
};
|
||||
|
||||
let project_id = match block_on(db.send(CurrentUserProject { user_id })) {
|
||||
Ok(Ok(user_project)) => user_project.project_id,
|
||||
_ => return Ok(HttpResponse::UnprocessableEntity().finish()),
|
||||
};
|
||||
|
||||
match (user_id, avatar_url) {
|
||||
(user_id, Some(avatar_url)) => {
|
||||
let user = update_user_avatar(user_id, avatar_url.clone(), db).await?;
|
||||
ws.send(BroadcastToChannel(
|
||||
project_id,
|
||||
WsMsg::AvatarUrlChanged(user.id, avatar_url),
|
||||
))
|
||||
.await
|
||||
.map_err(|_| HttpResponse::UnprocessableEntity().finish())?;
|
||||
Ok(HttpResponse::NoContent().finish())
|
||||
}
|
||||
_ => Ok(HttpResponse::UnprocessableEntity().finish()),
|
||||
}
|
||||
}
|
||||
|
||||
async fn update_user_avatar(
|
||||
user_id: UserId,
|
||||
new_url: String,
|
||||
db: Data<Addr<DbExecutor>>,
|
||||
) -> Result<User, Error> {
|
||||
match db
|
||||
.send(UpdateAvatarUrl {
|
||||
user_id,
|
||||
avatar_url: Some(new_url),
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(Ok(user)) => Ok(user),
|
||||
|
||||
Ok(Err(e)) => {
|
||||
error!("{:?}", e);
|
||||
Err(HttpResponse::Unauthorized().finish().into())
|
||||
}
|
||||
Err(e) => {
|
||||
error!("{:?}", e);
|
||||
Err(HttpResponse::Unauthorized().finish().into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_token(mut field: Field, db: Data<Addr<DbExecutor>>) -> Result<UserId, Error> {
|
||||
let mut f: Vec<u8> = vec![];
|
||||
while let Some(chunk) = field.next().await {
|
||||
let data = chunk.unwrap();
|
||||
f = web::block(move || f.write_all(&data).map(|_| f)).await?;
|
||||
}
|
||||
let access_token = String::from_utf8(f)
|
||||
.unwrap_or_default()
|
||||
.parse::<uuid::Uuid>()
|
||||
.map_err(|_| HttpResponse::Unauthorized().finish())?;
|
||||
match db.send(AuthorizeUser { access_token }).await {
|
||||
Ok(Ok(user)) => Ok(user.id),
|
||||
|
||||
Ok(Err(e)) => {
|
||||
error!("{:?}", e);
|
||||
Err(HttpResponse::Unauthorized().finish().into())
|
||||
}
|
||||
Err(e) => {
|
||||
error!("{:?}", e);
|
||||
Err(HttpResponse::Unauthorized().finish().into())
|
||||
}
|
||||
}
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
use actix_web::HttpResponse;
|
||||
use bitque_data::msg::WsError;
|
||||
use bitque_data::ErrorResponse;
|
||||
|
||||
use jirs_data::{msg::WsError, ErrorResponse};
|
||||
|
||||
const TOKEN_NOT_FOUND: &str = "Token not found";
|
||||
const DATABASE_CONNECTION_FAILED: &str = "Database connection failed";
|
164
actors/web-actor/src/handlers/upload_avatar_image.rs
Normal file
164
actors/web-actor/src/handlers/upload_avatar_image.rs
Normal file
@ -0,0 +1,164 @@
|
||||
use {
|
||||
actix::Addr,
|
||||
actix_multipart::Field,
|
||||
actix_web::{http::header::ContentDisposition, web::Data, Error},
|
||||
futures::StreamExt,
|
||||
jirs_data::UserId,
|
||||
tokio::sync::broadcast::{Receiver, Sender},
|
||||
};
|
||||
|
||||
#[cfg(all(feature = "local-storage", feature = "aws-s3"))]
|
||||
pub(crate) async fn handle_image(
|
||||
user_id: UserId,
|
||||
mut field: Field,
|
||||
disposition: ContentDisposition,
|
||||
fs: Data<Addr<filesystem_actor::FileSystemExecutor>>,
|
||||
amazon: Data<Addr<amazon_actor::AmazonExecutor>>,
|
||||
) -> Result<String, Error> {
|
||||
let filename = disposition.get_filename().unwrap();
|
||||
let system_file_name = format!("{}-{}", user_id, filename);
|
||||
|
||||
let (sender, receiver) = tokio::sync::broadcast::channel(64);
|
||||
|
||||
let fs_fut = local_storage_write(system_file_name.clone(), fs, user_id, sender.subscribe());
|
||||
let aws_fut = aws_s3(system_file_name, amazon, receiver);
|
||||
let read_fut = read_form_data(&mut field, sender);
|
||||
|
||||
let fs_join = tokio::task::spawn(fs_fut);
|
||||
let aws_join = tokio::task::spawn(aws_fut);
|
||||
read_fut.await;
|
||||
|
||||
let mut new_link = None;
|
||||
|
||||
if let Ok(url) = fs_join.await {
|
||||
new_link = url;
|
||||
}
|
||||
|
||||
if let Ok(url) = aws_join.await {
|
||||
new_link = url;
|
||||
}
|
||||
|
||||
Ok(new_link.unwrap_or_default())
|
||||
}
|
||||
|
||||
#[cfg(all(not(feature = "local-storage"), feature = "aws-s3"))]
|
||||
pub(crate) async fn handle_image(
|
||||
user_id: UserId,
|
||||
mut field: Field,
|
||||
disposition: ContentDisposition,
|
||||
amazon: Data<Addr<amazon_actor::AmazonExecutor>>,
|
||||
) -> Result<String, Error> {
|
||||
let filename = disposition.get_filename().unwrap();
|
||||
let system_file_name = format!("{}-{}", user_id, filename);
|
||||
|
||||
let (sender, receiver) = tokio::sync::broadcast::channel(64);
|
||||
|
||||
let aws_fut = aws_s3(system_file_name, amazon, receiver);
|
||||
let read_fut = read_form_data(&mut field, sender);
|
||||
|
||||
let aws_join = tokio::task::spawn(aws_fut);
|
||||
read_fut.await;
|
||||
|
||||
let mut new_link = None;
|
||||
|
||||
if let Ok(url) = aws_join.await {
|
||||
new_link = url;
|
||||
}
|
||||
|
||||
Ok(new_link.unwrap_or_default())
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "local-storage", not(feature = "aws-s3")))]
|
||||
pub(crate) async fn handle_image(
|
||||
user_id: UserId,
|
||||
mut field: Field,
|
||||
disposition: ContentDisposition,
|
||||
fs: Data<Addr<filesystem_actor::FileSystemExecutor>>,
|
||||
) -> Result<String, Error> {
|
||||
let filename = disposition.get_filename().unwrap();
|
||||
let system_file_name = format!("{}-{}", user_id, filename);
|
||||
|
||||
let (sender, receiver) = tokio::sync::broadcast::channel(64);
|
||||
|
||||
let fs_fut = local_storage_write(system_file_name, fs, user_id, sender.subscribe());
|
||||
let read_fut = read_form_data(&mut field, sender);
|
||||
|
||||
let fs_join = tokio::task::spawn(fs_fut);
|
||||
read_fut.await;
|
||||
|
||||
let mut new_link = None;
|
||||
|
||||
if let Ok(url) = fs_join.await {
|
||||
new_link = url;
|
||||
}
|
||||
|
||||
Ok(new_link.unwrap_or_default())
|
||||
}
|
||||
|
||||
/// Read file from client
|
||||
async fn read_form_data(field: &mut Field, sender: Sender<bytes::Bytes>) {
|
||||
while let Some(chunk) = field.next().await {
|
||||
let data = chunk.unwrap();
|
||||
if let Err(err) = sender.send(data) {
|
||||
log::error!("{:?}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Stream bytes directly to AWS S3 Service
|
||||
#[cfg(feature = "aws-s3")]
|
||||
async fn aws_s3(
|
||||
system_file_name: String,
|
||||
amazon: Data<Addr<amazon_actor::AmazonExecutor>>,
|
||||
receiver: Receiver<bytes::Bytes>,
|
||||
) -> Option<String> {
|
||||
let s3 = jirs_config::amazon::config();
|
||||
if !s3.active {
|
||||
return None;
|
||||
}
|
||||
|
||||
match amazon
|
||||
.send(amazon_actor::S3PutObject {
|
||||
source: receiver,
|
||||
file_name: system_file_name.to_string(),
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(Ok(s)) => Some(s),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "local-storage")]
|
||||
async fn local_storage_write(
|
||||
system_file_name: String,
|
||||
fs: Data<Addr<filesystem_actor::FileSystemExecutor>>,
|
||||
user_id: jirs_data::UserId,
|
||||
receiver: Receiver<bytes::Bytes>,
|
||||
) -> Option<String> {
|
||||
let web_config = jirs_config::web::config();
|
||||
let fs_config = jirs_config::fs::config();
|
||||
|
||||
match fs
|
||||
.send(filesystem_actor::CreateFile {
|
||||
source: receiver,
|
||||
file_name: system_file_name.to_string(),
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(Ok(_)) => Some(format!(
|
||||
"{proto}://{bind}{port}{client_path}/{user_id}-{filename}",
|
||||
proto = if web_config.ssl { "https" } else { "http" },
|
||||
bind = web_config.bind,
|
||||
port = match web_config.port.as_str() {
|
||||
"80" | "443" => "".to_string(),
|
||||
p => format!(":{}", p),
|
||||
},
|
||||
client_path = fs_config.client_path,
|
||||
user_id = user_id,
|
||||
filename = system_file_name
|
||||
)),
|
||||
Ok(_) => None,
|
||||
_ => None,
|
||||
}
|
||||
}
|
@ -1,15 +1,14 @@
|
||||
#![feature(async_fn_in_trait)]
|
||||
extern crate core;
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
|
||||
use actix::Addr;
|
||||
use actix_web::web::Data;
|
||||
use actix_web::{HttpRequest, HttpResponse};
|
||||
use bitque_data::User;
|
||||
use database_actor::authorize_user::AuthorizeUser;
|
||||
use database_actor::DbExecutor;
|
||||
pub use errors::*;
|
||||
|
||||
use crate::middleware::authorize::token_from_headers;
|
||||
use {
|
||||
crate::middleware::authorize::token_from_headers,
|
||||
actix::Addr,
|
||||
actix_web::{web::Data, HttpRequest, HttpResponse},
|
||||
database_actor::{authorize_user::AuthorizeUser, DbExecutor},
|
||||
jirs_data::User,
|
||||
};
|
||||
|
||||
pub mod avatar;
|
||||
pub mod errors;
|
117
actors/web-actor/src/middleware/authorize.rs
Normal file
117
actors/web-actor/src/middleware/authorize.rs
Normal file
@ -0,0 +1,117 @@
|
||||
use {
|
||||
actix_service::{Service, Transform},
|
||||
actix_web::{
|
||||
dev::{ServiceRequest, ServiceResponse},
|
||||
http::header::{self},
|
||||
http::HeaderMap,
|
||||
Error,
|
||||
},
|
||||
futures::future::{ok, FutureExt, LocalBoxFuture, Ready},
|
||||
jirs_data::User,
|
||||
std::task::{Context, Poll},
|
||||
};
|
||||
|
||||
type Db = actix_web::web::Data<database_actor::DbPool>;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Authorize;
|
||||
|
||||
impl<S, B> Transform<S> for Authorize
|
||||
where
|
||||
S: Service<Request = ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
|
||||
S::Future: 'static,
|
||||
B: 'static,
|
||||
{
|
||||
type Request = ServiceRequest;
|
||||
type Response = ServiceResponse<B>;
|
||||
type Error = Error;
|
||||
type Transform = AuthorizeMiddleware<S>;
|
||||
type InitError = ();
|
||||
type Future = Ready<Result<Self::Transform, Self::InitError>>;
|
||||
|
||||
fn new_transform(&self, service: S) -> Self::Future {
|
||||
ok(AuthorizeMiddleware { service })
|
||||
}
|
||||
}
|
||||
|
||||
pub struct AuthorizeMiddleware<S> {
|
||||
service: S,
|
||||
}
|
||||
|
||||
impl<S, B> Service for AuthorizeMiddleware<S>
|
||||
where
|
||||
S: Service<Request = ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
|
||||
S::Future: 'static,
|
||||
B: 'static,
|
||||
{
|
||||
type Request = ServiceRequest;
|
||||
type Response = ServiceResponse<B>;
|
||||
type Error = Error;
|
||||
type Future = LocalBoxFuture<'static, Result<Self::Response, Error>>;
|
||||
|
||||
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
||||
self.service.poll_ready(cx)
|
||||
}
|
||||
|
||||
fn call(&mut self, req: ServiceRequest) -> Self::Future {
|
||||
let pool: &Db = match req.app_data::<Db>() {
|
||||
Some(d) => d,
|
||||
_ => {
|
||||
return async move {
|
||||
let res = crate::errors::ServiceError::DatabaseConnectionLost
|
||||
.into_http_response()
|
||||
.into_body();
|
||||
Ok(req.into_response(res))
|
||||
}
|
||||
.boxed_local();
|
||||
}
|
||||
};
|
||||
|
||||
match check_token(req.headers(), pool.clone()) {
|
||||
std::result::Result::Err(e) => {
|
||||
return async move {
|
||||
let res = e.into_http_response().into_body();
|
||||
Ok(req.into_response(res))
|
||||
}
|
||||
.boxed_local();
|
||||
}
|
||||
Ok(_user) => {}
|
||||
};
|
||||
|
||||
let fut = self.service.call(req);
|
||||
async move { fut.await }.boxed_local()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn token_from_headers(
|
||||
headers: &HeaderMap,
|
||||
) -> std::result::Result<uuid::Uuid, crate::errors::ServiceError> {
|
||||
headers
|
||||
.get(header::AUTHORIZATION)
|
||||
.ok_or_else(|| crate::errors::ServiceError::Unauthorized)
|
||||
.map(|h| h.to_str().unwrap_or_default())
|
||||
.and_then(|s| parse_bearer(s))
|
||||
}
|
||||
|
||||
fn check_token(
|
||||
headers: &HeaderMap,
|
||||
pool: Db,
|
||||
) -> std::result::Result<User, crate::errors::ServiceError> {
|
||||
token_from_headers(headers).and_then(|access_token| {
|
||||
use database_actor::authorize_user::AuthorizeUser;
|
||||
let conn = pool
|
||||
.get()
|
||||
.map_err(|_| crate::errors::ServiceError::DatabaseConnectionLost)?;
|
||||
AuthorizeUser { access_token }
|
||||
.execute(&conn)
|
||||
.map_err(|_| crate::errors::ServiceError::Unauthorized)
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_bearer(header: &str) -> Result<uuid::Uuid, crate::errors::ServiceError> {
|
||||
if !header.starts_with("Bearer ") {
|
||||
return Err(crate::errors::ServiceError::Unauthorized);
|
||||
}
|
||||
let (_bearer, token) = header.split_at(7);
|
||||
uuid::Uuid::parse_str(token).map_err(|_e| crate::errors::ServiceError::Unauthorized)
|
||||
}
|
59
actors/websocket-actor/Cargo.toml
Normal file
59
actors/websocket-actor/Cargo.toml
Normal file
@ -0,0 +1,59 @@
|
||||
[package]
|
||||
name = "websocket-actor"
|
||||
version = "0.1.0"
|
||||
authors = ["Adrian Wozniak <adrian.wozniak@ita-prog.pl>"]
|
||||
edition = "2018"
|
||||
description = "JIRS (Simplified JIRA in Rust) shared data types"
|
||||
repository = "https://gitlab.com/adrian.wozniak/jirs"
|
||||
license = "MPL-2.0"
|
||||
#license-file = "../LICENSE"
|
||||
|
||||
[lib]
|
||||
name = "websocket_actor"
|
||||
path = "./src/lib.rs"
|
||||
|
||||
[dependencies]
|
||||
serde = "*"
|
||||
bincode = "*"
|
||||
toml = { version = "*" }
|
||||
|
||||
actix = { version = "0.10.0" }
|
||||
actix-web = { version = "*" }
|
||||
actix-web-actors = "*"
|
||||
|
||||
futures = { version = "0.3.8" }
|
||||
openssl-sys = { version = "*", features = ["vendored"] }
|
||||
libc = { version = "0.2.0", default-features = false }
|
||||
|
||||
flate2 = { version = "*" }
|
||||
syntect = { version = "*" }
|
||||
lazy_static = { version = "*" }
|
||||
|
||||
log = "0.4"
|
||||
pretty_env_logger = "0.4"
|
||||
env_logger = "0.7"
|
||||
|
||||
uuid = { version = "0.8.1", features = ["serde", "v4", "v5"] }
|
||||
|
||||
[dependencies.comrak]
|
||||
version = "*"
|
||||
|
||||
[dependencies.pulldown-cmark]
|
||||
version = "*"
|
||||
|
||||
[dependencies.jirs-config]
|
||||
path = "../../shared/jirs-config"
|
||||
features = ["websocket"]
|
||||
|
||||
[dependencies.jirs-data]
|
||||
path = "../../shared/jirs-data"
|
||||
features = ["backend"]
|
||||
|
||||
[dependencies.database-actor]
|
||||
path = "../database-actor"
|
||||
|
||||
[dependencies.mail-actor]
|
||||
path = "../mail-actor"
|
||||
|
||||
[dependencies.highlight-actor]
|
||||
path = "../highlight-actor"
|
81
actors/websocket-actor/src/handlers/auth.rs
Normal file
81
actors/websocket-actor/src/handlers/auth.rs
Normal file
@ -0,0 +1,81 @@
|
||||
use {
|
||||
crate::{
|
||||
db_or_debug_and_return, mail_or_debug_and_return, WebSocketActor, WsHandler, WsResult,
|
||||
},
|
||||
actix::AsyncContext,
|
||||
database_actor::{
|
||||
authorize_user::AuthorizeUser,
|
||||
tokens::{CreateBindToken, FindBindToken},
|
||||
users::LookupUser,
|
||||
},
|
||||
futures::executor::block_on,
|
||||
jirs_data::{Token, WsMsg},
|
||||
mail_actor::welcome::Welcome,
|
||||
};
|
||||
|
||||
pub struct Authenticate {
|
||||
pub name: String,
|
||||
pub email: String,
|
||||
}
|
||||
|
||||
impl WsHandler<Authenticate> for WebSocketActor {
|
||||
fn handle_msg(&mut self, msg: Authenticate, _ctx: &mut Self::Context) -> WsResult {
|
||||
let Authenticate { name, email } = msg;
|
||||
// TODO check attempt number, allow only 5 times per day
|
||||
let user = db_or_debug_and_return!(self, LookupUser { name, email });
|
||||
let token = db_or_debug_and_return!(self, CreateBindToken { user_id: user.id });
|
||||
if let Some(bind_token) = token.bind_token.as_ref().cloned() {
|
||||
let _ = mail_or_debug_and_return!(
|
||||
self,
|
||||
Welcome {
|
||||
bind_token,
|
||||
email: user.email,
|
||||
}
|
||||
);
|
||||
}
|
||||
Ok(Some(WsMsg::AuthenticateSuccess))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CheckAuthToken {
|
||||
pub token: uuid::Uuid,
|
||||
}
|
||||
|
||||
impl WsHandler<CheckAuthToken> for WebSocketActor {
|
||||
fn handle_msg(&mut self, msg: CheckAuthToken, ctx: &mut Self::Context) -> WsResult {
|
||||
let user: jirs_data::User = db_or_debug_and_return!(
|
||||
self,
|
||||
AuthorizeUser {
|
||||
access_token: msg.token,
|
||||
},
|
||||
Ok(Some(WsMsg::AuthorizeLoaded(Err(
|
||||
"Invalid auth token".to_string()
|
||||
)))),
|
||||
Ok(Some(WsMsg::AuthorizeExpired))
|
||||
);
|
||||
self.current_user = Some(user.clone());
|
||||
self.current_user_project = self.load_user_project().ok();
|
||||
self.current_project = self.load_project().ok();
|
||||
|
||||
block_on(self.join_channel(ctx.address().recipient()));
|
||||
Ok(Some(WsMsg::AuthorizeLoaded(Ok(user))))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CheckBindToken {
|
||||
pub bind_token: uuid::Uuid,
|
||||
}
|
||||
|
||||
impl WsHandler<CheckBindToken> for WebSocketActor {
|
||||
fn handle_msg(&mut self, msg: CheckBindToken, _ctx: &mut Self::Context) -> WsResult {
|
||||
let token: Token = db_or_debug_and_return!(
|
||||
self,
|
||||
FindBindToken {
|
||||
token: msg.bind_token,
|
||||
},
|
||||
Ok(Some(WsMsg::BindTokenBad)),
|
||||
Ok(None)
|
||||
);
|
||||
Ok(Some(WsMsg::BindTokenOk(token.access_token)))
|
||||
}
|
||||
}
|
90
actors/websocket-actor/src/handlers/comments.rs
Normal file
90
actors/websocket-actor/src/handlers/comments.rs
Normal file
@ -0,0 +1,90 @@
|
||||
use {
|
||||
crate::{db_or_debug_and_return, WebSocketActor, WsHandler, WsResult},
|
||||
futures::executor::block_on,
|
||||
jirs_data::{CommentId, CreateCommentPayload, IssueId, UpdateCommentPayload, WsMsg},
|
||||
};
|
||||
|
||||
pub struct LoadIssueComments {
|
||||
pub issue_id: IssueId,
|
||||
}
|
||||
|
||||
impl WsHandler<LoadIssueComments> for WebSocketActor {
|
||||
fn handle_msg(&mut self, msg: LoadIssueComments, _ctx: &mut Self::Context) -> WsResult {
|
||||
self.require_user()?;
|
||||
|
||||
let comments = db_or_debug_and_return!(
|
||||
self,
|
||||
database_actor::comments::LoadIssueComments {
|
||||
issue_id: msg.issue_id,
|
||||
}
|
||||
);
|
||||
|
||||
Ok(Some(WsMsg::IssueCommentsLoaded(comments)))
|
||||
}
|
||||
}
|
||||
|
||||
impl WsHandler<CreateCommentPayload> for WebSocketActor {
|
||||
fn handle_msg(&mut self, mut msg: CreateCommentPayload, ctx: &mut Self::Context) -> WsResult {
|
||||
use database_actor::comments::CreateComment;
|
||||
|
||||
let user_id = self.require_user()?.id;
|
||||
if msg.user_id.is_none() {
|
||||
msg.user_id = Some(user_id);
|
||||
}
|
||||
let issue_id = msg.issue_id;
|
||||
let _ = db_or_debug_and_return!(
|
||||
self,
|
||||
CreateComment {
|
||||
user_id,
|
||||
issue_id,
|
||||
body: msg.body,
|
||||
}
|
||||
);
|
||||
self.handle_msg(LoadIssueComments { issue_id }, ctx)
|
||||
}
|
||||
}
|
||||
|
||||
impl WsHandler<UpdateCommentPayload> for WebSocketActor {
|
||||
fn handle_msg(&mut self, msg: UpdateCommentPayload, _ctx: &mut Self::Context) -> WsResult {
|
||||
use database_actor::comments::UpdateComment;
|
||||
|
||||
let user_id = self.require_user()?.id;
|
||||
|
||||
let UpdateCommentPayload {
|
||||
id: comment_id,
|
||||
body,
|
||||
} = msg;
|
||||
|
||||
let comment = db_or_debug_and_return!(
|
||||
self,
|
||||
UpdateComment {
|
||||
comment_id,
|
||||
user_id,
|
||||
body,
|
||||
}
|
||||
);
|
||||
self.broadcast(&WsMsg::CommentUpdated(comment));
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct DeleteComment {
|
||||
pub comment_id: CommentId,
|
||||
}
|
||||
|
||||
impl WsHandler<DeleteComment> for WebSocketActor {
|
||||
fn handle_msg(&mut self, msg: DeleteComment, _ctx: &mut Self::Context) -> WsResult {
|
||||
use database_actor::comments::DeleteComment;
|
||||
|
||||
let user_id = self.require_user()?.id;
|
||||
|
||||
let n = db_or_debug_and_return!(
|
||||
self,
|
||||
DeleteComment {
|
||||
comment_id: msg.comment_id,
|
||||
user_id,
|
||||
}
|
||||
);
|
||||
Ok(Some(WsMsg::CommentDeleted(msg.comment_id, n)))
|
||||
}
|
||||
}
|
132
actors/websocket-actor/src/handlers/epics.rs
Normal file
132
actors/websocket-actor/src/handlers/epics.rs
Normal file
@ -0,0 +1,132 @@
|
||||
use jirs_data::IssueType;
|
||||
use {
|
||||
crate::{db_or_debug_and_return, WebSocketActor, WsHandler, WsResult},
|
||||
futures::executor::block_on,
|
||||
jirs_data::{DescriptionString, EpicId, NameString, UserProject, WsMsg},
|
||||
};
|
||||
|
||||
pub struct LoadEpics;
|
||||
|
||||
impl WsHandler<LoadEpics> for WebSocketActor {
|
||||
fn handle_msg(&mut self, _msg: LoadEpics, _ctx: &mut Self::Context) -> WsResult {
|
||||
let project_id = self.require_user_project()?.project_id;
|
||||
let epics = db_or_debug_and_return!(self, database_actor::epics::LoadEpics { project_id });
|
||||
Ok(Some(WsMsg::EpicsLoaded(epics)))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CreateEpic {
|
||||
pub name: NameString,
|
||||
pub description: Option<DescriptionString>,
|
||||
pub description_html: Option<DescriptionString>,
|
||||
}
|
||||
|
||||
impl WsHandler<CreateEpic> for WebSocketActor {
|
||||
fn handle_msg(&mut self, msg: CreateEpic, _ctx: &mut Self::Context) -> WsResult {
|
||||
let CreateEpic {
|
||||
name,
|
||||
description,
|
||||
description_html,
|
||||
} = msg;
|
||||
let UserProject {
|
||||
user_id,
|
||||
project_id,
|
||||
..
|
||||
} = self.require_user_project()?;
|
||||
let epic = db_or_debug_and_return!(
|
||||
self,
|
||||
database_actor::epics::CreateEpic {
|
||||
user_id: *user_id,
|
||||
project_id: *project_id,
|
||||
description,
|
||||
description_html,
|
||||
name,
|
||||
}
|
||||
);
|
||||
Ok(Some(WsMsg::EpicCreated(epic)))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct UpdateEpic {
|
||||
pub epic_id: EpicId,
|
||||
pub name: NameString,
|
||||
}
|
||||
|
||||
impl WsHandler<UpdateEpic> for WebSocketActor {
|
||||
fn handle_msg(&mut self, msg: UpdateEpic, _ctx: &mut Self::Context) -> WsResult {
|
||||
let UpdateEpic { epic_id, name } = msg;
|
||||
let UserProject { project_id, .. } = self.require_user_project()?;
|
||||
let epic = db_or_debug_and_return!(
|
||||
self,
|
||||
database_actor::epics::UpdateEpic {
|
||||
project_id: *project_id,
|
||||
epic_id: epic_id,
|
||||
name: name.clone(),
|
||||
}
|
||||
);
|
||||
Ok(Some(WsMsg::EpicUpdated(epic)))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct DeleteEpic {
|
||||
pub epic_id: EpicId,
|
||||
}
|
||||
|
||||
impl WsHandler<DeleteEpic> for WebSocketActor {
|
||||
fn handle_msg(&mut self, msg: DeleteEpic, _ctx: &mut Self::Context) -> WsResult {
|
||||
let DeleteEpic { epic_id } = msg;
|
||||
let UserProject { user_id, .. } = self.require_user_project()?;
|
||||
let n = db_or_debug_and_return!(
|
||||
self,
|
||||
database_actor::epics::DeleteEpic {
|
||||
user_id: *user_id,
|
||||
epic_id: epic_id,
|
||||
}
|
||||
);
|
||||
Ok(Some(WsMsg::EpicDeleted(epic_id, n)))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TransformEpic {
|
||||
pub epic_id: EpicId,
|
||||
pub issue_type: IssueType,
|
||||
}
|
||||
|
||||
impl WsHandler<TransformEpic> for WebSocketActor {
|
||||
fn handle_msg(&mut self, msg: TransformEpic, _ctx: &mut Self::Context) -> WsResult {
|
||||
let epic: jirs_data::Epic = db_or_debug_and_return!(
|
||||
self,
|
||||
database_actor::epics::FindEpic {
|
||||
epic_id: msg.epic_id
|
||||
}
|
||||
);
|
||||
let issue: database_actor::models::Issue = db_or_debug_and_return!(
|
||||
self,
|
||||
database_actor::issues::CreateIssue {
|
||||
title: epic.name,
|
||||
issue_type: msg.issue_type,
|
||||
issue_status_id: 0,
|
||||
priority: Default::default(),
|
||||
description: epic.description_html,
|
||||
description_text: epic.description,
|
||||
estimate: None,
|
||||
time_spent: None,
|
||||
time_remaining: None,
|
||||
project_id: epic.project_id,
|
||||
reporter_id: epic.user_id,
|
||||
user_ids: vec![epic.user_id],
|
||||
epic_id: None
|
||||
}
|
||||
);
|
||||
let n = db_or_debug_and_return!(
|
||||
self,
|
||||
database_actor::epics::DeleteEpic {
|
||||
user_id: epic.user_id,
|
||||
epic_id: epic.id
|
||||
}
|
||||
);
|
||||
self.broadcast(&WsMsg::EpicDeleted(msg.epic_id, n));
|
||||
self.broadcast(&WsMsg::IssueCreated(issue.into()));
|
||||
Ok(None)
|
||||
}
|
||||
}
|
22
actors/websocket-actor/src/handlers/hi.rs
Normal file
22
actors/websocket-actor/src/handlers/hi.rs
Normal file
@ -0,0 +1,22 @@
|
||||
use {
|
||||
crate::{actor_or_debug_and_return, WebSocketActor, WsHandler, WsResult},
|
||||
futures::executor::block_on,
|
||||
jirs_data::{Code, Lang, WsMsg},
|
||||
};
|
||||
|
||||
pub struct HighlightCode(pub Lang, pub Code);
|
||||
|
||||
impl WsHandler<HighlightCode> for WebSocketActor {
|
||||
fn handle_msg(&mut self, msg: HighlightCode, _ctx: &mut Self::Context) -> WsResult {
|
||||
self.require_user()?.id;
|
||||
let res = actor_or_debug_and_return!(
|
||||
self,
|
||||
hi,
|
||||
highlight_actor::HighlightCode {
|
||||
code: msg.1,
|
||||
lang: msg.0,
|
||||
}
|
||||
);
|
||||
Ok(Some(WsMsg::HighlightedCode(res)))
|
||||
}
|
||||
}
|
148
actors/websocket-actor/src/handlers/invitations.rs
Normal file
148
actors/websocket-actor/src/handlers/invitations.rs
Normal file
@ -0,0 +1,148 @@
|
||||
use {
|
||||
crate::{
|
||||
db_or_debug_and_return, mail_or_debug_and_return, server::InnerMsg, WebSocketActor,
|
||||
WsHandler, WsMessageSender, WsResult,
|
||||
},
|
||||
database_actor::{invitations, messages::CreateMessageReceiver},
|
||||
futures::executor::block_on,
|
||||
jirs_data::{
|
||||
EmailString, InvitationId, InvitationToken, MessageType, UserRole, UsernameString, WsMsg,
|
||||
},
|
||||
};
|
||||
|
||||
pub struct ListInvitation;
|
||||
|
||||
impl WsHandler<ListInvitation> for WebSocketActor {
|
||||
fn handle_msg(&mut self, _msg: ListInvitation, _ctx: &mut Self::Context) -> WsResult {
|
||||
let user_id = match self.current_user.as_ref().map(|u| u.id) {
|
||||
Some(id) => id,
|
||||
_ => return Ok(None),
|
||||
};
|
||||
let v = db_or_debug_and_return!(self, invitations::ListInvitation { user_id });
|
||||
Ok(Some(WsMsg::InvitationListLoaded(v)))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CreateInvitation {
|
||||
pub email: EmailString,
|
||||
pub name: UsernameString,
|
||||
pub role: UserRole,
|
||||
}
|
||||
|
||||
impl WsHandler<CreateInvitation> for WebSocketActor {
|
||||
fn handle_msg(&mut self, msg: CreateInvitation, _ctx: &mut Self::Context) -> WsResult {
|
||||
let project_id = match self.current_user_project.as_ref() {
|
||||
Some(up) => up.project_id,
|
||||
_ => return Ok(None),
|
||||
};
|
||||
let (user_id, inviter_name) = self.require_user().map(|u| (u.id, u.name.clone()))?;
|
||||
|
||||
let CreateInvitation { email, name, role } = msg;
|
||||
let invitation = db_or_debug_and_return!(
|
||||
self,
|
||||
database_actor::invitations::CreateInvitation {
|
||||
user_id,
|
||||
project_id,
|
||||
email: email.clone(),
|
||||
name: name.clone(),
|
||||
role,
|
||||
},
|
||||
Ok(Some(WsMsg::InvitationSendFailure)),
|
||||
Ok(Some(WsMsg::InvitationSendFailure))
|
||||
);
|
||||
let _ = mail_or_debug_and_return!(
|
||||
self,
|
||||
mail_actor::invite::Invite {
|
||||
bind_token: invitation.bind_token,
|
||||
email: invitation.email,
|
||||
inviter_name,
|
||||
},
|
||||
Ok(Some(WsMsg::InvitationSendFailure)),
|
||||
Ok(Some(WsMsg::InvitationSendFailure))
|
||||
);
|
||||
|
||||
// If user exists then send message to him
|
||||
if let Ok(Ok(message)) = block_on(self.db.send(database_actor::messages::CreateMessage {
|
||||
receiver: CreateMessageReceiver::Lookup { name, email },
|
||||
sender_id: user_id,
|
||||
summary: "You have been invited to project".to_string(),
|
||||
description: "You have been invited to project".to_string(),
|
||||
message_type: MessageType::ReceivedInvitation,
|
||||
hyper_link: format!("#{}", invitation.bind_token),
|
||||
})) {
|
||||
self.addr.do_send(InnerMsg::SendToUser(
|
||||
message.receiver_id,
|
||||
WsMsg::MessageUpdated(message),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(Some(WsMsg::InvitationSendSuccess))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct DeleteInvitation {
|
||||
pub id: InvitationId,
|
||||
}
|
||||
|
||||
impl WsHandler<DeleteInvitation> for WebSocketActor {
|
||||
fn handle_msg(&mut self, msg: DeleteInvitation, _ctx: &mut Self::Context) -> WsResult {
|
||||
self.require_user()?;
|
||||
let DeleteInvitation { id } = msg;
|
||||
let _ = db_or_debug_and_return!(self, invitations::DeleteInvitation { id });
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct RevokeInvitation {
|
||||
pub id: InvitationId,
|
||||
}
|
||||
|
||||
impl WsHandler<RevokeInvitation> for WebSocketActor {
|
||||
fn handle_msg(&mut self, msg: RevokeInvitation, _ctx: &mut Self::Context) -> WsResult {
|
||||
self.require_user()?;
|
||||
let RevokeInvitation { id } = msg;
|
||||
let _ = db_or_debug_and_return!(self, invitations::RevokeInvitation { id });
|
||||
Ok(Some(WsMsg::InvitationRevokeSuccess(id)))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct AcceptInvitation {
|
||||
pub invitation_token: InvitationToken,
|
||||
}
|
||||
|
||||
impl WsHandler<AcceptInvitation> for WebSocketActor {
|
||||
fn handle_msg(&mut self, msg: AcceptInvitation, ctx: &mut Self::Context) -> WsResult {
|
||||
let AcceptInvitation { invitation_token } = msg;
|
||||
let token = db_or_debug_and_return!(
|
||||
self,
|
||||
invitations::AcceptInvitation { invitation_token },
|
||||
Ok(Some(WsMsg::InvitationAcceptFailure(invitation_token))),
|
||||
Ok(Some(WsMsg::InvitationAcceptFailure(invitation_token)))
|
||||
);
|
||||
|
||||
for message in crate::actor_or_debug_and_fallback!(
|
||||
self,
|
||||
db,
|
||||
database_actor::messages::LookupMessagesByToken {
|
||||
token: invitation_token,
|
||||
user_id: token.user_id,
|
||||
},
|
||||
vec![],
|
||||
vec![]
|
||||
) {
|
||||
crate::actor_or_debug_and_ignore!(
|
||||
self,
|
||||
db,
|
||||
database_actor::messages::MarkMessageSeen {
|
||||
user_id: token.user_id,
|
||||
message_id: message.id,
|
||||
},
|
||||
|n| {
|
||||
ctx.send_msg(&WsMsg::MessageMarkedSeen(message.id, n));
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
Ok(Some(WsMsg::InvitationAcceptSuccess(token.access_token)))
|
||||
}
|
||||
}
|
91
actors/websocket-actor/src/handlers/issue_statuses.rs
Normal file
91
actors/websocket-actor/src/handlers/issue_statuses.rs
Normal file
@ -0,0 +1,91 @@
|
||||
use {
|
||||
crate::{db_or_debug_and_return, WebSocketActor, WsHandler, WsResult},
|
||||
database_actor::issue_statuses,
|
||||
futures::executor::block_on,
|
||||
jirs_data::{IssueStatusId, Position, TitleString, WsMsg},
|
||||
};
|
||||
|
||||
pub struct LoadIssueStatuses;
|
||||
|
||||
impl WsHandler<LoadIssueStatuses> for WebSocketActor {
|
||||
fn handle_msg(&mut self, _msg: LoadIssueStatuses, _ctx: &mut Self::Context) -> WsResult {
|
||||
let project_id = self.require_user_project()?.project_id;
|
||||
|
||||
let v = db_or_debug_and_return!(self, issue_statuses::LoadIssueStatuses { project_id });
|
||||
Ok(Some(WsMsg::IssueStatusesLoaded(v)))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CreateIssueStatus {
|
||||
pub position: i32,
|
||||
pub name: TitleString,
|
||||
}
|
||||
|
||||
impl WsHandler<CreateIssueStatus> for WebSocketActor {
|
||||
fn handle_msg(&mut self, msg: CreateIssueStatus, _ctx: &mut Self::Context) -> WsResult {
|
||||
let project_id = self.require_user_project()?.project_id;
|
||||
|
||||
let CreateIssueStatus { position, name } = msg;
|
||||
let issue_status = db_or_debug_and_return!(
|
||||
self,
|
||||
issue_statuses::CreateIssueStatus {
|
||||
project_id,
|
||||
position,
|
||||
name,
|
||||
}
|
||||
);
|
||||
Ok(Some(WsMsg::IssueStatusCreated(issue_status)))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct DeleteIssueStatus {
|
||||
pub issue_status_id: IssueStatusId,
|
||||
}
|
||||
|
||||
impl WsHandler<DeleteIssueStatus> for WebSocketActor {
|
||||
fn handle_msg(&mut self, msg: DeleteIssueStatus, _ctx: &mut Self::Context) -> WsResult {
|
||||
let project_id = self.require_user_project()?.project_id;
|
||||
|
||||
let DeleteIssueStatus { issue_status_id } = msg;
|
||||
let n = db_or_debug_and_return!(
|
||||
self,
|
||||
issue_statuses::DeleteIssueStatus {
|
||||
issue_status_id,
|
||||
project_id,
|
||||
}
|
||||
);
|
||||
Ok(Some(WsMsg::IssueStatusDeleted(msg.issue_status_id, n)))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct UpdateIssueStatus {
|
||||
pub issue_status_id: IssueStatusId,
|
||||
pub position: Position,
|
||||
pub name: TitleString,
|
||||
}
|
||||
|
||||
impl WsHandler<UpdateIssueStatus> for WebSocketActor {
|
||||
fn handle_msg(&mut self, msg: UpdateIssueStatus, _ctx: &mut Self::Context) -> WsResult {
|
||||
let project_id = self.require_user_project()?.project_id;
|
||||
|
||||
let UpdateIssueStatus {
|
||||
issue_status_id,
|
||||
position,
|
||||
name,
|
||||
} = msg;
|
||||
let issue_status = db_or_debug_and_return!(
|
||||
self,
|
||||
issue_statuses::UpdateIssueStatus {
|
||||
issue_status_id,
|
||||
position,
|
||||
name,
|
||||
project_id,
|
||||
}
|
||||
);
|
||||
let msg = Some(WsMsg::IssueStatusUpdated(issue_status));
|
||||
if let Some(ws_msg) = msg.as_ref() {
|
||||
self.broadcast(ws_msg)
|
||||
}
|
||||
Ok(msg)
|
||||
}
|
||||
}
|
237
actors/websocket-actor/src/handlers/issues.rs
Normal file
237
actors/websocket-actor/src/handlers/issues.rs
Normal file
@ -0,0 +1,237 @@
|
||||
use {
|
||||
crate::{db_or_debug_and_return, WebSocketActor, WsHandler, WsResult},
|
||||
database_actor::{
|
||||
issue_assignees::LoadAssignees,
|
||||
issues::{LoadProjectIssues, UpdateIssue},
|
||||
},
|
||||
futures::executor::block_on,
|
||||
jirs_data::{
|
||||
CreateIssuePayload, IssueAssignee, IssueFieldId, IssueId, IssueStatusId, ListPosition,
|
||||
PayloadVariant, WsMsg,
|
||||
},
|
||||
std::collections::HashMap,
|
||||
};
|
||||
|
||||
pub struct UpdateIssueHandler {
|
||||
pub id: i32,
|
||||
pub field_id: IssueFieldId,
|
||||
pub payload: PayloadVariant,
|
||||
}
|
||||
|
||||
impl WsHandler<UpdateIssueHandler> for WebSocketActor {
|
||||
fn handle_msg(&mut self, msg: UpdateIssueHandler, _ctx: &mut Self::Context) -> WsResult {
|
||||
self.require_user()?;
|
||||
|
||||
let UpdateIssueHandler {
|
||||
id,
|
||||
field_id,
|
||||
payload,
|
||||
} = msg;
|
||||
|
||||
let mut msg = UpdateIssue {
|
||||
issue_id: id,
|
||||
..Default::default()
|
||||
};
|
||||
match (field_id, payload) {
|
||||
(IssueFieldId::Type, PayloadVariant::IssueType(t)) => {
|
||||
msg.issue_type = Some(t);
|
||||
}
|
||||
(IssueFieldId::Title, PayloadVariant::String(s)) => {
|
||||
msg.title = Some(s);
|
||||
}
|
||||
(IssueFieldId::Description, PayloadVariant::String(s)) => {
|
||||
let html: String = {
|
||||
use pulldown_cmark::*;
|
||||
let parser = pulldown_cmark::Parser::new(s.as_str());
|
||||
enum ParseState {
|
||||
Code(highlight_actor::TextHighlightCode),
|
||||
Other,
|
||||
}
|
||||
let mut state = ParseState::Other;
|
||||
|
||||
let parser = parser.flat_map(|event| match event {
|
||||
Event::Text(s) => {
|
||||
if let ParseState::Code(h) = &mut state {
|
||||
h.code.push_str(s.as_ref());
|
||||
return vec![];
|
||||
}
|
||||
vec![Event::Text(s)]
|
||||
}
|
||||
Event::Start(Tag::CodeBlock(CodeBlockKind::Fenced(name))) => {
|
||||
state = ParseState::Code(highlight_actor::TextHighlightCode {
|
||||
lang: name.to_string(),
|
||||
code: String::new(),
|
||||
});
|
||||
vec![Event::Start(Tag::CodeBlock(CodeBlockKind::Fenced(name)))]
|
||||
}
|
||||
Event::End(Tag::CodeBlock(CodeBlockKind::Fenced(lang))) => {
|
||||
let ev = if let ParseState::Code(h) = &mut state {
|
||||
let mut msg = highlight_actor::TextHighlightCode {
|
||||
code: String::new(),
|
||||
lang: String::new(),
|
||||
};
|
||||
std::mem::swap(h, &mut msg);
|
||||
let highlighted =
|
||||
match futures::executor::block_on(self.hi.send(msg)) {
|
||||
Ok(Ok(res)) => res,
|
||||
_ => s.to_string(),
|
||||
};
|
||||
vec![
|
||||
Event::Html(highlighted.into()),
|
||||
Event::End(Tag::CodeBlock(CodeBlockKind::Fenced(lang))),
|
||||
]
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
state = ParseState::Other;
|
||||
ev
|
||||
}
|
||||
_ => vec![event],
|
||||
});
|
||||
let mut buff = String::new();
|
||||
let _ = html::push_html(&mut buff, parser);
|
||||
buff
|
||||
};
|
||||
msg.description = Some(html);
|
||||
msg.description_text = Some(s);
|
||||
}
|
||||
(IssueFieldId::IssueStatusId, PayloadVariant::I32(s)) => {
|
||||
msg.issue_status_id = Some(s);
|
||||
}
|
||||
(IssueFieldId::ListPosition, PayloadVariant::I32(i)) => {
|
||||
msg.list_position = Some(i);
|
||||
}
|
||||
(IssueFieldId::Assignees, PayloadVariant::VecI32(v)) => {
|
||||
msg.user_ids = Some(v);
|
||||
}
|
||||
(IssueFieldId::Reporter, PayloadVariant::I32(i)) => {
|
||||
msg.reporter_id = Some(i);
|
||||
}
|
||||
(IssueFieldId::Priority, PayloadVariant::IssuePriority(p)) => {
|
||||
msg.priority = Some(p);
|
||||
}
|
||||
(IssueFieldId::Estimate, PayloadVariant::OptionI32(o)) => {
|
||||
msg.estimate = o;
|
||||
}
|
||||
(IssueFieldId::TimeSpent, PayloadVariant::OptionI32(o)) => {
|
||||
msg.time_spent = o;
|
||||
}
|
||||
(IssueFieldId::TimeRemaining, PayloadVariant::OptionI32(o)) => {
|
||||
msg.time_remaining = o;
|
||||
}
|
||||
(IssueFieldId::EpicName, PayloadVariant::OptionI32(o)) => {
|
||||
msg.epic_id = Some(o);
|
||||
}
|
||||
_ => (),
|
||||
};
|
||||
|
||||
let issue = db_or_debug_and_return!(self, msg);
|
||||
let mut issue: jirs_data::Issue = issue.into();
|
||||
|
||||
let assignees: Vec<IssueAssignee> =
|
||||
db_or_debug_and_return!(self, LoadAssignees { issue_id: issue.id });
|
||||
|
||||
for assignee in assignees {
|
||||
issue.user_ids.push(assignee.user_id);
|
||||
}
|
||||
self.broadcast(&WsMsg::IssueUpdated(issue));
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
impl WsHandler<CreateIssuePayload> for WebSocketActor {
|
||||
fn handle_msg(&mut self, msg: CreateIssuePayload, _ctx: &mut Self::Context) -> WsResult {
|
||||
self.require_user()?;
|
||||
let msg = database_actor::issues::CreateIssue {
|
||||
title: msg.title,
|
||||
issue_type: msg.issue_type,
|
||||
issue_status_id: msg.issue_status_id,
|
||||
priority: msg.priority,
|
||||
description: msg.description,
|
||||
description_text: msg.description_text,
|
||||
estimate: msg.estimate,
|
||||
time_spent: msg.time_spent,
|
||||
time_remaining: msg.time_remaining,
|
||||
project_id: msg.project_id,
|
||||
reporter_id: msg.reporter_id,
|
||||
user_ids: msg.user_ids,
|
||||
epic_id: msg.epic_id,
|
||||
};
|
||||
let issue = db_or_debug_and_return!(self, msg);
|
||||
Ok(Some(WsMsg::IssueCreated(issue.into())))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct DeleteIssue {
|
||||
pub id: IssueId,
|
||||
}
|
||||
|
||||
impl WsHandler<DeleteIssue> for WebSocketActor {
|
||||
fn handle_msg(&mut self, msg: DeleteIssue, _ctx: &mut Self::Context) -> WsResult {
|
||||
self.require_user()?;
|
||||
let n = db_or_debug_and_return!(
|
||||
self,
|
||||
database_actor::issues::DeleteIssue { issue_id: msg.id }
|
||||
);
|
||||
Ok(Some(WsMsg::IssueDeleted(msg.id, n)))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct LoadIssues;
|
||||
|
||||
impl WsHandler<LoadIssues> for WebSocketActor {
|
||||
fn handle_msg(&mut self, _msg: LoadIssues, _ctx: &mut Self::Context) -> WsResult {
|
||||
let project_id = self.require_user_project()?.project_id;
|
||||
|
||||
let v = db_or_debug_and_return!(self, LoadProjectIssues { project_id });
|
||||
let issues: Vec<jirs_data::Issue> = v.into_iter().map(|i| i.into()).collect();
|
||||
let mut issue_map = HashMap::new();
|
||||
let mut queue = vec![];
|
||||
for issue in issues {
|
||||
let f = self.db.send(LoadAssignees { issue_id: issue.id });
|
||||
queue.push(f);
|
||||
issue_map.insert(issue.id, issue);
|
||||
}
|
||||
for f in queue {
|
||||
if let Ok(Ok(assignees)) = block_on(f) {
|
||||
for assignee in assignees {
|
||||
if let Some(issue) = issue_map.get_mut(&assignee.issue_id) {
|
||||
issue.user_ids.push(assignee.user_id);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
let mut issues = vec![];
|
||||
for (_, issue) in issue_map {
|
||||
issues.push(issue);
|
||||
}
|
||||
issues.sort_by(|a, b| a.list_position.cmp(&b.list_position));
|
||||
|
||||
Ok(Some(WsMsg::ProjectIssuesLoaded(issues)))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SyncIssueListPosition(pub Vec<(IssueId, ListPosition, IssueStatusId, Option<IssueId>)>);
|
||||
|
||||
impl WsHandler<SyncIssueListPosition> for WebSocketActor {
|
||||
fn handle_msg(&mut self, msg: SyncIssueListPosition, ctx: &mut Self::Context) -> WsResult {
|
||||
let _project_id = self.require_user_project()?.project_id;
|
||||
for (issue_id, list_position, status_id, epic_id) in msg.0 {
|
||||
crate::actor_or_debug_and_ignore!(
|
||||
self,
|
||||
db,
|
||||
database_actor::issues::UpdateIssue {
|
||||
issue_id,
|
||||
list_position: Some(list_position),
|
||||
issue_status_id: Some(status_id),
|
||||
epic_id: Some(epic_id),
|
||||
..Default::default()
|
||||
},
|
||||
|_| {}
|
||||
);
|
||||
}
|
||||
|
||||
self.handle_msg(LoadIssues, ctx)
|
||||
}
|
||||
}
|
34
actors/websocket-actor/src/handlers/messages.rs
Normal file
34
actors/websocket-actor/src/handlers/messages.rs
Normal file
@ -0,0 +1,34 @@
|
||||
use {
|
||||
crate::{db_or_debug_and_return, WebSocketActor, WsHandler, WsResult},
|
||||
database_actor::messages,
|
||||
futures::executor::block_on,
|
||||
jirs_data::{MessageId, WsMsg},
|
||||
};
|
||||
|
||||
pub struct LoadMessages;
|
||||
|
||||
impl WsHandler<LoadMessages> for WebSocketActor {
|
||||
fn handle_msg(&mut self, _msg: LoadMessages, _ctx: &mut Self::Context) -> WsResult {
|
||||
let user_id = self.require_user()?.id;
|
||||
let v = db_or_debug_and_return!(self, messages::LoadMessages { user_id });
|
||||
Ok(Some(WsMsg::MessagesLoaded(v)))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct MarkMessageSeen {
|
||||
pub id: MessageId,
|
||||
}
|
||||
|
||||
impl WsHandler<MarkMessageSeen> for WebSocketActor {
|
||||
fn handle_msg(&mut self, msg: MarkMessageSeen, _ctx: &mut Self::Context) -> WsResult {
|
||||
let user_id = self.require_user()?.id;
|
||||
let count = db_or_debug_and_return!(
|
||||
self,
|
||||
messages::MarkMessageSeen {
|
||||
message_id: msg.id,
|
||||
user_id,
|
||||
}
|
||||
);
|
||||
Ok(Some(WsMsg::MessageMarkedSeen(msg.id, count)))
|
||||
}
|
||||
}
|
16
actors/websocket-actor/src/handlers/mod.rs
Normal file
16
actors/websocket-actor/src/handlers/mod.rs
Normal file
@ -0,0 +1,16 @@
|
||||
pub use {
|
||||
auth::*, comments::*, epics::*, hi::*, invitations::*, issue_statuses::*, issues::*,
|
||||
messages::*, projects::*, user_projects::*, users::*,
|
||||
};
|
||||
|
||||
pub mod auth;
|
||||
pub mod comments;
|
||||
pub mod epics;
|
||||
pub mod hi;
|
||||
pub mod invitations;
|
||||
pub mod issue_statuses;
|
||||
pub mod issues;
|
||||
pub mod messages;
|
||||
pub mod projects;
|
||||
pub mod user_projects;
|
||||
pub mod users;
|
42
actors/websocket-actor/src/handlers/projects.rs
Normal file
42
actors/websocket-actor/src/handlers/projects.rs
Normal file
@ -0,0 +1,42 @@
|
||||
use {
|
||||
crate::{db_or_debug_and_return, WebSocketActor, WsHandler, WsResult},
|
||||
database_actor as db,
|
||||
futures::executor::block_on,
|
||||
jirs_data::{UpdateProjectPayload, UserProject, WsMsg},
|
||||
};
|
||||
|
||||
impl WsHandler<UpdateProjectPayload> for WebSocketActor {
|
||||
fn handle_msg(&mut self, msg: UpdateProjectPayload, _ctx: &mut Self::Context) -> WsResult {
|
||||
let UserProject {
|
||||
user_id,
|
||||
project_id,
|
||||
..
|
||||
} = self.require_user_project()?;
|
||||
let _ = db_or_debug_and_return!(
|
||||
self,
|
||||
database_actor::projects::UpdateProject {
|
||||
project_id: *project_id,
|
||||
name: msg.name,
|
||||
url: msg.url,
|
||||
description: msg.description,
|
||||
category: msg.category,
|
||||
time_tracking: msg.time_tracking,
|
||||
}
|
||||
);
|
||||
let projects = db_or_debug_and_return!(
|
||||
self,
|
||||
database_actor::projects::LoadProjects { user_id: *user_id }
|
||||
);
|
||||
Ok(Some(WsMsg::ProjectsLoaded(projects)))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct LoadProjects;
|
||||
|
||||
impl WsHandler<LoadProjects> for WebSocketActor {
|
||||
fn handle_msg(&mut self, _msg: LoadProjects, _ctx: &mut Self::Context) -> WsResult {
|
||||
let user_id = self.require_user()?.id;
|
||||
let v = db_or_debug_and_return!(self, db::projects::LoadProjects { user_id });
|
||||
Ok(Some(WsMsg::ProjectsLoaded(v)))
|
||||
}
|
||||
}
|
35
actors/websocket-actor/src/handlers/user_projects.rs
Normal file
35
actors/websocket-actor/src/handlers/user_projects.rs
Normal file
@ -0,0 +1,35 @@
|
||||
use {
|
||||
crate::{db_or_debug_and_return, WebSocketActor, WsHandler, WsResult},
|
||||
database_actor as db,
|
||||
futures::executor::block_on,
|
||||
jirs_data::{UserProjectId, WsMsg},
|
||||
};
|
||||
|
||||
pub struct LoadUserProjects;
|
||||
|
||||
impl WsHandler<LoadUserProjects> for WebSocketActor {
|
||||
fn handle_msg(&mut self, _msg: LoadUserProjects, _ctx: &mut Self::Context) -> WsResult {
|
||||
let user_id = self.require_user()?.id;
|
||||
let v = db_or_debug_and_return!(self, db::user_projects::LoadUserProjects { user_id });
|
||||
Ok(Some(WsMsg::UserProjectsLoaded(v)))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SetCurrentUserProject {
|
||||
pub id: UserProjectId,
|
||||
}
|
||||
|
||||
impl WsHandler<SetCurrentUserProject> for WebSocketActor {
|
||||
fn handle_msg(&mut self, msg: SetCurrentUserProject, _ctx: &mut Self::Context) -> WsResult {
|
||||
let user_id = self.require_user()?.id;
|
||||
let user_project = db_or_debug_and_return!(
|
||||
self,
|
||||
db::user_projects::ChangeCurrentUserProject {
|
||||
user_id,
|
||||
id: msg.id,
|
||||
}
|
||||
);
|
||||
self.current_user_project = Some(user_project.clone());
|
||||
Ok(Some(WsMsg::UserProjectCurrentChanged(user_project)))
|
||||
}
|
||||
}
|
111
actors/websocket-actor/src/handlers/users.rs
Normal file
111
actors/websocket-actor/src/handlers/users.rs
Normal file
@ -0,0 +1,111 @@
|
||||
use {
|
||||
crate::{
|
||||
db_or_debug_and_return, handlers::auth::Authenticate, WebSocketActor, WsHandler, WsResult,
|
||||
},
|
||||
database_actor::{self, users::Register as DbRegister},
|
||||
futures::executor::block_on,
|
||||
jirs_data::{UserId, UserProject, UserRole, WsMsg},
|
||||
};
|
||||
|
||||
pub struct LoadProjectUsers;
|
||||
|
||||
impl WsHandler<LoadProjectUsers> for WebSocketActor {
|
||||
fn handle_msg(&mut self, _msg: LoadProjectUsers, _ctx: &mut Self::Context) -> WsResult {
|
||||
use database_actor::users::LoadProjectUsers as Msg;
|
||||
|
||||
let project_id = self.require_user_project()?.project_id;
|
||||
let v = db_or_debug_and_return!(self, Msg { project_id });
|
||||
Ok(Some(WsMsg::ProjectUsersLoaded(v)))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Register {
|
||||
pub name: String,
|
||||
pub email: String,
|
||||
}
|
||||
|
||||
impl WsHandler<Register> for WebSocketActor {
|
||||
fn handle_msg(&mut self, msg: Register, ctx: &mut Self::Context) -> WsResult {
|
||||
let Register { name, email } = msg;
|
||||
let _ = db_or_debug_and_return!(
|
||||
self,
|
||||
DbRegister {
|
||||
name: name.clone(),
|
||||
email: email.clone(),
|
||||
project_id: None,
|
||||
role: UserRole::Owner,
|
||||
},
|
||||
Ok(Some(WsMsg::SignUpPairTaken)),
|
||||
Ok(None)
|
||||
);
|
||||
|
||||
match self.handle_msg(Authenticate { name, email }, ctx) {
|
||||
Ok(_) => (),
|
||||
Err(e) => return Ok(Some(e)),
|
||||
};
|
||||
|
||||
Ok(Some(WsMsg::SignUpSuccess))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct LoadInvitedUsers;
|
||||
|
||||
impl WsHandler<LoadInvitedUsers> for WebSocketActor {
|
||||
fn handle_msg(&mut self, _msg: LoadInvitedUsers, _ctx: &mut Self::Context) -> WsResult {
|
||||
let user_id = self.require_user()?.id;
|
||||
|
||||
let users =
|
||||
db_or_debug_and_return!(self, database_actor::users::LoadInvitedUsers { user_id });
|
||||
|
||||
Ok(Some(WsMsg::InvitedUsersLoaded(users)))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ProfileUpdate {
|
||||
pub name: String,
|
||||
pub email: String,
|
||||
}
|
||||
|
||||
impl WsHandler<ProfileUpdate> for WebSocketActor {
|
||||
fn handle_msg(&mut self, msg: ProfileUpdate, _ctx: &mut Self::Context) -> WsResult {
|
||||
let user_id = self.require_user()?.id;
|
||||
let ProfileUpdate { name, email } = msg;
|
||||
|
||||
let _ = db_or_debug_and_return!(
|
||||
self,
|
||||
database_actor::users::ProfileUpdate {
|
||||
user_id,
|
||||
name,
|
||||
email,
|
||||
}
|
||||
);
|
||||
|
||||
Ok(Some(WsMsg::ProfileUpdated))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct RemoveInvitedUser {
|
||||
pub user_id: UserId,
|
||||
}
|
||||
|
||||
impl WsHandler<RemoveInvitedUser> for WebSocketActor {
|
||||
fn handle_msg(&mut self, msg: RemoveInvitedUser, _ctx: &mut Self::Context) -> WsResult {
|
||||
let RemoveInvitedUser {
|
||||
user_id: invited_id,
|
||||
} = msg;
|
||||
let UserProject {
|
||||
user_id: inviter_id,
|
||||
project_id,
|
||||
..
|
||||
} = self.require_user_project()?.clone();
|
||||
let _ = db_or_debug_and_return!(
|
||||
self,
|
||||
database_actor::user_projects::RemoveInvitedUser {
|
||||
invited_id,
|
||||
inviter_id,
|
||||
project_id,
|
||||
}
|
||||
);
|
||||
Ok(Some(WsMsg::InvitedUserRemoveSuccess(invited_id)))
|
||||
}
|
||||
}
|
364
actors/websocket-actor/src/lib.rs
Normal file
364
actors/websocket-actor/src/lib.rs
Normal file
@ -0,0 +1,364 @@
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
|
||||
use {
|
||||
crate::{
|
||||
handlers::*,
|
||||
server::{InnerMsg, WsServer},
|
||||
},
|
||||
actix::{Actor, ActorContext, Addr, AsyncContext, Handler, Recipient, StreamHandler},
|
||||
actix_web::{
|
||||
get,
|
||||
web::{self, Data},
|
||||
Error, HttpRequest, HttpResponse,
|
||||
},
|
||||
actix_web_actors::ws,
|
||||
database_actor::{projects::LoadCurrentProject, user_projects::CurrentUserProject, DbExecutor},
|
||||
futures::executor::block_on,
|
||||
jirs_data::{Project, User, UserProject, WsMsg},
|
||||
log::*,
|
||||
mail_actor::MailExecutor,
|
||||
};
|
||||
|
||||
pub mod handlers;
|
||||
pub mod prelude;
|
||||
pub mod server;
|
||||
|
||||
pub type WsResult = std::result::Result<Option<WsMsg>, WsMsg>;
|
||||
|
||||
trait WsMessageSender {
|
||||
fn send_msg(&mut self, msg: &jirs_data::WsMsg);
|
||||
}
|
||||
|
||||
struct WebSocketActor {
|
||||
db: Data<Addr<DbExecutor>>,
|
||||
mail: Data<Addr<MailExecutor>>,
|
||||
addr: Addr<WsServer>,
|
||||
hi: Data<Addr<highlight_actor::HighlightActor>>,
|
||||
current_user: Option<jirs_data::User>,
|
||||
current_user_project: Option<jirs_data::UserProject>,
|
||||
current_project: Option<jirs_data::Project>,
|
||||
}
|
||||
|
||||
impl Actor for WebSocketActor {
|
||||
type Context = ws::WebsocketContext<WebSocketActor>;
|
||||
}
|
||||
|
||||
impl WsMessageSender for ws::WebsocketContext<WebSocketActor> {
|
||||
fn send_msg(&mut self, msg: &WsMsg) {
|
||||
self.binary(bincode::serialize(msg).unwrap())
|
||||
}
|
||||
}
|
||||
|
||||
impl Handler<InnerMsg> for WebSocketActor {
|
||||
type Result = ();
|
||||
|
||||
fn handle(&mut self, msg: InnerMsg, ctx: &mut <Self as Actor>::Context) -> Self::Result {
|
||||
if let InnerMsg::Transfer(msg) = msg {
|
||||
ctx.send_msg(&msg)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
impl WebSocketActor {
|
||||
fn broadcast(&self, msg: &WsMsg) {
|
||||
let project_id = match self.require_user_project() {
|
||||
Ok(up) => up.project_id,
|
||||
_ => return,
|
||||
};
|
||||
self.addr
|
||||
.do_send(InnerMsg::BroadcastToChannel(project_id, msg.clone()));
|
||||
}
|
||||
|
||||
fn handle_ws_msg(
|
||||
&mut self,
|
||||
msg: WsMsg,
|
||||
ctx: &mut <WebSocketActor as Actor>::Context,
|
||||
) -> WsResult {
|
||||
if msg != WsMsg::Ping && msg != WsMsg::Pong {
|
||||
debug!("incoming message: {:?}", msg);
|
||||
}
|
||||
|
||||
let msg = match msg {
|
||||
WsMsg::Ping => Some(WsMsg::Pong),
|
||||
WsMsg::Pong => Some(WsMsg::Ping),
|
||||
|
||||
// issues
|
||||
WsMsg::IssueUpdate(id, field_id, payload) => self.handle_msg(
|
||||
UpdateIssueHandler {
|
||||
id,
|
||||
field_id,
|
||||
payload,
|
||||
},
|
||||
ctx,
|
||||
)?,
|
||||
WsMsg::IssueCreate(payload) => self.handle_msg(payload, ctx)?,
|
||||
WsMsg::IssueDelete(id) => self.handle_msg(DeleteIssue { id }, ctx)?,
|
||||
WsMsg::IssueSyncListPosition(sync) => {
|
||||
self.handle_msg(SyncIssueListPosition(sync), ctx)?
|
||||
}
|
||||
WsMsg::ProjectIssuesLoad => self.handle_msg(LoadIssues, ctx)?,
|
||||
|
||||
// issue statuses
|
||||
WsMsg::IssueStatusesLoad => self.handle_msg(LoadIssueStatuses, ctx)?,
|
||||
WsMsg::IssueStatusDelete(issue_status_id) => {
|
||||
self.handle_msg(DeleteIssueStatus { issue_status_id }, ctx)?
|
||||
}
|
||||
WsMsg::IssueStatusUpdate(issue_status_id, name, position) => self.handle_msg(
|
||||
UpdateIssueStatus {
|
||||
issue_status_id,
|
||||
name,
|
||||
position,
|
||||
},
|
||||
ctx,
|
||||
)?,
|
||||
WsMsg::IssueStatusCreate(name, position) => {
|
||||
self.handle_msg(CreateIssueStatus { name, position }, ctx)?
|
||||
}
|
||||
|
||||
// projects
|
||||
WsMsg::ProjectsLoad => self.handle_msg(LoadProjects, ctx)?,
|
||||
WsMsg::ProjectUpdateLoad(payload) => self.handle_msg(payload, ctx)?,
|
||||
|
||||
// user projects
|
||||
WsMsg::UserProjectsLoad => self.handle_msg(LoadUserProjects, ctx)?,
|
||||
WsMsg::UserProjectSetCurrent(user_project_id) => self.handle_msg(
|
||||
SetCurrentUserProject {
|
||||
id: user_project_id,
|
||||
},
|
||||
ctx,
|
||||
)?,
|
||||
|
||||
// auth
|
||||
WsMsg::AuthorizeLoad(uuid) => self.handle_msg(CheckAuthToken { token: uuid }, ctx)?,
|
||||
WsMsg::BindTokenCheck(uuid) => {
|
||||
self.handle_msg(CheckBindToken { bind_token: uuid }, ctx)?
|
||||
}
|
||||
WsMsg::AuthenticateRequest(email, name) => {
|
||||
self.handle_msg(Authenticate { name, email }, ctx)?
|
||||
}
|
||||
|
||||
// register
|
||||
WsMsg::SignUpRequest(email, username) => self.handle_msg(
|
||||
Register {
|
||||
name: username,
|
||||
email,
|
||||
},
|
||||
ctx,
|
||||
)?,
|
||||
|
||||
// users
|
||||
WsMsg::ProjectUsersLoad => self.handle_msg(LoadProjectUsers, ctx)?,
|
||||
WsMsg::InvitedUserRemoveRequest(user_id) => {
|
||||
self.handle_msg(RemoveInvitedUser { user_id }, ctx)?
|
||||
}
|
||||
|
||||
// comments
|
||||
WsMsg::IssueCommentsLoad(issue_id) => {
|
||||
self.handle_msg(LoadIssueComments { issue_id }, ctx)?
|
||||
}
|
||||
WsMsg::CommentCreate(payload) => self.handle_msg(payload, ctx)?,
|
||||
WsMsg::CommentUpdate(payload) => self.handle_msg(payload, ctx)?,
|
||||
WsMsg::CommentDelete(comment_id) => {
|
||||
self.handle_msg(DeleteComment { comment_id }, ctx)?
|
||||
}
|
||||
|
||||
// invitations
|
||||
WsMsg::InvitationSendRequest { name, email, role } => {
|
||||
self.handle_msg(CreateInvitation { name, email, role }, ctx)?
|
||||
}
|
||||
WsMsg::InvitationListLoad => self.handle_msg(ListInvitation, ctx)?,
|
||||
WsMsg::InvitationAcceptRequest(invitation_token) => {
|
||||
self.handle_msg(AcceptInvitation { invitation_token }, ctx)?
|
||||
}
|
||||
WsMsg::InvitationRevokeRequest(id) => self.handle_msg(RevokeInvitation { id }, ctx)?,
|
||||
WsMsg::InvitedUsersLoad => self.handle_msg(LoadInvitedUsers, ctx)?,
|
||||
|
||||
// users
|
||||
WsMsg::ProfileUpdate(email, name) => {
|
||||
self.handle_msg(ProfileUpdate { email, name }, ctx)?
|
||||
}
|
||||
|
||||
// messages
|
||||
WsMsg::MessagesLoad => self.handle_msg(LoadMessages, ctx)?,
|
||||
WsMsg::MessageMarkSeen(id) => self.handle_msg(MarkMessageSeen { id }, ctx)?,
|
||||
|
||||
// epics
|
||||
WsMsg::EpicsLoad => self.handle_msg(epics::LoadEpics, ctx)?,
|
||||
WsMsg::EpicCreate(name, description, description_html) => self.handle_msg(
|
||||
epics::CreateEpic {
|
||||
name,
|
||||
description_html,
|
||||
description,
|
||||
},
|
||||
ctx,
|
||||
)?,
|
||||
WsMsg::EpicUpdate(epic_id, name) => {
|
||||
self.handle_msg(epics::UpdateEpic { epic_id, name }, ctx)?
|
||||
}
|
||||
WsMsg::EpicDelete(epic_id) => self.handle_msg(epics::DeleteEpic { epic_id }, ctx)?,
|
||||
WsMsg::EpicTransform(epic_id, issue_type) => self.handle_msg(
|
||||
epics::TransformEpic {
|
||||
epic_id,
|
||||
issue_type,
|
||||
},
|
||||
ctx,
|
||||
)?,
|
||||
|
||||
// hi
|
||||
WsMsg::HighlightCode(lang, code) => {
|
||||
self.handle_msg(hi::HighlightCode(lang, code), ctx)?
|
||||
}
|
||||
|
||||
// else fail
|
||||
_ => {
|
||||
error!("No handle for {:?} specified", msg);
|
||||
None
|
||||
}
|
||||
};
|
||||
if msg.is_some() && msg != Some(WsMsg::Pong) {
|
||||
info!("sending message {:?}", msg);
|
||||
}
|
||||
Ok(msg)
|
||||
}
|
||||
|
||||
async fn join_channel(&self, addr: Recipient<InnerMsg>) {
|
||||
info!("joining channel...");
|
||||
info!(" current user {:?}", self.current_user);
|
||||
|
||||
let user = match self.current_user.as_ref() {
|
||||
None => return,
|
||||
Some(u) => u,
|
||||
};
|
||||
let project_id = match self.require_user_project() {
|
||||
Ok(user_project) => user_project.project_id,
|
||||
_ => return,
|
||||
};
|
||||
match self
|
||||
.addr
|
||||
.send(InnerMsg::Join(project_id, user.id, addr))
|
||||
.await
|
||||
{
|
||||
Err(e) => error!("{}", e),
|
||||
_ => info!(" joined channel"),
|
||||
};
|
||||
}
|
||||
|
||||
fn require_user(&self) -> Result<&User, WsMsg> {
|
||||
self.current_user
|
||||
.as_ref()
|
||||
.map(|u| u)
|
||||
.ok_or_else(|| WsMsg::AuthorizeExpired)
|
||||
}
|
||||
|
||||
fn require_user_project(&self) -> Result<&UserProject, WsMsg> {
|
||||
self.current_user_project
|
||||
.as_ref()
|
||||
.map(|u| u)
|
||||
.ok_or_else(|| WsMsg::AuthorizeExpired)
|
||||
}
|
||||
|
||||
fn load_user_project(&self) -> Result<UserProject, WsMsg> {
|
||||
let user_id = self.require_user()?.id;
|
||||
match block_on(self.db.send(CurrentUserProject { user_id })) {
|
||||
Ok(Ok(user_project)) => Ok(user_project),
|
||||
Ok(Err(e)) => {
|
||||
error!("load_user_project encounter service error {:?}", e);
|
||||
Err(WsMsg::AuthorizeExpired)
|
||||
}
|
||||
Err(e) => {
|
||||
error!("load_user_project encounter mailbox error {}", e);
|
||||
Err(WsMsg::AuthorizeExpired)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn load_project(&self) -> Result<Project, WsMsg> {
|
||||
let project_id = self.require_user_project()?.project_id;
|
||||
match block_on(self.db.send(LoadCurrentProject { project_id })) {
|
||||
Ok(Ok(project)) => Ok(project),
|
||||
Ok(Err(e)) => {
|
||||
error!("{:?}", e);
|
||||
Err(WsMsg::AuthorizeExpired)
|
||||
}
|
||||
Err(e) => {
|
||||
error!("{}", e);
|
||||
Err(WsMsg::AuthorizeExpired)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl StreamHandler<Result<ws::Message, ws::ProtocolError>> for WebSocketActor {
|
||||
fn handle(
|
||||
&mut self,
|
||||
msg: Result<ws::Message, ws::ProtocolError>,
|
||||
ctx: &mut <Self as Actor>::Context,
|
||||
) {
|
||||
match msg {
|
||||
Ok(ws::Message::Ping(msg)) => ctx.pong(&msg),
|
||||
Ok(ws::Message::Text(text)) => ctx.text(text),
|
||||
|
||||
Ok(ws::Message::Binary(bin)) => {
|
||||
let ws_msg: bincode::Result<jirs_data::WsMsg> =
|
||||
bincode::deserialize(bin.to_vec().as_slice());
|
||||
let msg = match ws_msg {
|
||||
Ok(m) => m,
|
||||
_ => return,
|
||||
};
|
||||
match self.handle_ws_msg(msg, ctx) {
|
||||
Ok(Some(msg)) => ctx.send_msg(&msg),
|
||||
Err(e) => ctx.send_msg(&e),
|
||||
_ => (),
|
||||
};
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
fn finished(&mut self, ctx: &mut <Self as Actor>::Context) {
|
||||
info!("Disconnected");
|
||||
if let (Some(user), Some(up)) = (
|
||||
self.current_user.as_ref(),
|
||||
self.current_user_project.as_ref(),
|
||||
) {
|
||||
self.addr.do_send(InnerMsg::Leave(
|
||||
up.project_id,
|
||||
user.id,
|
||||
ctx.address().recipient(),
|
||||
));
|
||||
}
|
||||
ctx.stop()
|
||||
}
|
||||
}
|
||||
|
||||
pub trait WsHandler<Message>
|
||||
where
|
||||
Self: Actor,
|
||||
{
|
||||
fn handle_msg(&mut self, msg: Message, _ctx: &mut <Self as Actor>::Context) -> WsResult;
|
||||
}
|
||||
|
||||
#[get("/ws/")]
|
||||
pub async fn index(
|
||||
req: HttpRequest,
|
||||
stream: web::Payload,
|
||||
db: Data<Addr<DbExecutor>>,
|
||||
mail: Data<Addr<MailExecutor>>,
|
||||
ws_server: Data<Addr<WsServer>>,
|
||||
hi: Data<Addr<highlight_actor::HighlightActor>>,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
ws::start(
|
||||
WebSocketActor {
|
||||
db,
|
||||
mail,
|
||||
hi,
|
||||
current_user: None,
|
||||
current_user_project: None,
|
||||
current_project: None,
|
||||
addr: ws_server.get_ref().clone(),
|
||||
},
|
||||
&req,
|
||||
stream,
|
||||
)
|
||||
}
|
73
actors/websocket-actor/src/prelude.rs
Normal file
73
actors/websocket-actor/src/prelude.rs
Normal file
@ -0,0 +1,73 @@
|
||||
#[macro_export]
|
||||
macro_rules! db_or_debug_and_return {
|
||||
($s: ident, $msg: expr, $actor_err: expr, $mailbox_err: expr) => {
|
||||
$crate::actor_or_debug_and_return!($s, db, $msg, $actor_err, $mailbox_err)
|
||||
};
|
||||
($s: ident, $msg: expr) => {
|
||||
$crate::actor_or_debug_and_return!($s, db, $msg)
|
||||
};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! mail_or_debug_and_return {
|
||||
($s: ident, $msg: expr, $actor_err: expr, $mailbox_err: expr) => {
|
||||
$crate::actor_or_debug_and_return!($s, mail, $msg, $actor_err, $mailbox_err)
|
||||
};
|
||||
($s: ident, $msg: expr) => {
|
||||
$crate::actor_or_debug_and_return!($s, mail, $msg)
|
||||
};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! actor_or_debug_and_return {
|
||||
($s: ident, $actor: ident, $msg: expr, $actor_err: expr, $mailbox_err: expr) => {
|
||||
match block_on($s.$actor.send($msg)) {
|
||||
Ok(Ok(r)) => r,
|
||||
Ok(Err(e)) => {
|
||||
log::error!("{:?}", e);
|
||||
return $actor_err;
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("{:?}", e);
|
||||
return $mailbox_err;
|
||||
}
|
||||
}
|
||||
};
|
||||
($s: ident, $actor: ident, $msg: expr) => {
|
||||
crate::actor_or_debug_and_return!($s, $actor, $msg, Ok(None), Ok(None))
|
||||
};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! actor_or_debug_and_ignore {
|
||||
($s: ident, $actor: ident, $msg: expr, $on_success: expr) => {
|
||||
match block_on($s.$actor.send($msg)) {
|
||||
Ok(Ok(r)) => {
|
||||
$on_success(r);
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
log::error!("{:?}", e);
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("{:?}", e);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! actor_or_debug_and_fallback {
|
||||
($s: ident, $actor: ident, $msg: expr, $actor_err: expr, $mailbox_err: expr) => {
|
||||
match block_on($s.$actor.send($msg)) {
|
||||
Ok(Ok(r)) => r,
|
||||
Ok(Err(e)) => {
|
||||
log::error!("{:?}", e);
|
||||
$actor_err
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("{:?}", e);
|
||||
$mailbox_err
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
@ -1,10 +1,10 @@
|
||||
use std::collections::HashMap;
|
||||
use {
|
||||
actix::{Actor, Context, Recipient},
|
||||
jirs_data::{ProjectId, UserId, WsMsg},
|
||||
std::collections::HashMap,
|
||||
};
|
||||
|
||||
use ::tracing::*;
|
||||
use actix::{Actor, Context, Recipient};
|
||||
use bitque_data::{ProjectId, UserId, WsMsg};
|
||||
|
||||
#[derive(Debug, actix::Message)]
|
||||
#[derive(actix::Message, Debug)]
|
||||
#[rtype(result = "()")]
|
||||
pub enum InnerMsg {
|
||||
Join(ProjectId, UserId, Recipient<InnerMsg>),
|
||||
@ -66,14 +66,14 @@ impl actix::Handler<InnerMsg> for WsServer {
|
||||
room.remove(&user_id);
|
||||
self.sessions.remove(&user_id);
|
||||
} else {
|
||||
let v = self
|
||||
.sessions
|
||||
.remove(&user_id)
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.filter(|r| r != &recipient)
|
||||
.collect::<Vec<Recipient<InnerMsg>>>();
|
||||
self.sessions.insert(user_id, v);
|
||||
let v = self.sessions.entry(user_id).or_insert_with(Vec::new);
|
||||
let mut old = vec![];
|
||||
std::mem::swap(&mut old, v);
|
||||
for r in old {
|
||||
if r != recipient {
|
||||
v.push(r);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
InnerMsg::SendToUser(user_id, msg) => {
|
||||
@ -110,7 +110,10 @@ impl WsServer {
|
||||
|
||||
fn send_to_recipients(&self, recipients: &[Recipient<InnerMsg>], msg: &WsMsg) {
|
||||
for recipient in recipients.iter() {
|
||||
recipient.do_send(InnerMsg::Transfer(msg.clone()));
|
||||
match recipient.do_send(InnerMsg::Transfer(msg.clone())) {
|
||||
Ok(_) => debug!("msg sent"),
|
||||
Err(e) => error!("{}", e),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
[package]
|
||||
name = "bitquec"
|
||||
version = "0.1.0"
|
||||
authors = ["Adrian Wozniak <adrian.wozniak@ita-prog.pl>"]
|
||||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
actix = { version = "0.13.0" }
|
||||
clap = { version = "4.1.13" }
|
||||
termion = { version = "*" }
|
||||
tui = { version = "0.19.0", features = ["termion"] }
|
@ -1,29 +0,0 @@
|
||||
[package]
|
||||
name = "bitque-config"
|
||||
version = "0.1.0"
|
||||
authors = ["Adrian Wozniak <adrian.wozniak@ita-prog.pl>"]
|
||||
edition = "2018"
|
||||
description = "BITQUE (Simplified JIRA in Rust) shared data types"
|
||||
repository = "https://gitlab.com/adrian.wozniak/bitque"
|
||||
license = "MPL-2.0"
|
||||
#license-file = "../LICENSE"
|
||||
|
||||
[lib]
|
||||
name = "bitque_config"
|
||||
path = "./src/lib.rs"
|
||||
|
||||
[features]
|
||||
cloud-storage = ["rust-s3"]
|
||||
local-storage = []
|
||||
database = []
|
||||
hi = []
|
||||
mail = []
|
||||
web = ["cloud-storage", "local-storage"]
|
||||
websocket = []
|
||||
default = ["local-storage", "database", "hi", "mail", "web", "websocket"]
|
||||
|
||||
[dependencies]
|
||||
serde = { version = "*" }
|
||||
toml = { version = "*" }
|
||||
rust-s3 = { version = "*", optional = true }
|
||||
tracing = { version = "*" }
|
@ -1,30 +0,0 @@
|
||||
[package]
|
||||
name = "bitque-data"
|
||||
version = "0.1.0"
|
||||
authors = ["Adrian Wozniak <adrian.wozniak@ita-prog.pl>"]
|
||||
edition = "2018"
|
||||
description = "BITQUE (Simplified JIRA in Rust) shared data types"
|
||||
repository = "https://gitlab.com/adrian.wozniak/bitque"
|
||||
license = "MPL-2.0"
|
||||
#license-file = "../LICENSE"
|
||||
|
||||
[lib]
|
||||
name = "bitque_data"
|
||||
path = "./src/lib.rs"
|
||||
|
||||
[features]
|
||||
backend = ["diesel", "actix", "diesel-derive-newtype"]
|
||||
frontend = ['derive_enum_primitive']
|
||||
|
||||
[dependencies]
|
||||
actix = { version = "0.13.0", optional = true }
|
||||
chrono = { version = "*", features = ["serde"] }
|
||||
diesel = { version = "2.0.3", features = ["postgres", "numeric", "uuid", "r2d2"], optional = true }
|
||||
diesel-derive-enum = { version = "2.0.1", features = ["postgres"] }
|
||||
derive_enum_primitive = { workspace = true, optional = true }
|
||||
diesel-derive-more = { version = "1.1.3" }
|
||||
diesel-derive-newtype = { version = "2.0.0-rc.0", optional = true }
|
||||
serde = { version = "*" }
|
||||
serde_json = { version = "*" }
|
||||
strum = { version = "0.24.1", features = ['derive', 'strum_macros', 'std'] }
|
||||
uuid = { version = "1.3.0", features = ["serde"] }
|
@ -1,38 +0,0 @@
|
||||
[package]
|
||||
name = "bitque"
|
||||
version = "0.1.0"
|
||||
authors = ["Adrian Wozniak <adrian.wozniak@ita-prog.pl>"]
|
||||
edition = "2018"
|
||||
description = "BITQUE (Simplified JIRA in Rust) Actix server"
|
||||
repository = "https://gitlab.com/adrian.wozniak/bitque"
|
||||
license = "MPL-2.0"
|
||||
#license-file = "../LICENSE"
|
||||
|
||||
[features]
|
||||
cloud-storage = ["cloud-storage-actor"]
|
||||
local-storage = ["filesystem-actor"]
|
||||
default = ["local-storage"]
|
||||
|
||||
[dependencies]
|
||||
actix = { version = "0" }
|
||||
actix-rt = { version = "2" }
|
||||
actix-web = { version = "4" }
|
||||
cloud-storage-actor = { workspace = true, optional = true }
|
||||
bitque-config = { workspace = true, features = ["web", "websocket", "local-storage", "hi", "database"] }
|
||||
bitque-data = { workspace = true, features = ["backend"] }
|
||||
database-actor = { workspace = true }
|
||||
dotenv = { version = "*" }
|
||||
filesystem-actor = { workspace = true, optional = true }
|
||||
futures = { version = "*" }
|
||||
highlight-actor = { workspace = true }
|
||||
libc = { version = "0.2.0", default-features = false }
|
||||
mail-actor = { workspace = true }
|
||||
openssl-sys = { version = "*", features = ["vendored"] }
|
||||
serde = { version = "*", features = ["derive"] }
|
||||
serde_json = { version = ">=0.8.0, <2.0" }
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
toml = { version = "0.7.3" }
|
||||
tracing = { version = "0" }
|
||||
tracing-subscriber = { version = "0", features = ['env-filter', 'thread_local', 'serde_json'] }
|
||||
web-actor = { workspace = true, features = ["local-storage"] }
|
||||
websocket-actor = { workspace = true }
|
@ -1,84 +0,0 @@
|
||||
#![feature(async_closure)]
|
||||
#![recursion_limit = "256"]
|
||||
|
||||
use actix::Actor;
|
||||
use actix_web::web::Data;
|
||||
use actix_web::{App, HttpServer};
|
||||
use tracing::info;
|
||||
use tracing_subscriber::util::SubscriberInitExt;
|
||||
|
||||
pub mod errors;
|
||||
|
||||
macro_rules! featured {
|
||||
($app: ident, $feature: expr, $connect: expr) => {
|
||||
#[cfg(feature = $feature)]
|
||||
let $app = $connect;
|
||||
};
|
||||
}
|
||||
|
||||
#[actix_rt::main]
|
||||
async fn main() -> Result<(), String> {
|
||||
dotenv::dotenv().ok();
|
||||
tracing_subscriber::fmt::fmt()
|
||||
.with_env_filter(tracing_subscriber::filter::EnvFilter::from_default_env())
|
||||
.finish()
|
||||
.init();
|
||||
|
||||
let web_config = bitque_config::web::Configuration::read();
|
||||
|
||||
let db_addr = actix::SyncArbiter::start(
|
||||
bitque_config::database::Configuration::read().concurrency,
|
||||
database_actor::DbExecutor::default,
|
||||
);
|
||||
let mail_addr = actix::SyncArbiter::start(
|
||||
bitque_config::mail::Configuration::read().concurrency,
|
||||
mail_actor::MailExecutor::default,
|
||||
);
|
||||
let hi_addr = actix::SyncArbiter::start(
|
||||
bitque_config::hi::Configuration::read().concurrency,
|
||||
highlight_actor::HighlightActor::default,
|
||||
);
|
||||
#[cfg(feature = "local-storage")]
|
||||
let fs_addr = actix::SyncArbiter::start(
|
||||
bitque_config::fs::Configuration::read().concurrency,
|
||||
filesystem_actor::LocalStorageExecutor::default,
|
||||
);
|
||||
#[cfg(feature = "cloud-storage")]
|
||||
let cloud_storage_addr = actix::SyncArbiter::start(
|
||||
bitque_config::web::Configuration::read().concurrency,
|
||||
cloud_storage_actor::CloudStorageExecutor::default,
|
||||
);
|
||||
|
||||
let ws_server = websocket_actor::server::WsServer::start_default();
|
||||
|
||||
info!("Listen at {}", web_config.bind_addr());
|
||||
|
||||
HttpServer::new(move || {
|
||||
let app = App::new().wrap(actix_web::middleware::Logger::default());
|
||||
|
||||
// data step
|
||||
let app = app
|
||||
.app_data(Data::new(ws_server.clone()))
|
||||
.app_data(Data::new(db_addr.clone()))
|
||||
.app_data(Data::new(mail_addr.clone()))
|
||||
.app_data(Data::new(hi_addr.clone()))
|
||||
.app_data(Data::new(database_actor::build_pool()));
|
||||
featured! { app, "local-storage", app.app_data(Data::new(fs_addr.clone())) };
|
||||
featured! { app, "cloud-storage", app.app_data(Data::new(cloud_storage_addr.clone())) };
|
||||
|
||||
// services step
|
||||
let app = app
|
||||
.service(websocket_actor::index)
|
||||
.service(actix_web::web::scope("/avatar").service(web_actor::avatar::upload));
|
||||
|
||||
featured! { app, "local-storage", app.service(filesystem_actor::service()) };
|
||||
app
|
||||
})
|
||||
.workers(web_config.concurrency)
|
||||
.bind(web_config.bind_addr())
|
||||
.map_err(|e| format!("{}", e))?
|
||||
.run()
|
||||
.await
|
||||
.expect("Server internal error");
|
||||
Ok(())
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
[package]
|
||||
name = "cloud-storage-actor"
|
||||
version = "0.1.0"
|
||||
authors = ["Adrian Wozniak <adrian.wozniak@ita-prog.pl>"]
|
||||
edition = "2018"
|
||||
description = "BITQUE (Simplified JIRA in Rust) shared data types"
|
||||
repository = "https://gitlab.com/adrian.wozniak/bitque"
|
||||
license = "MPL-2.0"
|
||||
#license-file = "../LICENSE"
|
||||
|
||||
[dependencies]
|
||||
actix = { version = "0.13.0" }
|
||||
bitque-config = { workspace = true, features = ["mail", "web", "local-storage"] }
|
||||
bytes = { version = "1" }
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
tracing = { version = "0.1.37" }
|
||||
rust-s3 = { version = "*" }
|
||||
aws-creds = { version = "=0.30.0", features = ['attohttpc'] }
|
||||
thiserror = { version = "*" }
|
@ -1,96 +0,0 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use awscreds::Credentials;
|
||||
use s3::Region;
|
||||
use tracing::warn;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum AmazonError {
|
||||
#[error("File upload to external storage failed")]
|
||||
UploadFailed,
|
||||
#[error("Failed to connect to bucket")]
|
||||
ConnectBucket,
|
||||
#[error("Malformed external storage credentials")]
|
||||
Credentials,
|
||||
}
|
||||
|
||||
pub struct CloudStorageExecutor;
|
||||
|
||||
impl Default for CloudStorageExecutor {
|
||||
fn default() -> Self {
|
||||
Self {}
|
||||
}
|
||||
}
|
||||
|
||||
impl actix::Actor for CloudStorageExecutor {
|
||||
type Context = actix::SyncContext<Self>;
|
||||
}
|
||||
|
||||
#[derive(actix::Message)]
|
||||
#[rtype(result = "Result<String, AmazonError>")]
|
||||
pub struct PutObject {
|
||||
pub source: tokio::sync::broadcast::Receiver<bytes::Bytes>,
|
||||
pub file_name: String,
|
||||
pub dir: String,
|
||||
}
|
||||
|
||||
impl actix::Handler<PutObject> for CloudStorageExecutor {
|
||||
type Result = Result<String, AmazonError>;
|
||||
|
||||
fn handle(&mut self, msg: PutObject, _ctx: &mut Self::Context) -> Self::Result {
|
||||
let PutObject {
|
||||
// source,
|
||||
mut source,
|
||||
file_name,
|
||||
dir,
|
||||
} = msg;
|
||||
bitque_config::cloud_storage::config().set_variables();
|
||||
|
||||
tokio::runtime::Runtime::new()
|
||||
.expect("Failed to start amazon agent")
|
||||
.block_on(async {
|
||||
let s3 = bitque_config::cloud_storage::config();
|
||||
tracing::debug!("{:?}", s3);
|
||||
|
||||
let mut v: Vec<u8> = Vec::with_capacity(1024 * 1024 * 16);
|
||||
while let Ok(b) = source.recv().await {
|
||||
v.extend_from_slice(&b)
|
||||
}
|
||||
|
||||
let config = bitque_config::cloud_storage::config();
|
||||
let bucket = s3::Bucket::new(
|
||||
config.bucket.as_str(),
|
||||
Region::from_str(config.region_name.as_str()).unwrap(),
|
||||
Credentials::new(
|
||||
Some(config.access_key_id.as_str()),
|
||||
Some(config.secret_access_key.as_str()),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.map_err(|e| {
|
||||
warn!("{e}");
|
||||
AmazonError::Credentials
|
||||
})?,
|
||||
)
|
||||
.map_err(|e| {
|
||||
warn!("{e}");
|
||||
AmazonError::ConnectBucket
|
||||
})?
|
||||
.with_path_style();
|
||||
let put = bucket
|
||||
.put_object(format!("{dir}/{file_name}").as_str(), &v)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
warn!("{e}");
|
||||
AmazonError::UploadFailed
|
||||
})?;
|
||||
if put.status_code() >= 300 {
|
||||
// Error
|
||||
Err(AmazonError::UploadFailed)
|
||||
} else {
|
||||
Ok(format!("{}/{}", bucket.url(), file_name))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -1,42 +0,0 @@
|
||||
[package]
|
||||
name = "database-actor"
|
||||
version = "0.1.0"
|
||||
authors = ["Adrian Wozniak <adrian.wozniak@ita-prog.pl>"]
|
||||
edition = "2018"
|
||||
description = "BITQUE (Simplified JIRA in Rust) shared data types"
|
||||
repository = "https://gitlab.com/adrian.wozniak/bitque"
|
||||
license = "MPL-2.0"
|
||||
#license-file = "../LICENSE"
|
||||
|
||||
[lib]
|
||||
name = "database_actor"
|
||||
path = "./src/lib.rs"
|
||||
|
||||
[dependencies]
|
||||
actix = { version = "0.13.0" }
|
||||
bigdecimal = { version = "0.3.0" }
|
||||
bincode = { version = "*" }
|
||||
bitflags = { version = "2.0.2" }
|
||||
bitque-config = { workspace = true, features = ["database"] }
|
||||
bitque-data = { workspace = true, features = ["backend"] }
|
||||
byteorder = { version = "1.0" }
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
derive_db_execute = { workspace = true }
|
||||
diesel = { version = "2.0.3", features = ["postgres", "numeric", "uuid", "r2d2", "chrono"] }
|
||||
dotenv = { version = "*" }
|
||||
futures = { version = "0.3.8" }
|
||||
ipnetwork = { version = "0.20.0" }
|
||||
libc = { version = "0.2.0", default-features = false }
|
||||
num-bigint = { version = "0.4.3" }
|
||||
num-integer = { version = "0.1.32" }
|
||||
num-traits = { version = "0.2" }
|
||||
openssl-sys = { version = "*", features = ["vendored"] }
|
||||
percent-encoding = { version = "2.1.0" }
|
||||
pq-sys = { version = ">=0.3.0, <0.5.0" }
|
||||
r2d2 = { version = ">= 0.8, < 0.9" }
|
||||
serde = { version = "*" }
|
||||
time = { version = "0.3.20" }
|
||||
toml = { version = "*" }
|
||||
tracing = { version = "0.1.37" }
|
||||
url = { version = "2.1.0" }
|
||||
uuid = { version = "1.3.0", features = ["serde", "v4", "v5"] }
|
@ -1,141 +0,0 @@
|
||||
diff --git a/crates/database-actor/src/schema.rs b/crates/database-actor/src/schema.rs
|
||||
index 34a35365..a8f775b2 100644
|
||||
--- a/crates/database-actor/src/schema.rs
|
||||
+++ b/crates/database-actor/src/schema.rs
|
||||
@@ -37,19 +37,19 @@ diesel::table! {
|
||||
use bitque_data::*;
|
||||
|
||||
invitations (id) {
|
||||
id -> Int4,
|
||||
name -> Text,
|
||||
email -> Text,
|
||||
- state -> InvitationState,
|
||||
+ state -> InvitationStateMapping,
|
||||
project_id -> Int4,
|
||||
invited_by_id -> Int4,
|
||||
created_at -> Timestamp,
|
||||
updated_at -> Timestamp,
|
||||
bind_token -> Uuid,
|
||||
- role -> UserRole,
|
||||
+ role -> UserRoleMapping,
|
||||
}
|
||||
}
|
||||
|
||||
diesel::table! {
|
||||
use diesel::sql_types::*;
|
||||
use bitque_data::*;
|
||||
@@ -81,14 +81,14 @@ diesel::table! {
|
||||
use diesel::sql_types::*;
|
||||
use bitque_data::*;
|
||||
|
||||
issues (id) {
|
||||
id -> Int4,
|
||||
title -> Text,
|
||||
- issue_type -> IssueType,
|
||||
- priority -> IssuePriority,
|
||||
+ issue_type -> IssueTypeMapping,
|
||||
+ priority -> IssuePriorityMapping,
|
||||
list_position -> Int4,
|
||||
description -> Nullable<Text>,
|
||||
description_text -> Nullable<Text>,
|
||||
estimate -> Nullable<Int4>,
|
||||
time_spent -> Nullable<Int4>,
|
||||
time_remaining -> Nullable<Int4>,
|
||||
@@ -108,13 +108,13 @@ diesel::table! {
|
||||
messages (id) {
|
||||
id -> Int4,
|
||||
receiver_id -> Int4,
|
||||
sender_id -> Int4,
|
||||
summary -> Text,
|
||||
description -> Text,
|
||||
- message_type -> MessageType,
|
||||
+ message_type -> MessageTypeMapping,
|
||||
hyper_link -> Text,
|
||||
created_at -> Timestamp,
|
||||
updated_at -> Timestamp,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,16 +124,16 @@ diesel::table! {
|
||||
|
||||
projects (id) {
|
||||
id -> Int4,
|
||||
name -> Text,
|
||||
url -> Text,
|
||||
description -> Text,
|
||||
- category -> ProjectCategory,
|
||||
+ category -> ProjectCategoryMapping,
|
||||
created_at -> Timestamp,
|
||||
updated_at -> Timestamp,
|
||||
- time_tracking -> TimeTracking,
|
||||
+ time_tracking -> TimeTrackingMapping,
|
||||
}
|
||||
}
|
||||
|
||||
diesel::table! {
|
||||
use diesel::sql_types::*;
|
||||
use bitque_data::*;
|
||||
@@ -156,26 +156,26 @@ diesel::table! {
|
||||
user_projects (id) {
|
||||
id -> Int4,
|
||||
user_id -> Int4,
|
||||
project_id -> Int4,
|
||||
is_default -> Bool,
|
||||
is_current -> Bool,
|
||||
- role -> UserRole,
|
||||
+ role -> UserRoleMapping,
|
||||
created_at -> Timestamp,
|
||||
updated_at -> Timestamp,
|
||||
}
|
||||
}
|
||||
|
||||
diesel::table! {
|
||||
use diesel::sql_types::*;
|
||||
use bitque_data::*;
|
||||
|
||||
user_settings (id) {
|
||||
id -> Int4,
|
||||
user_id -> Int4,
|
||||
- text_editor_mode -> TextEditorMode,
|
||||
+ text_editor_mode -> TextEditorModeMapping,
|
||||
}
|
||||
}
|
||||
|
||||
diesel::table! {
|
||||
use diesel::sql_types::*;
|
||||
use bitque_data::*;
|
||||
@@ -205,20 +205,20 @@ diesel::joinable!(issues -> projects (project_id));
|
||||
diesel::joinable!(issues -> users (reporter_id));
|
||||
diesel::joinable!(tokens -> users (user_id));
|
||||
diesel::joinable!(user_projects -> projects (project_id));
|
||||
diesel::joinable!(user_projects -> users (user_id));
|
||||
diesel::joinable!(user_settings -> users (user_id));
|
||||
|
||||
-diesel::allow_tables_to_appear_in_same_query!(
|
||||
- comments,
|
||||
- epics,
|
||||
- invitations,
|
||||
- issue_assignees,
|
||||
- issue_statuses,
|
||||
- issues,
|
||||
- messages,
|
||||
- projects,
|
||||
- tokens,
|
||||
- user_projects,
|
||||
- user_settings,
|
||||
- users,
|
||||
-);
|
||||
+// diesel::allow_tables_to_appear_in_same_query!(
|
||||
+// comments,
|
||||
+// epics,
|
||||
+// invitations,
|
||||
+// issue_assignees,
|
||||
+// issue_statuses,
|
||||
+// issues,
|
||||
+// messages,
|
||||
+// projects,
|
||||
+// tokens,
|
||||
+// user_projects,
|
||||
+// user_settings,
|
||||
+// users,
|
||||
+// );
|
@ -1,182 +0,0 @@
|
||||
use actix::{Handler, Message};
|
||||
use bitque_data::{
|
||||
EmailString, Invitation, InvitationId, InvitationState, InvitationToken, ProjectId, Token,
|
||||
User, UserId, UserRole, UsernameString,
|
||||
};
|
||||
use diesel::prelude::*;
|
||||
|
||||
use crate::tokens::CreateBindToken;
|
||||
use crate::users::{LookupUser, Register};
|
||||
use crate::{
|
||||
db_create, db_delete, db_find, db_load, db_pool, db_update, DatabaseError, DbExecutor,
|
||||
DbPooledConn, InvitationError,
|
||||
};
|
||||
|
||||
db_find! {
|
||||
FindByBindToken,
|
||||
msg => invitations => invitations.filter(bind_token.eq(msg.token)),
|
||||
Invitation,
|
||||
token => InvitationToken
|
||||
}
|
||||
|
||||
db_load! {
|
||||
ListInvitation,
|
||||
msg => invitations => invitations
|
||||
.filter(invited_by_id.eq(msg.user_id))
|
||||
.filter(state.ne(InvitationState::Accepted))
|
||||
.order_by(state.asc())
|
||||
.then_order_by(updated_at.desc()),
|
||||
Invitation,
|
||||
user_id => UserId
|
||||
}
|
||||
|
||||
db_create! {
|
||||
CreateInvitation,
|
||||
msg => invitations => diesel::insert_into(invitations).values((
|
||||
name.eq(msg.name),
|
||||
email.eq(msg.email),
|
||||
state.eq(InvitationState::Sent),
|
||||
project_id.eq(msg.project_id),
|
||||
invited_by_id.eq(msg.user_id),
|
||||
role.eq(msg.role),
|
||||
)),
|
||||
Invitation,
|
||||
user_id => UserId,
|
||||
project_id => ProjectId,
|
||||
email => EmailString,
|
||||
name => UsernameString,
|
||||
role => UserRole
|
||||
}
|
||||
|
||||
db_delete! {
|
||||
DeleteInvitation,
|
||||
msg => invitations => diesel::delete(invitations).filter(id.eq(msg.id)),
|
||||
Invitation,
|
||||
id => InvitationId
|
||||
}
|
||||
|
||||
db_update! {
|
||||
UpdateInvitationState,
|
||||
msg => invitations => diesel::update(invitations)
|
||||
.set((
|
||||
state.eq(msg.state),
|
||||
updated_at.eq(chrono::Utc::now().naive_utc()),
|
||||
))
|
||||
.filter(id.eq(msg.id)),
|
||||
Invitation,
|
||||
id => InvitationId,
|
||||
state => InvitationState
|
||||
}
|
||||
|
||||
pub struct RevokeInvitation {
|
||||
pub id: InvitationId,
|
||||
}
|
||||
|
||||
impl Message for RevokeInvitation {
|
||||
type Result = Result<(), crate::DatabaseError>;
|
||||
}
|
||||
|
||||
impl Handler<RevokeInvitation> for DbExecutor {
|
||||
type Result = Result<(), crate::DatabaseError>;
|
||||
|
||||
fn handle(&mut self, msg: RevokeInvitation, _ctx: &mut Self::Context) -> Self::Result {
|
||||
let mut conn = db_pool!(self);
|
||||
UpdateInvitationState {
|
||||
id: msg.id,
|
||||
state: InvitationState::Revoked,
|
||||
}
|
||||
.execute(&mut conn)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct AcceptInvitation {
|
||||
pub invitation_token: InvitationToken,
|
||||
}
|
||||
|
||||
impl AcceptInvitation {
|
||||
pub fn execute(self, conn: &mut DbPooledConn) -> Result<Token, crate::DatabaseError> {
|
||||
let mut res = Err(DatabaseError::DatabaseConnectionLost);
|
||||
conn.transaction(|conn| {
|
||||
res = self.exec_in_transaction(conn);
|
||||
if res.is_err() {
|
||||
Err(diesel::NotFound)
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
res
|
||||
}
|
||||
|
||||
fn exec_in_transaction(self, conn: &mut DbPooledConn) -> Result<Token, crate::DatabaseError> {
|
||||
let invitation = FindByBindToken {
|
||||
token: self.invitation_token,
|
||||
}
|
||||
.execute(conn)?;
|
||||
|
||||
if invitation.state == InvitationState::Revoked {
|
||||
return Err(crate::DatabaseError::Invitation(
|
||||
InvitationError::InvitationRevoked,
|
||||
));
|
||||
}
|
||||
|
||||
UpdateInvitationState {
|
||||
id: invitation.id,
|
||||
state: InvitationState::Accepted,
|
||||
}
|
||||
.execute(conn)?;
|
||||
|
||||
UpdateInvitationState {
|
||||
id: invitation.id,
|
||||
state: InvitationState::Accepted,
|
||||
}
|
||||
.execute(conn)?;
|
||||
|
||||
match {
|
||||
Register {
|
||||
name: invitation.name.clone(),
|
||||
email: invitation.email.clone(),
|
||||
project_id: Some(invitation.project_id),
|
||||
role: UserRole::User,
|
||||
}
|
||||
.execute(conn)
|
||||
} {
|
||||
Ok(_) => (),
|
||||
Err(crate::DatabaseError::User(crate::UserError::InvalidPair(..))) => (),
|
||||
Err(e) => return Err(e),
|
||||
};
|
||||
|
||||
let user: User = LookupUser {
|
||||
name: invitation.name.clone(),
|
||||
email: invitation.email.clone(),
|
||||
}
|
||||
.execute(conn)?;
|
||||
CreateBindToken { user_id: user.id }.execute(conn)?;
|
||||
|
||||
crate::user_projects::CreateUserProject {
|
||||
user_id: user.id,
|
||||
project_id: invitation.project_id,
|
||||
is_current: false,
|
||||
is_default: false,
|
||||
role: invitation.role,
|
||||
}
|
||||
.execute(conn)?;
|
||||
|
||||
crate::tokens::FindUserId { user_id: user.id }.execute(conn)
|
||||
}
|
||||
}
|
||||
|
||||
impl Message for AcceptInvitation {
|
||||
type Result = Result<Token, crate::DatabaseError>;
|
||||
}
|
||||
|
||||
impl Handler<AcceptInvitation> for DbExecutor {
|
||||
type Result = Result<Token, crate::DatabaseError>;
|
||||
|
||||
fn handle(&mut self, msg: AcceptInvitation, _ctx: &mut Self::Context) -> Self::Result {
|
||||
let mut conn = db_pool!(self);
|
||||
|
||||
msg.execute(&mut conn)
|
||||
}
|
||||
}
|
@ -1,65 +0,0 @@
|
||||
#![recursion_limit = "256"]
|
||||
|
||||
#[macro_use]
|
||||
extern crate diesel;
|
||||
|
||||
use actix::{Actor, SyncContext};
|
||||
use diesel::pg::PgConnection;
|
||||
use diesel::r2d2::{self, ConnectionManager};
|
||||
pub use errors::*;
|
||||
|
||||
pub mod authorize_user;
|
||||
pub mod comments;
|
||||
pub mod epics;
|
||||
pub mod errors;
|
||||
pub mod invitations;
|
||||
pub mod issue_assignees;
|
||||
pub mod issue_statuses;
|
||||
pub mod issues;
|
||||
pub mod messages;
|
||||
pub mod models;
|
||||
pub mod prelude;
|
||||
pub mod projects;
|
||||
pub mod schema;
|
||||
pub mod tokens;
|
||||
pub mod user_projects;
|
||||
pub mod user_settings;
|
||||
pub mod users;
|
||||
|
||||
pub type DbPool = r2d2::Pool<ConnectionManager<PgConnection>>;
|
||||
pub type DbPooledConn = r2d2::PooledConnection<ConnectionManager<PgConnection>>;
|
||||
|
||||
pub struct DbExecutor {
|
||||
pub pool: DbPool,
|
||||
pub config: bitque_config::database::Configuration,
|
||||
}
|
||||
|
||||
impl Actor for DbExecutor {
|
||||
type Context = SyncContext<Self>;
|
||||
}
|
||||
|
||||
impl Default for DbExecutor {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
pool: build_pool(),
|
||||
config: bitque_config::database::Configuration::read(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn build_pool() -> DbPool {
|
||||
dotenv::dotenv().ok();
|
||||
let config = bitque_config::database::Configuration::read();
|
||||
|
||||
let manager = ConnectionManager::<PgConnection>::new(&config.database_url);
|
||||
r2d2::Pool::builder()
|
||||
.max_size(config.concurrency as u32)
|
||||
.build(manager)
|
||||
.unwrap_or_else(|e| panic!("Failed to create pool. {}", e))
|
||||
}
|
||||
|
||||
pub trait SyncQuery {
|
||||
type Result;
|
||||
|
||||
fn handle(&self, pool: &DbPool) -> Self::Result;
|
||||
}
|
@ -1,224 +0,0 @@
|
||||
// @generated automatically by Diesel CLI.
|
||||
|
||||
diesel::table! {
|
||||
use diesel::sql_types::*;
|
||||
use bitque_data::*;
|
||||
|
||||
comments (id) {
|
||||
id -> Int4,
|
||||
body -> Text,
|
||||
user_id -> Int4,
|
||||
issue_id -> Int4,
|
||||
created_at -> Timestamp,
|
||||
updated_at -> Timestamp,
|
||||
}
|
||||
}
|
||||
|
||||
diesel::table! {
|
||||
use diesel::sql_types::*;
|
||||
use bitque_data::*;
|
||||
|
||||
epics (id) {
|
||||
id -> Int4,
|
||||
name -> Text,
|
||||
user_id -> Int4,
|
||||
project_id -> Int4,
|
||||
created_at -> Timestamp,
|
||||
updated_at -> Timestamp,
|
||||
starts_at -> Nullable<Timestamp>,
|
||||
ends_at -> Nullable<Timestamp>,
|
||||
description -> Nullable<Text>,
|
||||
description_html -> Nullable<Text>,
|
||||
}
|
||||
}
|
||||
|
||||
diesel::table! {
|
||||
use diesel::sql_types::*;
|
||||
use bitque_data::*;
|
||||
|
||||
invitations (id) {
|
||||
id -> Int4,
|
||||
name -> Text,
|
||||
email -> Text,
|
||||
state -> InvitationStateMapping,
|
||||
project_id -> Int4,
|
||||
invited_by_id -> Int4,
|
||||
created_at -> Timestamp,
|
||||
updated_at -> Timestamp,
|
||||
bind_token -> Uuid,
|
||||
role -> UserRoleMapping,
|
||||
}
|
||||
}
|
||||
|
||||
diesel::table! {
|
||||
use diesel::sql_types::*;
|
||||
use bitque_data::*;
|
||||
|
||||
issue_assignees (id) {
|
||||
id -> Int4,
|
||||
issue_id -> Int4,
|
||||
user_id -> Int4,
|
||||
created_at -> Timestamp,
|
||||
updated_at -> Timestamp,
|
||||
}
|
||||
}
|
||||
|
||||
diesel::table! {
|
||||
use diesel::sql_types::*;
|
||||
use bitque_data::*;
|
||||
|
||||
issue_statuses (id) {
|
||||
id -> Int4,
|
||||
name -> Varchar,
|
||||
position -> Int4,
|
||||
project_id -> Int4,
|
||||
created_at -> Timestamp,
|
||||
updated_at -> Timestamp,
|
||||
}
|
||||
}
|
||||
|
||||
diesel::table! {
|
||||
use diesel::sql_types::*;
|
||||
use bitque_data::*;
|
||||
|
||||
issues (id) {
|
||||
id -> Int4,
|
||||
title -> Text,
|
||||
issue_type -> IssueTypeMapping,
|
||||
priority -> IssuePriorityMapping,
|
||||
list_position -> Int4,
|
||||
description -> Nullable<Text>,
|
||||
description_text -> Nullable<Text>,
|
||||
estimate -> Nullable<Int4>,
|
||||
time_spent -> Nullable<Int4>,
|
||||
time_remaining -> Nullable<Int4>,
|
||||
reporter_id -> Int4,
|
||||
project_id -> Int4,
|
||||
created_at -> Timestamp,
|
||||
updated_at -> Timestamp,
|
||||
issue_status_id -> Int4,
|
||||
epic_id -> Nullable<Int4>,
|
||||
}
|
||||
}
|
||||
|
||||
diesel::table! {
|
||||
use diesel::sql_types::*;
|
||||
use bitque_data::*;
|
||||
|
||||
messages (id) {
|
||||
id -> Int4,
|
||||
receiver_id -> Int4,
|
||||
sender_id -> Int4,
|
||||
summary -> Text,
|
||||
description -> Text,
|
||||
message_type -> MessageTypeMapping,
|
||||
hyper_link -> Text,
|
||||
created_at -> Timestamp,
|
||||
updated_at -> Timestamp,
|
||||
}
|
||||
}
|
||||
|
||||
diesel::table! {
|
||||
use diesel::sql_types::*;
|
||||
use bitque_data::*;
|
||||
|
||||
projects (id) {
|
||||
id -> Int4,
|
||||
name -> Text,
|
||||
url -> Text,
|
||||
description -> Text,
|
||||
category -> ProjectCategoryMapping,
|
||||
created_at -> Timestamp,
|
||||
updated_at -> Timestamp,
|
||||
time_tracking -> TimeTrackingMapping,
|
||||
}
|
||||
}
|
||||
|
||||
diesel::table! {
|
||||
use diesel::sql_types::*;
|
||||
use bitque_data::*;
|
||||
|
||||
tokens (id) {
|
||||
id -> Int4,
|
||||
user_id -> Int4,
|
||||
access_token -> Uuid,
|
||||
refresh_token -> Uuid,
|
||||
created_at -> Timestamp,
|
||||
updated_at -> Timestamp,
|
||||
bind_token -> Nullable<Uuid>,
|
||||
}
|
||||
}
|
||||
|
||||
diesel::table! {
|
||||
use diesel::sql_types::*;
|
||||
use bitque_data::*;
|
||||
|
||||
user_projects (id) {
|
||||
id -> Int4,
|
||||
user_id -> Int4,
|
||||
project_id -> Int4,
|
||||
is_default -> Bool,
|
||||
is_current -> Bool,
|
||||
role -> UserRoleMapping,
|
||||
created_at -> Timestamp,
|
||||
updated_at -> Timestamp,
|
||||
}
|
||||
}
|
||||
|
||||
diesel::table! {
|
||||
use diesel::sql_types::*;
|
||||
use bitque_data::*;
|
||||
|
||||
user_settings (id) {
|
||||
id -> Int4,
|
||||
user_id -> Int4,
|
||||
text_editor_mode -> TextEditorModeMapping,
|
||||
}
|
||||
}
|
||||
|
||||
diesel::table! {
|
||||
use diesel::sql_types::*;
|
||||
use bitque_data::*;
|
||||
|
||||
users (id) {
|
||||
id -> Int4,
|
||||
name -> Text,
|
||||
email -> Text,
|
||||
avatar_url -> Nullable<Text>,
|
||||
created_at -> Timestamp,
|
||||
updated_at -> Timestamp,
|
||||
}
|
||||
}
|
||||
|
||||
diesel::joinable!(comments -> issues (issue_id));
|
||||
diesel::joinable!(comments -> users (user_id));
|
||||
diesel::joinable!(epics -> projects (project_id));
|
||||
diesel::joinable!(epics -> users (user_id));
|
||||
diesel::joinable!(invitations -> projects (project_id));
|
||||
diesel::joinable!(invitations -> users (invited_by_id));
|
||||
diesel::joinable!(issue_assignees -> issues (issue_id));
|
||||
diesel::joinable!(issue_assignees -> users (user_id));
|
||||
diesel::joinable!(issue_statuses -> projects (project_id));
|
||||
diesel::joinable!(issues -> epics (epic_id));
|
||||
diesel::joinable!(issues -> issue_statuses (issue_status_id));
|
||||
diesel::joinable!(issues -> projects (project_id));
|
||||
diesel::joinable!(issues -> users (reporter_id));
|
||||
diesel::joinable!(tokens -> users (user_id));
|
||||
diesel::joinable!(user_projects -> projects (project_id));
|
||||
diesel::joinable!(user_projects -> users (user_id));
|
||||
diesel::joinable!(user_settings -> users (user_id));
|
||||
|
||||
diesel::allow_tables_to_appear_in_same_query!(
|
||||
comments,
|
||||
epics,
|
||||
invitations,
|
||||
issue_assignees,
|
||||
issue_statuses,
|
||||
issues,
|
||||
messages,
|
||||
projects,
|
||||
tokens,
|
||||
user_projects,
|
||||
user_settings,
|
||||
users,
|
||||
);
|
@ -1,57 +0,0 @@
|
||||
use bitque_data::{TextEditorMode, UserId, UserSetting};
|
||||
use diesel::prelude::*;
|
||||
|
||||
use crate::{db_find, db_update};
|
||||
|
||||
db_find! {
|
||||
FindUserSetting,
|
||||
msg => user_settings => user_settings
|
||||
.distinct_on(id)
|
||||
.filter(user_id.eq(msg.user_id))
|
||||
.limit(1),
|
||||
UserSetting,
|
||||
user_id => UserId
|
||||
}
|
||||
|
||||
db_update! {
|
||||
UpdateUserSetting,
|
||||
msg => conn => user_settings => {
|
||||
inner::Update { user_id: msg.user_id, mode: msg.mode }
|
||||
.execute(conn).or_else(|_|
|
||||
inner::Create { user_id: msg.user_id, mode: msg.mode }
|
||||
.execute(conn)
|
||||
)?;
|
||||
user_settings.filter(user_id.eq(msg.user_id))
|
||||
},
|
||||
UserSetting,
|
||||
user_id => UserId,
|
||||
mode => TextEditorMode
|
||||
}
|
||||
|
||||
mod inner {
|
||||
use bitque_data::{TextEditorMode, UserId, UserSetting};
|
||||
use diesel::prelude::*;
|
||||
|
||||
use crate::{db_create, db_update};
|
||||
|
||||
db_update! {
|
||||
Update,
|
||||
msg => user_settings => {
|
||||
diesel::update(user_settings.filter(user_id.eq(msg.user_id))).set(text_editor_mode.eq(msg.mode))
|
||||
},
|
||||
UserSetting,
|
||||
user_id => UserId,
|
||||
mode => TextEditorMode
|
||||
}
|
||||
|
||||
db_create! {
|
||||
Create,
|
||||
msg => user_settings => diesel::insert_into(user_settings).values((
|
||||
user_id.eq(msg.user_id),
|
||||
text_editor_mode.eq(msg.mode)
|
||||
)),
|
||||
UserSetting,
|
||||
user_id => UserId,
|
||||
mode => TextEditorMode
|
||||
}
|
||||
}
|
@ -1,9 +0,0 @@
|
||||
use diesel::prelude::*;
|
||||
table! {
|
||||
use diesel::sql_types::*;
|
||||
|
||||
issues (id) {
|
||||
id -> Int4,
|
||||
title -> Text,
|
||||
}
|
||||
}
|
@ -1,354 +0,0 @@
|
||||
extern crate proc_macro;
|
||||
|
||||
use proc_macro::{TokenStream, TokenTree};
|
||||
|
||||
fn as_str(name: &str, variants: &[String]) -> String {
|
||||
let mut code = format!(
|
||||
r#"
|
||||
impl {name} {{
|
||||
pub fn as_str(&self) -> &'static str {{
|
||||
match self {{
|
||||
"#,
|
||||
name = name,
|
||||
);
|
||||
|
||||
for variant in variants {
|
||||
let lower = variant.to_lowercase();
|
||||
code.push_str(
|
||||
format!(
|
||||
" {name}::{variant} => \"{lower}\",\n",
|
||||
variant = variant,
|
||||
name = name,
|
||||
lower = lower
|
||||
)
|
||||
.as_str(),
|
||||
);
|
||||
}
|
||||
code.push_str(" }\n }\n}");
|
||||
code
|
||||
}
|
||||
|
||||
fn from_str(name: &str, variants: &[String]) -> String {
|
||||
let mut code = format!(
|
||||
r#"
|
||||
impl std::str::FromStr for {name} {{
|
||||
type Err = String;
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {{
|
||||
match s {{
|
||||
"#,
|
||||
name = name,
|
||||
);
|
||||
|
||||
for variant in variants {
|
||||
let lower = variant.to_lowercase();
|
||||
code.push_str(
|
||||
format!(
|
||||
" \"{lower}\" => Ok({name}::{variant}),\n",
|
||||
variant = variant,
|
||||
name = name,
|
||||
lower = lower
|
||||
)
|
||||
.as_str(),
|
||||
);
|
||||
}
|
||||
|
||||
code.push_str(
|
||||
format!(
|
||||
" _ => Err(format!(\"Unknown {name} {{}}\", s)),",
|
||||
name = name
|
||||
)
|
||||
.as_str(),
|
||||
);
|
||||
code.push_str(" }\n }\n}");
|
||||
code
|
||||
}
|
||||
|
||||
fn into_label(name: &str, variants: &[String]) -> String {
|
||||
let mut code = format!(
|
||||
r#"
|
||||
impl {name} {{
|
||||
pub fn to_label(&self) -> &'static str {{
|
||||
match self {{
|
||||
"#,
|
||||
name = name,
|
||||
);
|
||||
|
||||
for variant in variants {
|
||||
code.push_str(
|
||||
format!(
|
||||
" {name}::{variant} => \"{variant}\",\n",
|
||||
variant = variant,
|
||||
name = name,
|
||||
)
|
||||
.as_str(),
|
||||
);
|
||||
}
|
||||
code.push_str(" }\n }\n}");
|
||||
code
|
||||
}
|
||||
|
||||
fn into_u32(name: &str, variants: &[String]) -> String {
|
||||
let mut code = format!(
|
||||
r#"
|
||||
impl Into<u32> for {name} {{
|
||||
fn into(self) -> u32 {{
|
||||
match self {{
|
||||
"#,
|
||||
name = name
|
||||
);
|
||||
for (idx, variant) in variants.iter().enumerate() {
|
||||
code.push_str(
|
||||
format!(
|
||||
" {name}::{variant} => {idx},\n",
|
||||
variant = variant,
|
||||
name = name,
|
||||
idx = idx
|
||||
)
|
||||
.as_str(),
|
||||
);
|
||||
}
|
||||
code.push_str(" }\n }\n}");
|
||||
code
|
||||
}
|
||||
|
||||
fn from_u32(name: &str, variants: &[String]) -> String {
|
||||
let mut code = format!(
|
||||
r#"
|
||||
impl Into<{name}> for u32 {{
|
||||
fn into(self) -> {name} {{
|
||||
match self {{
|
||||
"#,
|
||||
name = name
|
||||
);
|
||||
for (idx, variant) in variants.iter().enumerate() {
|
||||
code.push_str(
|
||||
format!(
|
||||
" {idx} => {name}::{variant},\n",
|
||||
variant = variant,
|
||||
name = name,
|
||||
idx = idx
|
||||
)
|
||||
.as_str(),
|
||||
);
|
||||
}
|
||||
code.push_str(format!(" _ => {name}::default(),\n", name = name,).as_str());
|
||||
code.push_str(" }\n }\n}");
|
||||
code
|
||||
}
|
||||
|
||||
#[proc_macro_derive(EnumU32)]
|
||||
pub fn derive_enum_u32(item: TokenStream) -> TokenStream {
|
||||
let mut it = item.into_iter().peekable();
|
||||
|
||||
while let Some(token) = it.peek() {
|
||||
match token {
|
||||
TokenTree::Ident(_) => {
|
||||
break;
|
||||
}
|
||||
_ => {
|
||||
it.next();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(TokenTree::Ident(ident)) = it.next() {
|
||||
if ident.to_string().as_str() != "pub" {
|
||||
panic!("Expect to find keyword pub but was found {:?}", ident)
|
||||
}
|
||||
} else {
|
||||
panic!("Expect to find keyword pub but nothing was found")
|
||||
}
|
||||
if let Some(TokenTree::Ident(ident)) = it.next() {
|
||||
if ident.to_string().as_str() != "enum" {
|
||||
panic!("Expect to find keyword struct but was found {:?}", ident)
|
||||
}
|
||||
} else {
|
||||
panic!("Expect to find keyword struct but nothing was found")
|
||||
}
|
||||
let name = it
|
||||
.next()
|
||||
.expect("Expect to struct name but nothing was found")
|
||||
.to_string();
|
||||
|
||||
let mut variants = vec![];
|
||||
if let Some(TokenTree::Group(group)) = it.next() {
|
||||
for token in group.stream() {
|
||||
if let TokenTree::Ident(ident) = token {
|
||||
variants.push(ident.to_string())
|
||||
}
|
||||
}
|
||||
} else {
|
||||
panic!("Enum variants group expected");
|
||||
}
|
||||
if variants.is_empty() {
|
||||
panic!("Enum cannot be empty")
|
||||
}
|
||||
|
||||
let mut code = String::new();
|
||||
code.push_str(into_u32(&name, &variants).as_str());
|
||||
code.push_str(from_u32(&name, &variants).as_str());
|
||||
code.parse().unwrap()
|
||||
}
|
||||
|
||||
#[proc_macro_derive(EnumLabel)]
|
||||
pub fn derive_enum_label(item: TokenStream) -> TokenStream {
|
||||
let mut it = item.into_iter().peekable();
|
||||
|
||||
while let Some(token) = it.peek() {
|
||||
match token {
|
||||
TokenTree::Ident(_) => {
|
||||
break;
|
||||
}
|
||||
_ => {
|
||||
it.next();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(TokenTree::Ident(ident)) = it.next() {
|
||||
if ident.to_string().as_str() != "pub" {
|
||||
panic!("Expect to find keyword pub but was found {:?}", ident)
|
||||
}
|
||||
} else {
|
||||
panic!("Expect to find keyword pub but nothing was found")
|
||||
}
|
||||
if let Some(TokenTree::Ident(ident)) = it.next() {
|
||||
if ident.to_string().as_str() != "enum" {
|
||||
panic!("Expect to find keyword struct but was found {:?}", ident)
|
||||
}
|
||||
} else {
|
||||
panic!("Expect to find keyword struct but nothing was found")
|
||||
}
|
||||
let name = it
|
||||
.next()
|
||||
.expect("Expect to struct name but nothing was found")
|
||||
.to_string();
|
||||
|
||||
let mut variants = vec![];
|
||||
if let Some(TokenTree::Group(group)) = it.next() {
|
||||
for token in group.stream() {
|
||||
if let TokenTree::Ident(ident) = token {
|
||||
variants.push(ident.to_string())
|
||||
}
|
||||
}
|
||||
} else {
|
||||
panic!("Enum variants group expected");
|
||||
}
|
||||
if variants.is_empty() {
|
||||
panic!("Enum cannot be empty")
|
||||
}
|
||||
|
||||
let mut code = String::new();
|
||||
code.push_str(into_label(&name, &variants).as_str());
|
||||
code.parse().unwrap()
|
||||
}
|
||||
|
||||
#[proc_macro_derive(EnumStr)]
|
||||
pub fn derive_enum_str(item: TokenStream) -> TokenStream {
|
||||
let mut it = item.into_iter().peekable();
|
||||
|
||||
while let Some(token) = it.peek() {
|
||||
match token {
|
||||
TokenTree::Ident(_) => {
|
||||
break;
|
||||
}
|
||||
_ => {
|
||||
it.next();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(TokenTree::Ident(ident)) = it.next() {
|
||||
if ident.to_string().as_str() != "pub" {
|
||||
panic!("Expect to find keyword pub but was found {:?}", ident)
|
||||
}
|
||||
} else {
|
||||
panic!("Expect to find keyword pub but nothing was found")
|
||||
}
|
||||
if let Some(TokenTree::Ident(ident)) = it.next() {
|
||||
if ident.to_string().as_str() != "enum" {
|
||||
panic!("Expect to find keyword struct but was found {:?}", ident)
|
||||
}
|
||||
} else {
|
||||
panic!("Expect to find keyword struct but nothing was found")
|
||||
}
|
||||
let name = it
|
||||
.next()
|
||||
.expect("Expect to struct name but nothing was found")
|
||||
.to_string();
|
||||
|
||||
let mut variants = vec![];
|
||||
if let Some(TokenTree::Group(group)) = it.next() {
|
||||
for token in group.stream() {
|
||||
if let TokenTree::Ident(ident) = token {
|
||||
variants.push(ident.to_string())
|
||||
}
|
||||
}
|
||||
} else {
|
||||
panic!("Enum variants group expected");
|
||||
}
|
||||
if variants.is_empty() {
|
||||
panic!("Enum cannot be empty")
|
||||
}
|
||||
|
||||
let mut code = String::new();
|
||||
|
||||
code.push_str(from_str(&name, &variants).as_str());
|
||||
|
||||
code.parse().unwrap()
|
||||
}
|
||||
|
||||
#[proc_macro_derive(EnumAsStr)]
|
||||
pub fn derive_enum_as_str(item: TokenStream) -> TokenStream {
|
||||
let mut it = item.into_iter().peekable();
|
||||
|
||||
while let Some(token) = it.peek() {
|
||||
match token {
|
||||
TokenTree::Ident(_) => {
|
||||
break;
|
||||
}
|
||||
_ => {
|
||||
it.next();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(TokenTree::Ident(ident)) = it.next() {
|
||||
if ident.to_string().as_str() != "pub" {
|
||||
panic!("Expect to find keyword pub but was found {:?}", ident)
|
||||
}
|
||||
} else {
|
||||
panic!("Expect to find keyword pub but nothing was found")
|
||||
}
|
||||
if let Some(TokenTree::Ident(ident)) = it.next() {
|
||||
if ident.to_string().as_str() != "enum" {
|
||||
panic!("Expect to find keyword struct but was found {:?}", ident)
|
||||
}
|
||||
} else {
|
||||
panic!("Expect to find keyword struct but nothing was found")
|
||||
}
|
||||
let name = it
|
||||
.next()
|
||||
.expect("Expect to struct name but nothing was found")
|
||||
.to_string();
|
||||
|
||||
let mut variants = vec![];
|
||||
if let Some(TokenTree::Group(group)) = it.next() {
|
||||
for token in group.stream() {
|
||||
if let TokenTree::Ident(ident) = token {
|
||||
variants.push(ident.to_string())
|
||||
}
|
||||
}
|
||||
} else {
|
||||
panic!("Enum variants group expected");
|
||||
}
|
||||
if variants.is_empty() {
|
||||
panic!("Enum cannot be empty")
|
||||
}
|
||||
|
||||
let mut code = String::new();
|
||||
|
||||
code.push_str(as_str(&name, &variants).as_str());
|
||||
|
||||
code.parse().unwrap()
|
||||
}
|
@ -1,26 +0,0 @@
|
||||
[package]
|
||||
name = "diesel-derive-enum"
|
||||
version = "2.0.1"
|
||||
description = "Derive diesel boilerplate for using enums in databases"
|
||||
authors = ["Alex Whitney <adwhit@fastmail.com>"]
|
||||
repository = "http://github.com/adwhit/diesel-derive-enum"
|
||||
homepage = "http://github.com/adwhit/diesel-derive-enum"
|
||||
keywords = ["diesel", "postgres", "sqlite", "mysql", "sql"]
|
||||
license = "MIT OR Apache-2.0"
|
||||
readme = "README.md"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
quote = "1"
|
||||
syn = "1"
|
||||
heck = "0.4.0"
|
||||
proc-macro2 = "1"
|
||||
|
||||
[features]
|
||||
postgres = []
|
||||
sqlite = []
|
||||
mysql = []
|
||||
|
||||
[lib]
|
||||
name = "diesel_derive_enum"
|
||||
proc-macro = true
|
@ -1,500 +0,0 @@
|
||||
#![recursion_limit = "1024"]
|
||||
|
||||
extern crate proc_macro;
|
||||
|
||||
use heck::{ToKebabCase, ToLowerCamelCase, ToShoutySnakeCase, ToSnakeCase, ToUpperCamelCase};
|
||||
use proc_macro::TokenStream;
|
||||
use proc_macro2::{Ident, Span};
|
||||
use quote::quote;
|
||||
use syn::*;
|
||||
|
||||
/// Implement the traits necessary for inserting the enum directly into a
|
||||
/// database
|
||||
///
|
||||
/// # Attributes
|
||||
///
|
||||
/// ## Type attributes
|
||||
///
|
||||
/// * `#[ExistingTypePath = "crate::schema::sql_types::NewEnum"]` specifies the
|
||||
/// path to a corresponding diesel type that was already created by the diesel
|
||||
/// CLI. If omitted, the type will be generated by this macro. *Note*: Only
|
||||
/// applies to `postgres`, will error if specified for other databases
|
||||
/// * `#[DieselType = "NewEnumMapping"]` specifies the name for the diesel type
|
||||
/// to create. If omitted, uses `<enum name>Mapping`. *Note*: Cannot be
|
||||
/// specified alongside `ExistingTypePath`
|
||||
/// * `#[DbValueStyle = "snake_case"]` specifies a renaming style from each of
|
||||
/// the rust enum variants to each of the database variants. Either
|
||||
/// `camelCase`, `kebab-case`, `PascalCase`, `SCREAMING_SNAKE_CASE`,
|
||||
/// `snake_case`, `verbatim`. If omitted, uses `snake_case`.
|
||||
///
|
||||
/// ## Variant attributes
|
||||
///
|
||||
/// * `#[db_rename = "variant"]` specifies the db name for a specific variant.
|
||||
#[proc_macro_derive(
|
||||
DbEnum,
|
||||
attributes(PgType, DieselType, ExistingTypePath, DbValueStyle, db_rename)
|
||||
)]
|
||||
pub fn derive(input: TokenStream) -> TokenStream {
|
||||
let input: DeriveInput = parse_macro_input!(input as DeriveInput);
|
||||
|
||||
let existing_mapping_path = val_from_attrs(&input.attrs, "ExistingTypePath");
|
||||
if !cfg!(feature = "postgres") && existing_mapping_path.is_some() {
|
||||
panic!("ExistingTypePath attribute only applies when the 'postgres' feature is enabled");
|
||||
}
|
||||
|
||||
// we could allow a default value here but... I'm not very keen
|
||||
// let existing_mapping_path = existing_mapping_path
|
||||
// .unwrap_or_else(|| format!("crate::schema::sql_types::{}", input.ident));
|
||||
|
||||
let pg_internal_type = val_from_attrs(&input.attrs, "PgType");
|
||||
|
||||
if existing_mapping_path.is_some() && pg_internal_type.is_some() {
|
||||
panic!("Cannot specify both `ExistingTypePath` and `PgType` attributes");
|
||||
}
|
||||
|
||||
let pg_internal_type = pg_internal_type.unwrap_or(input.ident.to_string().to_snake_case());
|
||||
|
||||
let new_diesel_mapping = val_from_attrs(&input.attrs, "DieselType");
|
||||
if existing_mapping_path.is_some() && new_diesel_mapping.is_some() {
|
||||
panic!("Cannot specify both `ExistingTypePath` and `DieselType` attributes");
|
||||
}
|
||||
let new_diesel_mapping = new_diesel_mapping.unwrap_or_else(|| format!("{}Type", input.ident));
|
||||
|
||||
// Maintain backwards compatibility by defaulting to snake case.
|
||||
let case_style =
|
||||
val_from_attrs(&input.attrs, "DbValueStyle").unwrap_or_else(|| "snake_case".to_string());
|
||||
let case_style = CaseStyle::from_string(&case_style);
|
||||
|
||||
let existing_mapping_path = existing_mapping_path.map(|v| {
|
||||
v.parse::<proc_macro2::TokenStream>()
|
||||
.expect("ExistingTypePath is not a valid token")
|
||||
});
|
||||
let new_diesel_mapping = Ident::new(new_diesel_mapping.as_ref(), Span::call_site());
|
||||
if let Data::Enum(syn::DataEnum {
|
||||
variants: data_variants,
|
||||
..
|
||||
}) = input.data
|
||||
{
|
||||
generate_derive_enum_impls(
|
||||
&existing_mapping_path,
|
||||
&new_diesel_mapping,
|
||||
&pg_internal_type,
|
||||
case_style,
|
||||
&input.ident,
|
||||
&data_variants,
|
||||
)
|
||||
} else {
|
||||
syn::Error::new(
|
||||
Span::call_site(),
|
||||
"derive(DbEnum) can only be applied to enums",
|
||||
)
|
||||
.to_compile_error()
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
fn val_from_attrs(attrs: &[Attribute], attr_name: &str) -> Option<String> {
|
||||
for attr in attrs {
|
||||
if attr.path.is_ident(attr_name) {
|
||||
match attr.parse_meta().ok()? {
|
||||
Meta::NameValue(MetaNameValue {
|
||||
lit: Lit::Str(lit_str),
|
||||
..
|
||||
}) => return Some(lit_str.value()),
|
||||
_ => panic!(
|
||||
"Attribute '{}' must have form: {} = \"value\"",
|
||||
attr_name, attr_name
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Defines the casing for the database representation. Follows serde naming
|
||||
/// convention.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
enum CaseStyle {
|
||||
Camel,
|
||||
Kebab,
|
||||
Pascal,
|
||||
Upper,
|
||||
ScreamingSnake,
|
||||
Snake,
|
||||
Verbatim,
|
||||
}
|
||||
|
||||
impl CaseStyle {
|
||||
fn from_string(name: &str) -> Self {
|
||||
match name {
|
||||
"camelCase" => CaseStyle::Camel,
|
||||
"kebab-case" => CaseStyle::Kebab,
|
||||
"PascalCase" => CaseStyle::Pascal,
|
||||
"SCREAMING_SNAKE_CASE" => CaseStyle::ScreamingSnake,
|
||||
"UPPERCASE" => CaseStyle::Upper,
|
||||
"snake_case" => CaseStyle::Snake,
|
||||
"verbatim" | "verbatimcase" => CaseStyle::Verbatim,
|
||||
s => panic!("unsupported casing: `{}`", s),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_derive_enum_impls(
|
||||
existing_mapping_path: &Option<proc_macro2::TokenStream>,
|
||||
new_diesel_mapping: &Ident,
|
||||
pg_internal_type: &str,
|
||||
case_style: CaseStyle,
|
||||
enum_ty: &Ident,
|
||||
variants: &syn::punctuated::Punctuated<Variant, syn::token::Comma>,
|
||||
) -> TokenStream {
|
||||
let modname = Ident::new(&format!("db_enum_impl_{}", enum_ty), Span::call_site());
|
||||
let variant_ids: Vec<proc_macro2::TokenStream> = variants
|
||||
.iter()
|
||||
.map(|variant| {
|
||||
if let Fields::Unit = variant.fields {
|
||||
let id = &variant.ident;
|
||||
quote! {
|
||||
#enum_ty::#id
|
||||
}
|
||||
} else {
|
||||
panic!("Variants must be fieldless")
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
let variants_db: Vec<String> = variants
|
||||
.iter()
|
||||
.map(|variant| {
|
||||
val_from_attrs(&variant.attrs, "db_rename")
|
||||
.unwrap_or_else(|| stylize_value(&variant.ident.to_string(), case_style))
|
||||
})
|
||||
.collect();
|
||||
let variants_db_bytes: Vec<LitByteStr> = variants_db
|
||||
.iter()
|
||||
.map(|variant_str| LitByteStr::new(variant_str.as_bytes(), Span::call_site()))
|
||||
.collect();
|
||||
|
||||
let common = generate_common(enum_ty, &variant_ids, &variants_db, &variants_db_bytes);
|
||||
let (diesel_mapping_def, diesel_mapping_use) =
|
||||
// Skip this part if we already have an existing mapping
|
||||
if existing_mapping_path.is_some() {
|
||||
(None, None)
|
||||
} else {
|
||||
let new_diesel_mapping_def = generate_new_diesel_mapping(new_diesel_mapping, pg_internal_type);
|
||||
let common_impls_on_new_diesel_mapping =
|
||||
generate_common_impls("e! { #new_diesel_mapping }, enum_ty);
|
||||
(
|
||||
Some(quote! {
|
||||
#new_diesel_mapping_def
|
||||
#common_impls_on_new_diesel_mapping
|
||||
}),
|
||||
Some(quote! {
|
||||
pub use self::#modname::#new_diesel_mapping;
|
||||
}),
|
||||
)
|
||||
};
|
||||
|
||||
let pg_impl = if cfg!(feature = "postgres") {
|
||||
match existing_mapping_path {
|
||||
Some(path) => {
|
||||
let common_impls_on_existing_diesel_mapping = generate_common_impls(path, enum_ty);
|
||||
let postgres_impl = generate_postgres_impl(path, enum_ty, true);
|
||||
Some(quote! {
|
||||
#common_impls_on_existing_diesel_mapping
|
||||
#postgres_impl
|
||||
})
|
||||
}
|
||||
None => Some(generate_postgres_impl(
|
||||
"e! { #new_diesel_mapping },
|
||||
enum_ty,
|
||||
false,
|
||||
)),
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let mysql_impl = if cfg!(feature = "mysql") {
|
||||
Some(generate_mysql_impl(new_diesel_mapping, enum_ty))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let sqlite_impl = if cfg!(feature = "sqlite") {
|
||||
Some(generate_sqlite_impl(new_diesel_mapping, enum_ty))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let imports = quote! {
|
||||
use super::*;
|
||||
use diesel::{
|
||||
backend::{self, Backend},
|
||||
deserialize::{self, FromSql},
|
||||
expression::AsExpression,
|
||||
internal::derives::as_expression::Bound,
|
||||
query_builder::{bind_collector::RawBytesBindCollector, QueryId},
|
||||
row::Row,
|
||||
serialize::{self, IsNull, Output, ToSql},
|
||||
sql_types::*,
|
||||
Queryable,
|
||||
};
|
||||
use std::io::Write;
|
||||
};
|
||||
|
||||
let quoted = quote! {
|
||||
#diesel_mapping_use
|
||||
#[allow(non_snake_case)]
|
||||
mod #modname {
|
||||
#imports
|
||||
|
||||
#common
|
||||
#diesel_mapping_def
|
||||
#pg_impl
|
||||
#mysql_impl
|
||||
#sqlite_impl
|
||||
}
|
||||
};
|
||||
|
||||
quoted.into()
|
||||
}
|
||||
|
||||
fn stylize_value(value: &str, style: CaseStyle) -> String {
|
||||
match style {
|
||||
CaseStyle::Camel => value.to_lower_camel_case(),
|
||||
CaseStyle::Kebab => value.to_kebab_case(),
|
||||
CaseStyle::Pascal => value.to_upper_camel_case(),
|
||||
CaseStyle::Upper => value.to_uppercase(),
|
||||
CaseStyle::ScreamingSnake => value.to_shouty_snake_case(),
|
||||
CaseStyle::Snake => value.to_snake_case(),
|
||||
CaseStyle::Verbatim => value.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_common(
|
||||
enum_ty: &Ident,
|
||||
variants_rs: &[proc_macro2::TokenStream],
|
||||
variants_db: &[String],
|
||||
variants_db_bytes: &[LitByteStr],
|
||||
) -> proc_macro2::TokenStream {
|
||||
quote! {
|
||||
fn db_str_representation(e: &#enum_ty) -> &'static str {
|
||||
match *e {
|
||||
#(#variants_rs => #variants_db,)*
|
||||
}
|
||||
}
|
||||
|
||||
fn from_db_binary_representation(bytes: &[u8]) -> deserialize::Result<#enum_ty> {
|
||||
match bytes {
|
||||
#(#variants_db_bytes => Ok(#variants_rs),)*
|
||||
v => Err(format!("Unrecognized enum variant: '{}'",
|
||||
String::from_utf8_lossy(v)).into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_new_diesel_mapping(
|
||||
new_diesel_mapping: &Ident,
|
||||
pg_internal_type: &str,
|
||||
) -> proc_macro2::TokenStream {
|
||||
// Note - we only generate a new mapping for mysql and sqlite, postgres
|
||||
// should already have one
|
||||
quote! {
|
||||
#[derive(SqlType, Clone)]
|
||||
#[diesel(mysql_type(name = "Enum"))]
|
||||
#[diesel(sqlite_type(name = "Text"))]
|
||||
#[diesel(postgres_type(name = #pg_internal_type))]
|
||||
pub struct #new_diesel_mapping;
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_common_impls(
|
||||
diesel_mapping: &proc_macro2::TokenStream,
|
||||
enum_ty: &Ident,
|
||||
) -> proc_macro2::TokenStream {
|
||||
quote! {
|
||||
|
||||
// NOTE: at some point this impl will no longer be necessary
|
||||
// for diesel-cli schemas
|
||||
// See https://github.com/adwhit/diesel-derive-enum/issues/10
|
||||
// and https://github.com/adwhit/diesel-derive-enum/pull/79
|
||||
impl QueryId for #diesel_mapping {
|
||||
type QueryId = #diesel_mapping;
|
||||
const HAS_STATIC_QUERY_ID: bool = true;
|
||||
}
|
||||
|
||||
impl AsExpression<#diesel_mapping> for #enum_ty {
|
||||
type Expression = Bound<#diesel_mapping, Self>;
|
||||
|
||||
fn as_expression(self) -> Self::Expression {
|
||||
Bound::new(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl AsExpression<Nullable<#diesel_mapping>> for #enum_ty {
|
||||
type Expression = Bound<Nullable<#diesel_mapping>, Self>;
|
||||
|
||||
fn as_expression(self) -> Self::Expression {
|
||||
Bound::new(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> AsExpression<#diesel_mapping> for &'a #enum_ty {
|
||||
type Expression = Bound<#diesel_mapping, Self>;
|
||||
|
||||
fn as_expression(self) -> Self::Expression {
|
||||
Bound::new(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> AsExpression<Nullable<#diesel_mapping>> for &'a #enum_ty {
|
||||
type Expression = Bound<Nullable<#diesel_mapping>, Self>;
|
||||
|
||||
fn as_expression(self) -> Self::Expression {
|
||||
Bound::new(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'b> AsExpression<#diesel_mapping> for &'a &'b #enum_ty {
|
||||
type Expression = Bound<#diesel_mapping, Self>;
|
||||
|
||||
fn as_expression(self) -> Self::Expression {
|
||||
Bound::new(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'b> AsExpression<Nullable<#diesel_mapping>> for &'a &'b #enum_ty {
|
||||
type Expression = Bound<Nullable<#diesel_mapping>, Self>;
|
||||
|
||||
fn as_expression(self) -> Self::Expression {
|
||||
Bound::new(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl<DB> ToSql<Nullable<#diesel_mapping>, DB> for #enum_ty
|
||||
where
|
||||
DB: Backend,
|
||||
Self: ToSql<#diesel_mapping, DB>,
|
||||
{
|
||||
fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, DB>) -> serialize::Result {
|
||||
ToSql::<#diesel_mapping, DB>::to_sql(self, out)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_postgres_impl(
|
||||
diesel_mapping: &proc_macro2::TokenStream,
|
||||
enum_ty: &Ident,
|
||||
with_clone: bool,
|
||||
) -> proc_macro2::TokenStream {
|
||||
// If the type was generated by postgres, we have to manually add a clone impl,
|
||||
// if generated by 'us' it has already been done
|
||||
let clone_impl = if with_clone {
|
||||
Some(quote! {
|
||||
impl Clone for #diesel_mapping {
|
||||
fn clone(&self) -> Self {
|
||||
#diesel_mapping
|
||||
}
|
||||
}
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
quote! {
|
||||
mod pg_impl {
|
||||
use super::*;
|
||||
use diesel::pg::{Pg, PgValue};
|
||||
|
||||
#clone_impl
|
||||
|
||||
impl FromSql<#diesel_mapping, Pg> for #enum_ty {
|
||||
fn from_sql(raw: PgValue) -> deserialize::Result<Self> {
|
||||
from_db_binary_representation(raw.as_bytes())
|
||||
}
|
||||
}
|
||||
|
||||
impl ToSql<#diesel_mapping, Pg> for #enum_ty
|
||||
{
|
||||
fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, Pg>) -> serialize::Result {
|
||||
out.write_all(db_str_representation(self).as_bytes())?;
|
||||
Ok(IsNull::No)
|
||||
}
|
||||
}
|
||||
|
||||
impl Queryable<#diesel_mapping, Pg> for #enum_ty {
|
||||
type Row = Self;
|
||||
|
||||
fn build(row: Self::Row) -> deserialize::Result<Self> {
|
||||
Ok(row)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_mysql_impl(diesel_mapping: &Ident, enum_ty: &Ident) -> proc_macro2::TokenStream {
|
||||
quote! {
|
||||
mod mysql_impl {
|
||||
use super::*;
|
||||
use diesel;
|
||||
use diesel::mysql::{Mysql, MysqlValue};
|
||||
|
||||
impl FromSql<#diesel_mapping, Mysql> for #enum_ty {
|
||||
fn from_sql(raw: MysqlValue) -> deserialize::Result<Self> {
|
||||
from_db_binary_representation(raw.as_bytes())
|
||||
}
|
||||
}
|
||||
|
||||
impl ToSql<#diesel_mapping, Mysql> for #enum_ty
|
||||
{
|
||||
fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, Mysql>) -> serialize::Result {
|
||||
out.write_all(db_str_representation(self).as_bytes())?;
|
||||
Ok(IsNull::No)
|
||||
}
|
||||
}
|
||||
|
||||
impl Queryable<#diesel_mapping, Mysql> for #enum_ty {
|
||||
type Row = Self;
|
||||
|
||||
fn build(row: Self::Row) -> deserialize::Result<Self> {
|
||||
Ok(row)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_sqlite_impl(diesel_mapping: &Ident, enum_ty: &Ident) -> proc_macro2::TokenStream {
|
||||
quote! {
|
||||
mod sqlite_impl {
|
||||
use super::*;
|
||||
use diesel;
|
||||
use diesel::sql_types;
|
||||
use diesel::sqlite::Sqlite;
|
||||
|
||||
impl FromSql<#diesel_mapping, Sqlite> for #enum_ty {
|
||||
fn from_sql(value: backend::RawValue<Sqlite>) -> deserialize::Result<Self> {
|
||||
let bytes = <Vec<u8> as FromSql<sql_types::Binary, Sqlite>>::from_sql(value)?;
|
||||
from_db_binary_representation(bytes.as_slice())
|
||||
}
|
||||
}
|
||||
|
||||
impl ToSql<#diesel_mapping, Sqlite> for #enum_ty {
|
||||
fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, Sqlite>) -> serialize::Result {
|
||||
<str as ToSql<sql_types::Text, Sqlite>>::to_sql(db_str_representation(self), out)
|
||||
}
|
||||
}
|
||||
|
||||
impl Queryable<#diesel_mapping, Sqlite> for #enum_ty {
|
||||
type Row = Self;
|
||||
|
||||
fn build(row: Self::Row) -> deserialize::Result<Self> {
|
||||
Ok(row)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
[package]
|
||||
name = "filesystem-actor"
|
||||
version = "0.1.0"
|
||||
authors = ["Adrian Wozniak <adrian.wozniak@ita-prog.pl>"]
|
||||
edition = "2018"
|
||||
description = "BITQUE (Simplified JIRA in Rust) shared data types"
|
||||
repository = "https://gitlab.com/adrian.wozniak/bitque"
|
||||
license = "MPL-2.0"
|
||||
#license-file = "../LICENSE"
|
||||
|
||||
[lib]
|
||||
name = "filesystem_actor"
|
||||
path = "./src/lib.rs"
|
||||
|
||||
[dependencies]
|
||||
actix = { version = "0.13.0" }
|
||||
actix-files = { version = "0.6.2" }
|
||||
bitque-config = { workspace = true, features = ["local-storage"] }
|
||||
bytes = { version = "1.4.0" }
|
||||
futures = { version = "0.3.8" }
|
||||
tokio = { version = "1", features = ["full"] }
|
@ -1,25 +0,0 @@
|
||||
[package]
|
||||
name = "highlight-actor"
|
||||
version = "0.1.0"
|
||||
authors = ["Adrian Wozniak <adrian.wozniak@ita-prog.pl>"]
|
||||
edition = "2018"
|
||||
description = "BITQUE (Simplified JIRA in Rust) shared data types"
|
||||
repository = "https://gitlab.com/adrian.wozniak/bitque"
|
||||
license = "MPL-2.0"
|
||||
#license-file = "../LICENSE"
|
||||
|
||||
[lib]
|
||||
name = "highlight_actor"
|
||||
path = "./src/lib.rs"
|
||||
|
||||
[dependencies]
|
||||
actix = { version = "0.13.0" }
|
||||
bincode = { version = "*" }
|
||||
bitque-config = { workspace = true, features = ["hi"] }
|
||||
bitque-data = { workspace = true, features = ["backend"] }
|
||||
flate2 = { version = "*" }
|
||||
lazy_static = { version = "*" }
|
||||
serde = { version = "*" }
|
||||
simsearch = { version = "0.2" }
|
||||
syntect = { version = "*" }
|
||||
toml = { version = "*" }
|
File diff suppressed because it is too large
Load Diff
@ -1,12 +0,0 @@
|
||||
use syntect::highlighting::ThemeSet;
|
||||
use syntect::parsing::SyntaxSet;
|
||||
|
||||
#[inline(always)]
|
||||
pub fn integrated_syntaxset() -> syntect::parsing::SyntaxSet {
|
||||
SyntaxSet::load_defaults_newlines()
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn integrated_themeset() -> syntect::highlighting::ThemeSet {
|
||||
ThemeSet::load_defaults()
|
||||
}
|
@ -1,65 +0,0 @@
|
||||
use actix::{Handler, Message};
|
||||
use lettre::message::header::ContentType;
|
||||
use lettre::message::Mailbox;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{email_address, MailError, MailExecutor};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Invite {
|
||||
pub bind_token: Uuid,
|
||||
pub email: String,
|
||||
pub inviter_name: String,
|
||||
}
|
||||
|
||||
impl Message for Invite {
|
||||
type Result = Result<(), MailError>;
|
||||
}
|
||||
|
||||
impl Handler<Invite> for MailExecutor {
|
||||
type Result = Result<(), MailError>;
|
||||
|
||||
fn handle(&mut self, msg: Invite, _ctx: &mut Self::Context) -> Self::Result {
|
||||
use lettre::Transport;
|
||||
let transport = &mut self.transport;
|
||||
let addr = bitque_config::web::Configuration::read().full_addr();
|
||||
let from = email_address(self.config.from.as_str())?;
|
||||
let to = email_address(&msg.email)?;
|
||||
|
||||
let html = format!(
|
||||
r#"
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head><meta charset="UTF-8"></head>
|
||||
<body>
|
||||
<h1>You have been invited to project by {inviter_name}!</h1>
|
||||
<p>
|
||||
</p>
|
||||
<p>
|
||||
Please click this link: <a href="{addr}/invite?token={bind_token}">{addr}/invite?token={bind_token}</a>
|
||||
</p>
|
||||
</body>
|
||||
</html>
|
||||
"#,
|
||||
bind_token = msg.bind_token,
|
||||
inviter_name = msg.inviter_name,
|
||||
addr = addr,
|
||||
);
|
||||
|
||||
let mail = lettre::Message::builder()
|
||||
.to(Mailbox::new(None, to))
|
||||
.from(Mailbox::new(None, from))
|
||||
.subject("Invitation to BITQUE project")
|
||||
.header(ContentType::TEXT_HTML)
|
||||
.body(html)
|
||||
.map_err(|e| {
|
||||
tracing::error!("{:?}", e);
|
||||
MailError::MalformedBody
|
||||
})?;
|
||||
|
||||
transport.send(&mail).map(|_| ()).map_err(|e| {
|
||||
tracing::error!("Mailer: {}", e);
|
||||
MailError::FailedToSendEmail
|
||||
})
|
||||
}
|
||||
}
|
@ -1,74 +0,0 @@
|
||||
use std::convert::TryFrom;
|
||||
|
||||
use actix::{Actor, SyncContext};
|
||||
use lettre::transport::smtp::authentication::Credentials;
|
||||
use lettre::transport::smtp::PoolConfig;
|
||||
|
||||
pub mod invite;
|
||||
pub mod welcome;
|
||||
|
||||
pub type MailTransport = lettre::SmtpTransport;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum MailError {
|
||||
EmailWithoutUser,
|
||||
EmailWithoutDomain,
|
||||
InvalidEmailAddress,
|
||||
FailedToSendEmail,
|
||||
MalformedBody,
|
||||
}
|
||||
|
||||
pub struct MailExecutor {
|
||||
pub transport: MailTransport,
|
||||
pub config: bitque_config::mail::Configuration,
|
||||
}
|
||||
|
||||
impl Actor for MailExecutor {
|
||||
type Context = SyncContext<Self>;
|
||||
}
|
||||
|
||||
impl Default for MailExecutor {
|
||||
fn default() -> Self {
|
||||
let config = bitque_config::mail::Configuration::read();
|
||||
Self {
|
||||
transport: mail_transport(&config),
|
||||
config,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn mail_client(config: &bitque_config::mail::Configuration) -> lettre::SmtpTransport {
|
||||
let bitque_config::mail::Configuration {
|
||||
user: mail_user,
|
||||
pass: mail_pass,
|
||||
host: mail_host,
|
||||
..
|
||||
} = &config;
|
||||
|
||||
lettre::SmtpTransport::relay(mail_host)
|
||||
.expect("Failed to init SMTP client")
|
||||
.credentials(Credentials::new(mail_user.clone(), mail_pass.clone()))
|
||||
.pool_config(PoolConfig::default())
|
||||
.build()
|
||||
}
|
||||
|
||||
fn mail_transport(config: &bitque_config::mail::Configuration) -> MailTransport {
|
||||
mail_client(config)
|
||||
}
|
||||
|
||||
pub fn email_address(email: &str) -> Result<lettre::Address, MailError> {
|
||||
let (user, domain) = {
|
||||
let mut parts = email.split("@");
|
||||
(
|
||||
match parts.next() {
|
||||
Some(s) => s,
|
||||
None => return Err(MailError::EmailWithoutUser),
|
||||
},
|
||||
match parts.next() {
|
||||
Some(s) => s,
|
||||
None => return Err(MailError::EmailWithoutDomain),
|
||||
},
|
||||
)
|
||||
};
|
||||
lettre::Address::try_from((user, domain)).map_err(|_| MailError::InvalidEmailAddress)
|
||||
}
|
@ -1,67 +0,0 @@
|
||||
use actix::{Handler, Message};
|
||||
use lettre::message::header::ContentType;
|
||||
use lettre::message::Mailbox;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{email_address, MailError, MailExecutor};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Welcome {
|
||||
pub bind_token: Uuid,
|
||||
pub email: String,
|
||||
}
|
||||
|
||||
impl Message for Welcome {
|
||||
type Result = Result<(), MailError>;
|
||||
}
|
||||
|
||||
impl Handler<Welcome> for MailExecutor {
|
||||
type Result = Result<(), MailError>;
|
||||
|
||||
fn handle(&mut self, msg: Welcome, _ctx: &mut Self::Context) -> Self::Result {
|
||||
use lettre::Transport;
|
||||
let transport = &mut self.transport;
|
||||
let from = email_address(self.config.from.as_str())?;
|
||||
let to = email_address(&msg.email)?;
|
||||
|
||||
let html = format!(
|
||||
r#"
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head><meta charset="UTF-8"></head>
|
||||
<body>
|
||||
<h1>Welcome in BITQUE!</h1>
|
||||
<p>
|
||||
</p>
|
||||
<p>
|
||||
Please copy this code to sign-in single use token field: <pre><code>{bind_token}</code</pre>
|
||||
</p>
|
||||
<p>
|
||||
Notice: This token is single use and will be removed from system once you use it.
|
||||
</p>
|
||||
</body>
|
||||
</html>
|
||||
"#,
|
||||
bind_token = msg.bind_token,
|
||||
);
|
||||
if cfg!(debug_assetrions) {
|
||||
tracing::info!("Sending email:\n{}", html);
|
||||
}
|
||||
|
||||
let mail = lettre::Message::builder()
|
||||
.to(Mailbox::new(None, to))
|
||||
.from(Mailbox::new(None, from))
|
||||
.subject("Welcome to BITQUE")
|
||||
.header(ContentType::TEXT_HTML)
|
||||
.body(html)
|
||||
.map_err(|e| {
|
||||
tracing::error!("{:?}", e);
|
||||
MailError::MalformedBody
|
||||
})?;
|
||||
|
||||
transport.send(&mail).map(|_| ()).map_err(|e| {
|
||||
tracing::error!("{:?}", e);
|
||||
MailError::FailedToSendEmail
|
||||
})
|
||||
}
|
||||
}
|
@ -1,41 +0,0 @@
|
||||
[package]
|
||||
name = "web-actor"
|
||||
version = "0.1.0"
|
||||
authors = ["Adrian Wozniak <adrian.wozniak@ita-prog.pl>"]
|
||||
edition = "2018"
|
||||
description = "BITQUE (Simplified JIRA in Rust) shared data types"
|
||||
repository = "https://gitlab.com/adrian.wozniak/bitque"
|
||||
license = "MPL-2.0"
|
||||
#license-file = "../LICENSE"
|
||||
|
||||
[lib]
|
||||
name = "web_actor"
|
||||
path = "./src/lib.rs"
|
||||
|
||||
[features]
|
||||
local-storage = ["filesystem-actor"]
|
||||
cloud-storage = ["cloud-storage-actor"]
|
||||
default = ["local-storage"]
|
||||
|
||||
[dependencies]
|
||||
actix = { version = "0.13.0" }
|
||||
actix-multipart = { version = "*" }
|
||||
actix-web = { version = "4" }
|
||||
actix-web-actors = { version = "4" }
|
||||
cloud-storage-actor = { workspace = true, optional = true }
|
||||
bincode = { version = "*" }
|
||||
bitque-config = { workspace = true, features = ["mail", "web", "local-storage"] }
|
||||
bitque-data = { workspace = true, features = ["backend"] }
|
||||
bytes = { version = "1" }
|
||||
database-actor = { workspace = true }
|
||||
filesystem-actor = { workspace = true, optional = true }
|
||||
futures = { version = "0.3.8" }
|
||||
libc = { version = "0.2.0", default-features = false }
|
||||
mail-actor = { workspace = true }
|
||||
openssl-sys = { version = "*", features = ["vendored"] }
|
||||
serde = { version = "*" }
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
toml = { version = "*" }
|
||||
tracing = { version = "0.1.37" }
|
||||
uuid = { version = "1.3.0", features = ["serde", "v4", "v5"] }
|
||||
websocket-actor = { workspace = true }
|
Binary file not shown.
Before Width: | Height: | Size: 3.2 MiB |
@ -1,202 +0,0 @@
|
||||
use std::io::Write;
|
||||
|
||||
use actix::Addr;
|
||||
use actix_multipart::{Field, Multipart};
|
||||
use actix_web::web::Data;
|
||||
use actix_web::{post, web, Error, HttpResponse};
|
||||
use bitque_data::msg::WsMsgUser;
|
||||
use bitque_data::{User, UserId};
|
||||
use database_actor::authorize_user::AuthorizeUser;
|
||||
use database_actor::user_projects::CurrentUserProject;
|
||||
use database_actor::users::UpdateAvatarUrl;
|
||||
use database_actor::DbExecutor;
|
||||
use futures::executor::block_on;
|
||||
use futures::{StreamExt, TryStreamExt};
|
||||
use tracing::{error, warn};
|
||||
use websocket_actor::server::InnerMsg::BroadcastToChannel;
|
||||
use websocket_actor::server::WsServer;
|
||||
|
||||
use crate::ServiceError;
|
||||
|
||||
#[cfg(feature = "cloud-storage")]
|
||||
#[post("/")]
|
||||
pub async fn upload(
|
||||
mut payload: Multipart,
|
||||
db: Data<Addr<DbExecutor>>,
|
||||
ws: Data<Addr<WsServer>>,
|
||||
fs: Data<Addr<filesystem_actor::LocalStorageExecutor>>,
|
||||
cloud_storage: Data<Addr<cloud_storage_actor::CloudStorageExecutor>>,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
let mut user_id: Option<UserId> = None;
|
||||
let mut avatar_url: Option<String> = None;
|
||||
|
||||
while let Ok(Some(field)) = payload.try_next().await {
|
||||
let disposition = field.content_disposition();
|
||||
if !disposition.is_form_data() {
|
||||
return Ok(HttpResponse::BadRequest().finish());
|
||||
}
|
||||
match disposition.get_name() {
|
||||
Some("token") => {
|
||||
user_id = Some(handle_token(field, db.clone()).await?);
|
||||
}
|
||||
Some("avatar") => {
|
||||
let Some(id) = user_id else {
|
||||
warn!("user id not found. Not authorized");
|
||||
return Ok(ServiceError::Unauthorized.into());
|
||||
};
|
||||
avatar_url = Some(
|
||||
crate::handlers::upload_avatar_image::handle_image(
|
||||
id,
|
||||
field,
|
||||
fs.clone(),
|
||||
cloud_storage.clone(),
|
||||
)
|
||||
.await?,
|
||||
);
|
||||
}
|
||||
_ => continue,
|
||||
};
|
||||
}
|
||||
|
||||
tracing::info!("user_id {user_id:?}");
|
||||
tracing::info!("token {avatar_url:?}");
|
||||
|
||||
let user_id = match user_id {
|
||||
Some(id) => id,
|
||||
_ => return Ok(HttpResponse::Unauthorized().finish()),
|
||||
};
|
||||
|
||||
let project_id = match block_on(db.send(CurrentUserProject { user_id })) {
|
||||
Ok(Ok(user_project)) => user_project.project_id,
|
||||
_ => return Ok(HttpResponse::UnprocessableEntity().finish()),
|
||||
};
|
||||
|
||||
match (user_id, avatar_url) {
|
||||
(user_id, Some(avatar_url)) => {
|
||||
let user = update_user_avatar(user_id, avatar_url.clone(), db).await?;
|
||||
let Ok(_) = ws.send(BroadcastToChannel(
|
||||
project_id,
|
||||
WsMsgUser::AvatarUrlChanged(user.id, avatar_url).into(),
|
||||
))
|
||||
.await else {
|
||||
return Ok(HttpResponse::UnprocessableEntity().finish());
|
||||
};
|
||||
Ok(HttpResponse::NoContent().finish())
|
||||
}
|
||||
_ => Ok(HttpResponse::UnprocessableEntity().finish()),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "cloud-storage"))]
|
||||
#[post("/")]
|
||||
pub async fn upload(
|
||||
mut payload: Multipart,
|
||||
db: Data<Addr<DbExecutor>>,
|
||||
ws: Data<Addr<WsServer>>,
|
||||
fs: Data<Addr<filesystem_actor::LocalStorageExecutor>>,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
let mut user_id: Option<UserId> = None;
|
||||
let mut avatar_url: Option<String> = None;
|
||||
|
||||
while let Ok(Some(field)) = payload.try_next().await {
|
||||
let disposition = field.content_disposition();
|
||||
if !disposition.is_form_data() {
|
||||
return Ok(HttpResponse::BadRequest().finish());
|
||||
}
|
||||
match disposition.get_name() {
|
||||
Some("token") => {
|
||||
user_id = Some(handle_token(field, db.clone()).await?);
|
||||
}
|
||||
Some("avatar") => {
|
||||
let Some(id) = user_id else { return Ok(HttpResponse::Unauthorized().finish()); };
|
||||
avatar_url = Some(
|
||||
crate::handlers::upload_avatar_image::handle_image(id, field, fs.clone())
|
||||
.await?,
|
||||
);
|
||||
}
|
||||
_ => continue,
|
||||
};
|
||||
}
|
||||
let user_id = match user_id {
|
||||
Some(id) => id,
|
||||
_ => return Ok(HttpResponse::Unauthorized().finish()),
|
||||
};
|
||||
|
||||
let project_id = match block_on(db.send(CurrentUserProject { user_id })) {
|
||||
Ok(Ok(user_project)) => user_project.project_id,
|
||||
_ => return Ok(HttpResponse::UnprocessableEntity().finish()),
|
||||
};
|
||||
|
||||
match (user_id, avatar_url) {
|
||||
(user_id, Some(avatar_url)) => {
|
||||
let user = update_user_avatar(user_id, avatar_url.clone(), db).await?;
|
||||
if ws
|
||||
.send(BroadcastToChannel(
|
||||
project_id,
|
||||
WsMsg::User(WsMsgUser::AvatarUrlChanged(user.id, avatar_url)),
|
||||
))
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
return Ok(HttpResponse::UnprocessableEntity().finish());
|
||||
};
|
||||
Ok(HttpResponse::NoContent().finish())
|
||||
}
|
||||
_ => Ok(HttpResponse::UnprocessableEntity().finish()),
|
||||
}
|
||||
}
|
||||
|
||||
async fn update_user_avatar(
|
||||
user_id: UserId,
|
||||
new_url: String,
|
||||
db: Data<Addr<DbExecutor>>,
|
||||
) -> Result<User, Error> {
|
||||
match db
|
||||
.send(UpdateAvatarUrl {
|
||||
user_id,
|
||||
avatar_url: Some(new_url),
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(Ok(user)) => Ok(user),
|
||||
|
||||
Ok(Err(e)) => {
|
||||
error!("{:?}", e);
|
||||
Err(ServiceError::Unauthorized.into())
|
||||
}
|
||||
Err(e) => {
|
||||
error!("{:?}", e);
|
||||
Err(ServiceError::Unauthorized.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_token(mut field: Field, db: Data<Addr<DbExecutor>>) -> Result<UserId, Error> {
|
||||
let mut f: Vec<u8> = vec![];
|
||||
while let Some(chunk) = field.next().await {
|
||||
let data = chunk.unwrap();
|
||||
f = web::block(move || {
|
||||
if let Err(e) = f.write_all(&data) {
|
||||
error!("{e}");
|
||||
}
|
||||
f
|
||||
})
|
||||
.await?;
|
||||
}
|
||||
let access_token = String::from_utf8(f)
|
||||
.unwrap_or_default()
|
||||
.parse::<uuid::Uuid>()
|
||||
.map_err(|_| ServiceError::Unauthorized)?;
|
||||
match db.send(AuthorizeUser { access_token }).await {
|
||||
Ok(Ok(user)) => Ok(user.id),
|
||||
|
||||
Ok(Err(e)) => {
|
||||
error!("{:?}", e);
|
||||
Err(ServiceError::Unauthorized.into())
|
||||
}
|
||||
Err(e) => {
|
||||
error!("{:?}", e);
|
||||
Err(ServiceError::Unauthorized.into())
|
||||
}
|
||||
}
|
||||
}
|
@ -1,257 +0,0 @@
|
||||
use actix::{spawn, Addr};
|
||||
use actix_multipart::Field;
|
||||
use actix_web::web::Data;
|
||||
use actix_web::Error;
|
||||
use bitque_data::UserId;
|
||||
use bytes::Bytes;
|
||||
use futures::future::join_all;
|
||||
use futures::StreamExt;
|
||||
use tokio::sync::broadcast::{Receiver, Sender};
|
||||
use tracing::error;
|
||||
|
||||
use crate::{AvatarError, ServiceError};
|
||||
|
||||
pub trait UploadField {
|
||||
fn name(&self) -> &str;
|
||||
|
||||
/// Read file from client
|
||||
async fn read_content(&mut self, sender: Sender<Bytes>);
|
||||
}
|
||||
|
||||
impl UploadField for Field {
|
||||
fn name(&self) -> &str {
|
||||
self.content_disposition().get_filename().unwrap()
|
||||
}
|
||||
|
||||
async fn read_content(&mut self, sender: Sender<Bytes>) {
|
||||
while let Some(chunk) = self.next().await {
|
||||
let data = chunk.unwrap();
|
||||
if let Err(err) = sender.send(data) {
|
||||
error!("{:?}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "local-storage", feature = "cloud-storage"))]
|
||||
pub(crate) async fn handle_image(
|
||||
user_id: UserId,
|
||||
mut field: impl UploadField,
|
||||
local_storage: Data<Addr<filesystem_actor::LocalStorageExecutor>>,
|
||||
cloud_storage: Data<Addr<cloud_storage_actor::CloudStorageExecutor>>,
|
||||
) -> Result<String, Error> {
|
||||
let filename = field.name();
|
||||
let system_file_name = format!("{}-{}", user_id, filename);
|
||||
|
||||
let (sender, receiver) = tokio::sync::broadcast::channel(64);
|
||||
|
||||
let results_fut = join_all([
|
||||
spawn(local_storage_write(
|
||||
system_file_name.clone(),
|
||||
local_storage,
|
||||
user_id,
|
||||
sender.subscribe(),
|
||||
)),
|
||||
spawn(cloud_storage_write(
|
||||
filename.to_string(),
|
||||
cloud_storage,
|
||||
format!("user_{user_id}"),
|
||||
receiver,
|
||||
)),
|
||||
]);
|
||||
let (_, results) = tokio::join!(field.read_content(sender), results_fut);
|
||||
|
||||
for res in results {
|
||||
return if let Ok(Some(link)) = res {
|
||||
Ok(link)
|
||||
} else {
|
||||
Err(ServiceError::Avatar(AvatarError::Upload).into())
|
||||
};
|
||||
}
|
||||
Err(ServiceError::Avatar(AvatarError::Upload).into())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod local_and_cloud {
|
||||
use std::borrow::Cow;
|
||||
|
||||
use actix::SyncArbiter;
|
||||
use actix_web::web::Data;
|
||||
use bytes::Bytes;
|
||||
use tokio::spawn;
|
||||
use tokio::sync::broadcast::Sender;
|
||||
|
||||
use crate::handlers::upload_avatar_image::{handle_image, UploadField};
|
||||
|
||||
pub struct FieldMock<'s>(Cow<'s, [u8]>);
|
||||
|
||||
impl<'s> UploadField for FieldMock<'s> {
|
||||
fn name(&self) -> &str {
|
||||
"foo.bar"
|
||||
}
|
||||
|
||||
async fn read_content(&mut self, sender: Sender<Bytes>) {
|
||||
loop {
|
||||
if self.0.is_empty() {
|
||||
break;
|
||||
}
|
||||
let len = self.0.len().min(30);
|
||||
let slice = self.0[0..len].to_vec();
|
||||
self.0 = self.0[len..].to_vec().into();
|
||||
sender.send(Bytes::copy_from_slice(&slice)).unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[actix::test]
|
||||
async fn asset_test_mock() {
|
||||
let v = (0..255).into_iter().collect::<Vec<_>>();
|
||||
let mut field = FieldMock(v.into());
|
||||
let (tx, mut rx) = tokio::sync::broadcast::channel::<Bytes>(64);
|
||||
|
||||
let read = async {
|
||||
spawn(async move {
|
||||
let mut len = 0;
|
||||
while let Ok(bytes) = rx.recv().await {
|
||||
len += bytes.len();
|
||||
}
|
||||
len
|
||||
})
|
||||
.await
|
||||
};
|
||||
let write = async move {
|
||||
spawn(async move {
|
||||
field.read_content(tx).await;
|
||||
})
|
||||
.await
|
||||
};
|
||||
let (_, len) = tokio::join!(write, read);
|
||||
assert_eq!(len.unwrap(), 255);
|
||||
}
|
||||
|
||||
#[actix::test]
|
||||
async fn large_image() {
|
||||
let field = FieldMock(Cow::Borrowed(include_bytes!(
|
||||
"../../assets/LotC_Wallpaper_2560x1440.jpg"
|
||||
)));
|
||||
|
||||
let local_storage = Data::new(SyncArbiter::start(
|
||||
1,
|
||||
filesystem_actor::LocalStorageExecutor::default,
|
||||
));
|
||||
let cloud_storage = Data::new(SyncArbiter::start(
|
||||
1,
|
||||
cloud_storage_actor::CloudStorageExecutor::default,
|
||||
));
|
||||
|
||||
let res = handle_image(0, field, local_storage, cloud_storage).await;
|
||||
eprintln!("{res:#?}");
|
||||
res.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(not(feature = "local-storage"), feature = "cloud-storage"))]
|
||||
pub(crate) async fn handle_image(
|
||||
user_id: UserId,
|
||||
mut field: impl UploadField,
|
||||
cloud_storage: Data<Addr<cloud_storage_actor::CloudStorageExecutor>>,
|
||||
) -> Result<String, Error> {
|
||||
let filename = field.name();
|
||||
let system_file_name = format!("{}-{}", user_id, filename);
|
||||
|
||||
let (sender, receiver) = tokio::sync::broadcast::channel(64);
|
||||
|
||||
let aws_fut = cloud - storage_write(system_file_name, cloud_storage, receiver);
|
||||
let read_fut = field.read_content(sender);
|
||||
|
||||
let aws_join = tokio::task::spawn(aws_fut);
|
||||
read_fut.await;
|
||||
|
||||
let mut new_link = None;
|
||||
|
||||
if let Ok(url) = aws_join.await {
|
||||
new_link = url;
|
||||
}
|
||||
|
||||
Ok(new_link.unwrap_or_default())
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "local-storage", not(feature = "cloud-storage")))]
|
||||
pub(crate) async fn handle_image(
|
||||
user_id: UserId,
|
||||
mut field: impl UploadField,
|
||||
fs: Data<Addr<filesystem_actor::LocalStorageExecutor>>,
|
||||
) -> Result<String, Error> {
|
||||
let filename = field.name();
|
||||
let system_file_name = format!("{}-{}", user_id, filename);
|
||||
|
||||
let (sender, _receiver) = tokio::sync::broadcast::channel(64);
|
||||
|
||||
let fs_fut = local_storage_write(system_file_name, fs, user_id, sender.subscribe());
|
||||
let read_fut = field.read_content(sender);
|
||||
|
||||
let fs_join = tokio::task::spawn(fs_fut);
|
||||
read_fut.await;
|
||||
|
||||
let mut new_link = None;
|
||||
|
||||
if let Ok(url) = fs_join.await {
|
||||
new_link = url;
|
||||
}
|
||||
|
||||
Ok(new_link.unwrap_or_default())
|
||||
}
|
||||
|
||||
/// Stream bytes directly to Cloud Storage Service
|
||||
#[cfg(feature = "cloud-storage")]
|
||||
async fn cloud_storage_write(
|
||||
system_file_name: String,
|
||||
cloud_storage: Data<Addr<cloud_storage_actor::CloudStorageExecutor>>,
|
||||
dir: String,
|
||||
receiver: Receiver<Bytes>,
|
||||
) -> Option<String> {
|
||||
let s3 = bitque_config::cloud_storage::config();
|
||||
if !s3.active {
|
||||
return None;
|
||||
}
|
||||
|
||||
match cloud_storage
|
||||
.send(cloud_storage_actor::PutObject {
|
||||
source: receiver,
|
||||
file_name: system_file_name.to_string(),
|
||||
dir,
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(Ok(s)) => Some(s),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "local-storage")]
|
||||
async fn local_storage_write(
|
||||
system_file_name: String,
|
||||
fs: Data<Addr<filesystem_actor::LocalStorageExecutor>>,
|
||||
_user_id: UserId,
|
||||
receiver: Receiver<Bytes>,
|
||||
) -> Option<String> {
|
||||
let web_config = bitque_config::web::config();
|
||||
let fs_config = bitque_config::fs::config();
|
||||
|
||||
match fs
|
||||
.send(filesystem_actor::CreateFile {
|
||||
source: receiver,
|
||||
file_name: system_file_name.to_string(),
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(Ok(_)) => Some(format!(
|
||||
"{addr}{client_path}/{filename}",
|
||||
addr = web_config.full_addr(),
|
||||
client_path = fs_config.client_path,
|
||||
filename = system_file_name
|
||||
)),
|
||||
Ok(_) => None,
|
||||
_ => None,
|
||||
}
|
||||
}
|
@ -1,17 +0,0 @@
|
||||
use actix_web::http::header::{self, HeaderMap};
|
||||
|
||||
pub fn token_from_headers(headers: &HeaderMap) -> Result<uuid::Uuid, crate::errors::ServiceError> {
|
||||
headers
|
||||
.get(header::AUTHORIZATION)
|
||||
.ok_or(crate::errors::ServiceError::Unauthorized)
|
||||
.map(|h| h.to_str().unwrap_or_default())
|
||||
.and_then(|s| parse_bearer(s))
|
||||
}
|
||||
|
||||
fn parse_bearer(header: &str) -> Result<uuid::Uuid, crate::errors::ServiceError> {
|
||||
if !header.starts_with("Bearer ") {
|
||||
return Err(crate::errors::ServiceError::Unauthorized);
|
||||
}
|
||||
let (_bearer, token) = header.split_at(7);
|
||||
uuid::Uuid::parse_str(token).map_err(|_e| crate::errors::ServiceError::Unauthorized)
|
||||
}
|
@ -1,29 +0,0 @@
|
||||
FROM archlinux:latest
|
||||
|
||||
RUN pacman -Sy rustup gcc which --noconfirm
|
||||
|
||||
WORKDIR /app/
|
||||
|
||||
RUN rustup toolchain install nightly && \
|
||||
rustup default nightly && \
|
||||
curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
|
||||
|
||||
ADD ./bitque-data /app/bitque-data
|
||||
|
||||
ADD ./bitque-client /app/bitque-client
|
||||
|
||||
RUN cd ./bitque-client && \
|
||||
rm -Rf build && \
|
||||
mkdir build && \
|
||||
wasm-pack build --mode normal --release --out-name bitque --out-dir ./build --target web && \
|
||||
cp -r ./static/* ./build && \
|
||||
cat ./static/index.js \
|
||||
| sed -e "s/process.env.BITQUE_SERVER_BIND/'$BITQUE_SERVER_BIND'/g" \
|
||||
| sed -e "s/process.env.BITQUE_SERVER_PORT/'$BITQUE_SERVER_PORT'/g" &> ./build/index.js && \
|
||||
cp ./js/template.html ./build/index.html && \
|
||||
mkdir -p /assets && \
|
||||
cp -r ./build/* /assets
|
||||
|
||||
CMD cat /app/bitque-client/static/index.js \
|
||||
| sed -e "s/process.env.BITQUE_SERVER_BIND/'$BITQUE_SERVER_BIND'/g" \
|
||||
| sed -e "s/process.env.BITQUE_SERVER_PORT/'$BITQUE_SERVER_PORT'/g" &> /assets/index.js
|
@ -1,20 +0,0 @@
|
||||
[build]
|
||||
target = "./index.html"
|
||||
|
||||
[watch]
|
||||
ignore = [
|
||||
"tmp",
|
||||
]
|
||||
|
||||
[[hooks]]
|
||||
stage = "build"
|
||||
command = "zsh"
|
||||
command_arguments = ['./scripts/compile-css.sh']
|
||||
|
||||
[[proxy]]
|
||||
rewrite = "/ws"
|
||||
backend = "http://0.0.0.0:5000"
|
||||
|
||||
[[proxy]]
|
||||
rewrite = "/avatar"
|
||||
backend = "http://0.0.0.0:5000/avatar"
|
@ -1,51 +0,0 @@
|
||||
<svg xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640" width="42"
|
||||
height="42">
|
||||
<defs>
|
||||
<path d="M500 300c0 110.38-89.62 200-200 200s-200-89.62-200-200c0-110.39 89.62-200 200-200s200 89.61 200 200Z"
|
||||
id="a"/>
|
||||
<path d="M520 338.18c0 110.38-89.62 200-200 200s-200-89.62-200-200 89.62-200 200-200 200 89.62 200 200Z"
|
||||
id="b"/>
|
||||
<path d="M543.03 374.84c0 110.39-89.62 200-200 200s-200-89.61-200-200c0-110.38 89.62-200 200-200s200 89.62 200 200Z"
|
||||
id="c"/>
|
||||
<mask id="e" x="78" y="78" width="600" height="600" maskUnits="userSpaceOnUse">
|
||||
<path fill="#fff" d="M78 78h444v444H78z"/>
|
||||
<use xlink:href="#a" opacity=".46"/>
|
||||
</mask>
|
||||
<mask id="f" x="98" y="116.18" width="444" height="444" maskUnits="userSpaceOnUse">
|
||||
<path fill="#fff" d="M98 116.18h444v444H98z"/>
|
||||
<use xlink:href="#b" opacity=".46"/>
|
||||
</mask>
|
||||
<mask id="g" x="121.03" y="152.84" width="444" height="444" maskUnits="userSpaceOnUse">
|
||||
<path fill="#fff" d="M121.03 152.84h444v444h-444z"/>
|
||||
<use xlink:href="#c" opacity=".46"/>
|
||||
</mask>
|
||||
<filter id="d">
|
||||
<feFlood/>
|
||||
<feComposite in2="SourceAlpha" operator="in"/>
|
||||
<feGaussianBlur stdDeviation="1"/>
|
||||
<feOffset dx="14" dy="10" result="afterOffset"/>
|
||||
<feFlood flood-color="#0d0e44" flood-opacity=".5"/>
|
||||
<feComposite in2="afterOffset" operator="in"/>
|
||||
<feMorphology operator="dilate" radius="1"/>
|
||||
<feComposite in2="SourceAlpha" operator="out"/>
|
||||
</filter>
|
||||
</defs>
|
||||
<path d="M500 300c0 110.38-89.62 200-200 200s-200-89.62-200-200c0-110.39 89.62-200 200-200s200 89.61 200 200Z"
|
||||
filter="url(#d)"/>
|
||||
<use xlink:href="#a" opacity=".46" fill="#fefefe"/>
|
||||
<g mask="url(#e)">
|
||||
<use xlink:href="#a" opacity=".46" fill-opacity="0" stroke="#06697d" stroke-width="44"/>
|
||||
</g>
|
||||
<path d="M520 338.18c0 110.38-89.62 200-200 200s-200-89.62-200-200 89.62-200 200-200 200 89.62 200 200Z"
|
||||
filter="url(#d)"/>
|
||||
<use xlink:href="#b" opacity=".46" fill="#fefefe"/>
|
||||
<g mask="url(#f)">
|
||||
<use xlink:href="#b" opacity=".46" fill-opacity="0" stroke="#06697d" stroke-width="44"/>
|
||||
</g>
|
||||
<path d="M543.03 374.84c0 110.39-89.62 200-200 200s-200-89.61-200-200c0-110.38 89.62-200 200-200s200 89.62 200 200Z"
|
||||
filter="url(#d)"/>
|
||||
<use xlink:href="#c" opacity=".46" fill="#fefefe"/>
|
||||
<g mask="url(#g)">
|
||||
<use xlink:href="#c" opacity=".46" fill-opacity="0" stroke="#06697d" stroke-width="44"/>
|
||||
</g>
|
||||
</svg>
|
Before Width: | Height: | Size: 2.6 KiB |
@ -1,79 +0,0 @@
|
||||
#epics {
|
||||
> section {
|
||||
max-width: 1024px;
|
||||
margin: 0 auto;
|
||||
|
||||
> h1 {
|
||||
font-family: var(--font-bold);
|
||||
}
|
||||
|
||||
> .description {
|
||||
font-family: var(--font-regular);
|
||||
margin: {
|
||||
top: 16px;
|
||||
bottom: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
> ul.epicsList {
|
||||
margin: 0;
|
||||
margin: {
|
||||
top: 5px;
|
||||
}
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
|
||||
|
||||
> li.epic {
|
||||
padding: 0;
|
||||
margin: 5px 0 0;
|
||||
|
||||
> .firstRow {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin: {
|
||||
top: 5px;
|
||||
bottom: 5px;
|
||||
};
|
||||
|
||||
> .epicName {
|
||||
font-family: var(--font-black);
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
> .date {
|
||||
width: 400px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
> .counter {
|
||||
//
|
||||
}
|
||||
}
|
||||
|
||||
> .secondRow {
|
||||
margin: {
|
||||
top: 5px;
|
||||
bottom: 5px;
|
||||
left: 20px;
|
||||
right: 20px;
|
||||
};
|
||||
|
||||
> .issues {
|
||||
> .issue {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
> .flags {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
width: 40px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,233 +0,0 @@
|
||||
#issuesAndFilters {
|
||||
display: block;
|
||||
|
||||
> .filters {
|
||||
margin: {
|
||||
top: 5px;
|
||||
bottom: 5px;
|
||||
}
|
||||
|
||||
> .pseudoInput {
|
||||
display: flex;
|
||||
|
||||
> .part {
|
||||
display: flex;
|
||||
line-height: 24px;
|
||||
background: var(--borderLightest);
|
||||
padding: 8px 14px;
|
||||
cursor: pointer;
|
||||
margin: {
|
||||
left: 5px;
|
||||
right: 5px;
|
||||
}
|
||||
|
||||
> span {
|
||||
word-wrap: break-spaces;
|
||||
word-break: keep-all;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
> .styledButton {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
> .styledButton {
|
||||
margin: {
|
||||
left: 5px;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
};
|
||||
font-size: 12px;
|
||||
line-height: 24px;
|
||||
background: var(--borderLightest);
|
||||
|
||||
> .styledIcon {
|
||||
color: var(----secondary);
|
||||
background: var(--borderLightest);
|
||||
}
|
||||
|
||||
display: none;
|
||||
|
||||
&:hover {
|
||||
color: var(--borderInputFocus);
|
||||
background: var(--borderLightest);
|
||||
|
||||
> .styledIcon {
|
||||
color: var(----secondary);
|
||||
background: var(--borderLightest);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .styledIcon {
|
||||
line-height: 39px;
|
||||
width: 24px;
|
||||
}
|
||||
|
||||
> .styledSelect {
|
||||
display: inline;
|
||||
min-width: 20px;
|
||||
|
||||
> .valueContainer {
|
||||
min-height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
--listWidth: 338px;
|
||||
|
||||
> .issueInfo {
|
||||
width: calc(100% - var(--listWidth));
|
||||
padding: {
|
||||
left: 10px;
|
||||
right: 10px;
|
||||
top: 5px;
|
||||
bottom: 5px;
|
||||
};
|
||||
|
||||
> .header {
|
||||
display: grid;
|
||||
grid-template-areas: "icon link" "icon name";
|
||||
grid-template-columns: 48px;
|
||||
|
||||
> .logo {
|
||||
width: 48px;
|
||||
}
|
||||
|
||||
> .path {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
> span {
|
||||
margin: 0 5px;
|
||||
font-family: var(--font-medium);
|
||||
height: 32px;
|
||||
vertical-align: middle;
|
||||
line-height: 2;
|
||||
appearance: none;
|
||||
cursor: none;
|
||||
user-select: none;
|
||||
font-size: 14.5px;
|
||||
}
|
||||
|
||||
> .styledLink, > span {
|
||||
color: var(--textLink);
|
||||
|
||||
> span {
|
||||
color: var(--textLink);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .title {
|
||||
}
|
||||
}
|
||||
|
||||
> .issueBody {
|
||||
display: flex;
|
||||
|
||||
> .details {
|
||||
list-style: none;
|
||||
|
||||
> .line {
|
||||
--lineWidth: 460px;
|
||||
--nameWidth: 150px;
|
||||
|
||||
list-style: none;
|
||||
width: var(--lineWidth);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: {
|
||||
top: 2px;
|
||||
bottom: 2px;
|
||||
left: 5px;
|
||||
right: 5px;
|
||||
};
|
||||
font-size: 14px;
|
||||
|
||||
> .detailsTitle {
|
||||
color: var(--textLight);
|
||||
width: var(--nameWidth);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
> .detailsValue {
|
||||
width: calc(var(--lineWidth) - var(--nameWidth));
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .description {
|
||||
padding: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
> .issuesList {
|
||||
width: var(--listWidth);
|
||||
list-style: none;
|
||||
|
||||
> .listItem {
|
||||
list-style: none;
|
||||
|
||||
> .issue {
|
||||
display: grid;
|
||||
grid-template-areas: "type number" "priority name";
|
||||
grid-template-columns: 32px auto;
|
||||
border: {
|
||||
bottom: 1px solid var(--borderLight);
|
||||
}
|
||||
padding: {
|
||||
left: 10px;
|
||||
right: 10px;
|
||||
top: 5px;
|
||||
bottom: 5px;
|
||||
};
|
||||
cursor: pointer;
|
||||
|
||||
&.active {
|
||||
background-color: var(--backgroundLightPrimary);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: var(--backgroundLightest);
|
||||
|
||||
&.active {
|
||||
background-color: var(--backgroundLightPrimary);
|
||||
}
|
||||
}
|
||||
|
||||
> .type {
|
||||
grid-area: type;
|
||||
}
|
||||
|
||||
> .number {
|
||||
grid-area: number;
|
||||
|
||||
> .issueLink {
|
||||
display: inline;
|
||||
cursor: alias;
|
||||
}
|
||||
}
|
||||
|
||||
> .priority {
|
||||
grid-area: priority;
|
||||
}
|
||||
|
||||
> .name {
|
||||
grid-area: name;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
#profile {
|
||||
> .formContainer {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
.styledForm {
|
||||
max-width: 1024px;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,127 +0,0 @@
|
||||
.styledEditor {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
flex-wrap: wrap;
|
||||
max-width: 100%;
|
||||
|
||||
> input[type="radio"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
> .navbar {
|
||||
border: 1px solid var(--borderLight);
|
||||
border-top-left-radius: 5px;
|
||||
border-top-right-radius: 5px;
|
||||
font-family: var(--font-medium);
|
||||
font-weight: normal;
|
||||
text-align: center;
|
||||
height: 32px;
|
||||
vertical-align: middle;
|
||||
line-height: 2;
|
||||
white-space: nowrap;
|
||||
transition: all 0.1s;
|
||||
appearance: none;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
font-size: 14.5px;
|
||||
border-color: var(--borderInputFocus);
|
||||
|
||||
&:not(:hover) {
|
||||
border-color: var(--backgroundLightest);
|
||||
background-color: var(--borderLight);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: #fff;
|
||||
border: 1px solid var(--borderInputFocus);
|
||||
box-shadow: 0 0 0 1px var(--borderInputFocus);
|
||||
}
|
||||
}
|
||||
|
||||
> .navbar.activeTab {
|
||||
background-color: var(--backgroundLightest);
|
||||
border-color: var(--borderLight);
|
||||
}
|
||||
|
||||
> .navbar.editorTab {
|
||||
min-width: 50%;
|
||||
}
|
||||
|
||||
> .navbar.viewTab {
|
||||
min-width: 50%;
|
||||
}
|
||||
|
||||
> .styledTextArea {
|
||||
grid-area: view;
|
||||
display: none;
|
||||
}
|
||||
|
||||
> .view {
|
||||
min-width: 100%;
|
||||
display: none;
|
||||
min-height: 40px;
|
||||
padding-top: 15px;
|
||||
|
||||
ul {
|
||||
> li {
|
||||
list-style: disc;
|
||||
margin-left: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
ol {
|
||||
> li {
|
||||
list-style: decimal;
|
||||
margin-left: 28px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> input.editorRadio {
|
||||
&:checked {
|
||||
~ {
|
||||
.styledTextArea {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> input.viewRadio {
|
||||
&:checked {
|
||||
~ {
|
||||
.view {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.styledCheckbox.textEditorModeSwitcher {
|
||||
justify-content: end;
|
||||
margin-top: 5px;
|
||||
margin-bottom: 5px;
|
||||
|
||||
& > .styledCheckboxChild {
|
||||
&.mdonly, &.rteonly {
|
||||
display: flex;
|
||||
margin-left: 5px;
|
||||
border: none;
|
||||
color: var(--textDark);
|
||||
|
||||
&.selected {
|
||||
color: var(--borderInputFocus);
|
||||
|
||||
.styledIcon {
|
||||
color: var(--borderInputFocus);
|
||||
}
|
||||
}
|
||||
|
||||
.styledIcon {
|
||||
font-size: 24px;
|
||||
color: var(--textDark);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user