master
veypi 1 year ago
parent 2563324bb5
commit f841f2599f

@ -29,3 +29,8 @@ build:
syncDB: syncDB:
@scp -P 19529 oa.db root@alco.host:/root/ @scp -P 19529 oa.db root@alco.host:/root/
buildweb:
@cd oaweb && yarn build
@rm -rf ./oab/dist/*
@mv ./oaweb/dist/spa/* ./oab/dist/

55
oab/Cargo.lock generated

@ -1914,8 +1914,10 @@ dependencies = [
"include_dir", "include_dir",
"jsonwebtoken", "jsonwebtoken",
"lazy_static", "lazy_static",
"mime_guess",
"proc", "proc",
"rand", "rand",
"rust-embed",
"sea-orm", "sea-orm",
"serde", "serde",
"serde-big-array", "serde-big-array",
@ -2445,6 +2447,40 @@ dependencies = [
"zeroize", "zeroize",
] ]
[[package]]
name = "rust-embed"
version = "8.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1e7d90385b59f0a6bf3d3b757f3ca4ece2048265d70db20a2016043d4509a40"
dependencies = [
"rust-embed-impl",
"rust-embed-utils",
"walkdir",
]
[[package]]
name = "rust-embed-impl"
version = "8.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c3d8c6fd84090ae348e63a84336b112b5c3918b3bf0493a581f7bd8ee623c29"
dependencies = [
"proc-macro2",
"quote",
"rust-embed-utils",
"syn 2.0.37",
"walkdir",
]
[[package]]
name = "rust-embed-utils"
version = "8.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "873feff8cb7bf86fdf0a71bb21c95159f4e4a37dd7a4bd1855a940909b583ada"
dependencies = [
"sha2",
"walkdir",
]
[[package]] [[package]]
name = "rust_decimal" name = "rust_decimal"
version = "1.32.0" version = "1.32.0"
@ -2538,6 +2574,15 @@ version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741"
[[package]]
name = "same-file"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
dependencies = [
"winapi-util",
]
[[package]] [[package]]
name = "scopeguard" name = "scopeguard"
version = "1.2.0" version = "1.2.0"
@ -3637,6 +3682,16 @@ version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
[[package]]
name = "walkdir"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee"
dependencies = [
"same-file",
"winapi-util",
]
[[package]] [[package]]
name = "wasi" name = "wasi"
version = "0.11.0+wasi-snapshot-preview1" version = "0.11.0+wasi-snapshot-preview1"

@ -53,4 +53,6 @@ http = "0.2.9"
http-auth-basic = "0.3.3" http-auth-basic = "0.3.3"
actix-multipart = "0.6.1" actix-multipart = "0.6.1"
actix-cors = "0.6.4" actix-cors = "0.6.4"
rust-embed = "8.0.0"
mime_guess = "2.0.4"

@ -0,0 +1,3 @@
<!DOCTYPE html><html><head><title>OA</title><meta charset=utf-8><meta name=description content=oneauth><meta name=format-detection content="telephone=no"><meta name=msapplication-tap-highlight content=no><meta name=viewport content="user-scalable=no,initial-scale=1,maximum-scale=1,minimum-scale=1,width=device-width"><link rel=icon type=image/ico href="/favicon.ico"> <script type="module" crossorigin src="/assets/index.c16e83bc.js"></script>
<link rel="stylesheet" href="/assets/index.30f45a7e.css">
</head><body><div id=v-msg></div><div id=q-app></div></body></html>

@ -9,9 +9,9 @@ use proc_macro::TokenStream;
use quote::{quote, ToTokens}; use quote::{quote, ToTokens};
use syn::{parse_macro_input, AttributeArgs, ItemFn}; use syn::{parse_macro_input, AttributeArgs, ItemFn};
mod access; mod access;
mod curd; mod crud;
use access::AccessWrap; use access::AccessWrap;
use curd::CrudWrap; use crud::CrudWrap;
#[proc_macro_attribute] #[proc_macro_attribute]
pub fn have_access(args: TokenStream, input: TokenStream) -> TokenStream { pub fn have_access(args: TokenStream, input: TokenStream) -> TokenStream {

@ -6,109 +6,108 @@
// //
// //
// //
use actix_web::{web, Responder}; use actix_web::{delete, get, patch, post, web, Responder};
use proc::crud_update; use proc::{access_create, access_delete, access_read, crud_update};
use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, TransactionTrait}; use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tracing::info;
use crate::{models::app, AppState, Error, Result}; use crate::{
models::{self},
AppState, Error, Result,
};
#[derive(Debug, Deserialize, Serialize, Clone)] #[derive(Debug, Deserialize, Serialize)]
pub struct UpdateOpt { pub struct Options {
pub name: Option<String>, name: Option<String>,
pub icon: Option<String>, role_id: Option<String>,
pub des: Option<String>, user_id: Option<String>,
pub join_method: Option<i32>, rid: Option<String>,
pub role_id: Option<String>, level: Option<bool>,
pub redirect: Option<String>,
pub status: Option<i32>,
} }
impl UpdateOpt {
// #[crud_update( #[get("/app/{aid}/access/")]
// app, #[access_read("app")]
// id = "Id", pub async fn list(
// name, aid: web::Path<String>,
// icon,
// des,
// join_method,
// role_id,
// redirect,
// status
// )]
pub async fn update(
id: web::Path<String>,
stat: web::Data<AppState>, stat: web::Data<AppState>,
data: web::Json<UpdateOpt>, query: web::Query<Options>,
) -> Result<impl Responder> { ) -> Result<impl Responder> {
let data = data.into_inner(); let aid = aid.into_inner();
let id = id.into_inner(); let mut q = models::access::Entity::find().filter(models::access::Column::AppId.eq(aid));
let obj = app::Entity::find_by_id(&id).one(stat.db()).await?; let query = query.into_inner();
let mut obj: app::ActiveModel = match obj { if let Some(rid) = query.role_id {
Some(o) => o.into(), q = q.filter(models::access::Column::RoleId.eq(rid));
None => return Err(Error::NotFound(id)),
}; };
if let Some(name) = data.name { if let Some(v) = query.name {
obj.name = sea_orm::Set(name) q = q.filter(models::access::Column::Name.eq(v));
}; };
let obj = obj.update(stat.db()).await?; if let Some(v) = query.user_id {
Ok(web::Json(obj)) q = q.filter(models::access::Column::UserId.eq(v));
} };
let aus = q.all(stat.db()).await?;
Ok(web::Json(aus))
} }
pub async fn update( #[derive(Debug, Deserialize, Serialize)]
id: web::Path<String>, pub struct CreateOptions {
name: String,
level: i32,
role_id: Option<String>,
user_id: Option<String>,
rid: Option<String>,
}
#[post("/app/{aid}/access/")]
#[access_create("app")]
pub async fn creat(
aid: web::Path<String>,
stat: web::Data<AppState>, stat: web::Data<AppState>,
data: web::Json<UpdateOpt>, query: web::Json<CreateOptions>,
) -> Result<impl Responder> { ) -> Result<impl Responder> {
let _id = &id.clone(); let aid = aid.into_inner();
let _data = data.clone(); let query = query.into_inner();
let _db = &stat.db().clone(); let obj = models::access::ActiveModel {
let f = || async move { app_id: sea_orm::ActiveValue::Set(aid),
let data = data.into_inner(); name: sea_orm::ActiveValue::Set(query.name),
let id = id.into_inner(); level: sea_orm::ActiveValue::Set(query.level),
let obj = app::Entity::find_by_id(&id).one(stat.db()).await?; role_id: sea_orm::ActiveValue::Set(query.role_id),
let mut obj: app::ActiveModel = match obj { user_id: sea_orm::ActiveValue::Set(query.user_id),
Some(o) => o.into(), rid: sea_orm::ActiveValue::Set(query.rid),
None => return Err(Error::NotFound(id)), ..Default::default()
};
if let Some(name) = data.name {
obj.name = sea_orm::Set(name)
}; };
let obj = obj.update(stat.db()).await?; let obj: models::access::Model = obj.insert(stat.db()).await?;
Ok(web::Json(obj)) Ok(web::Json(obj))
};
let res = f().await;
match res {
Err(e) => Err(e),
Ok(res) => {
let obj = crate::models::app::Entity::find_by_id(_id).one(_db).await?;
let mut obj: crate::models::app::ActiveModel = match obj {
Some(o) => o.into(),
None => return Err(Error::NotFound(_id.to_owned())),
};
if let Some(name) = _data.name {
obj.name = sea_orm::Set(name.into())
};
if let Some(icon) = _data.icon {
obj.icon = sea_orm::Set(icon.into())
};
if let Some(des) = _data.des {
obj.des = sea_orm::Set(des.into())
};
if let Some(join_method) = _data.join_method {
obj.join_method = sea_orm::Set(join_method.into())
};
if let Some(role_id) = _data.role_id {
obj.role_id = sea_orm::Set(role_id.into())
};
if let Some(redirect) = _data.redirect {
obj.redirect = sea_orm::Set(redirect.into())
};
if let Some(status) = _data.status {
obj.status = sea_orm::Set(status.into())
};
let obj = obj.update(_db).await?;
Ok(actix_web::web::Json(obj))
} }
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct UpdateOpt {
pub level: Option<i32>,
pub rid: Option<String>,
}
#[patch("/app/{aid}/access/{id}")]
#[access_delete("app")]
#[crud_update(access, AppId = "_id", Id = "_id", level, rid)]
pub async fn update(
id: web::Path<(String, String)>,
data: web::Json<UpdateOpt>,
stat: web::Data<AppState>,
) -> Result<impl Responder> {
Ok("")
} }
#[delete("/app/{aid}/access/{id}")]
#[access_delete("app")]
pub async fn delete(
params: web::Path<(String, String)>,
stat: web::Data<AppState>,
) -> Result<impl Responder> {
let (aid, rid) = params.into_inner();
let res = models::access::Entity::delete_many()
.filter(models::access::Column::AppId.eq(aid))
.filter(models::access::Column::Id.eq(rid))
.exec(stat.db())
.await?;
info!("{:#?}", res);
Ok("ok")
} }

@ -88,6 +88,7 @@ pub struct UpdateOpt {
pub join_method: Option<i32>, pub join_method: Option<i32>,
pub role_id: Option<String>, pub role_id: Option<String>,
pub redirect: Option<String>, pub redirect: Option<String>,
pub host: Option<String>,
pub status: Option<i32>, pub status: Option<i32>,
} }
@ -102,6 +103,7 @@ pub struct UpdateOpt {
join_method, join_method,
role_id, role_id,
redirect, redirect,
host,
status status
)] )]
pub async fn update( pub async fn update(

@ -21,6 +21,7 @@ pub fn routes(cfg: &mut web::ServiceConfig) {
.service(user::list) .service(user::list)
.service(user::register) .service(user::register)
.service(user::login) .service(user::login)
.service(user::update)
.service(user::delete); .service(user::delete);
cfg.service(app::get) cfg.service(app::get)
.service(app::list) .service(app::list)
@ -32,4 +33,19 @@ pub fn routes(cfg: &mut web::ServiceConfig) {
cfg.service(appuser::get) cfg.service(appuser::get)
.service(appuser::add) .service(appuser::add)
.service(appuser::update); .service(appuser::update);
cfg.service(access::list)
.service(access::creat)
.service(access::update)
.service(access::delete);
cfg.service(resource::list)
.service(resource::create)
.service(resource::update)
.service(resource::delete);
cfg.service(role::list)
.service(role::create)
.service(role::update)
.service(role::delete)
.service(role::add)
.service(role::drop);
} }

@ -4,3 +4,85 @@
// 2022-07-09 03:09 // 2022-07-09 03:09
// Distributed under terms of the Apache license. // Distributed under terms of the Apache license.
// //
use actix_web::{delete, get, patch, post, web, Responder};
use proc::{access_create, access_delete, access_read, crud_update};
use sea_orm::{
ActiveModelTrait, ColumnTrait, EntityTrait, LoaderTrait, QueryFilter, TransactionTrait,
};
use serde::{Deserialize, Serialize};
use tracing::info;
use crate::{
models::{self},
AppState, Error, Result,
};
#[get("/app/{aid}/resource/")]
#[access_read("app")]
pub async fn list(aid: web::Path<String>, stat: web::Data<AppState>) -> Result<impl Responder> {
let n = aid.into_inner();
if !n.is_empty() {
let s = models::resource::Entity::find()
.filter(models::resource::Column::AppId.eq(n))
.all(stat.db())
.await?;
Ok(web::Json(s))
} else {
Err(Error::Missing("id".to_string()))
}
}
#[derive(Debug, Deserialize, Serialize)]
pub struct CreateOpt {
name: String,
des: Option<String>,
}
#[post("/app/{aid}/resource/")]
#[access_create("app")]
pub async fn create(
aid: web::Path<String>,
stat: web::Data<AppState>,
data: web::Json<CreateOpt>,
) -> Result<impl Responder> {
let data = data.into_inner();
let obj = models::resource::ActiveModel {
name: sea_orm::ActiveValue::Set(data.name),
des: sea_orm::ActiveValue::Set(data.des),
app_id: sea_orm::ActiveValue::Set(aid.into_inner()),
..Default::default()
};
let obj = obj.insert(stat.db()).await?;
Ok(web::Json(obj))
}
#[delete("/app/{aid}/resource/{rid}")]
#[access_delete("app")]
pub async fn delete(
params: web::Path<(String, String)>,
stat: web::Data<AppState>,
) -> Result<impl Responder> {
let (aid, rid) = params.into_inner();
let res = models::resource::Entity::delete_many()
.filter(models::resource::Column::AppId.eq(aid))
.filter(models::resource::Column::Name.eq(rid))
.exec(stat.db())
.await?;
info!("{:#?}", res);
Ok("ok")
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct UpdateOpt {
pub des: Option<String>,
}
#[patch("/app/{aid}/resource/{rid}")]
#[access_delete("app")]
#[crud_update(resource, AppId = "_id", Name = "_id", des)]
pub async fn update(
id: web::Path<(String, String)>,
data: web::Json<UpdateOpt>,
stat: web::Data<AppState>,
) -> Result<impl Responder> {
Ok("")
}

@ -4,3 +4,141 @@
// 2022-07-09 03:10 // 2022-07-09 03:10
// Distributed under terms of the Apache license. // Distributed under terms of the Apache license.
// //
//
use actix_web::{delete, get, patch, post, web, Responder};
use proc::{access_create, access_delete, access_read, access_update, crud_update};
use sea_orm::{
ActiveModelTrait, ColumnTrait, ConnectionTrait, EntityTrait, LoaderTrait, QueryFilter,
TransactionTrait,
};
use serde::{Deserialize, Serialize};
use tracing::info;
use crate::{
libs,
models::{self},
AppState, Error, Result,
};
#[get("/app/{aid}/role/")]
#[access_read("app")]
pub async fn list(aid: web::Path<String>, stat: web::Data<AppState>) -> Result<impl Responder> {
let n = aid.into_inner();
if !n.is_empty() {
let s = models::role::Entity::find()
.filter(models::role::Column::AppId.eq(n))
.all(stat.db())
.await?;
Ok(web::Json(s))
} else {
Err(Error::Missing("id".to_string()))
}
}
#[derive(Debug, Deserialize, Serialize)]
pub struct CreateOpt {
name: String,
des: Option<String>,
}
#[post("/app/{aid}/role/")]
#[access_create("app")]
pub async fn create(
aid: web::Path<String>,
stat: web::Data<AppState>,
data: web::Json<CreateOpt>,
) -> Result<impl Responder> {
let data = data.into_inner();
let id = uuid::Uuid::new_v4().to_string().replace("-", "");
let obj = models::role::ActiveModel {
name: sea_orm::ActiveValue::Set(data.name),
id: sea_orm::ActiveValue::Set(id),
des: sea_orm::ActiveValue::Set(data.des),
app_id: sea_orm::ActiveValue::Set(aid.into_inner()),
..Default::default()
};
let obj = obj.insert(stat.db()).await?;
Ok(web::Json(obj))
}
#[delete("/app/{aid}/role/{rid}")]
#[access_delete("app", id = "&id.clone().0")]
pub async fn delete(
id: web::Path<(String, String)>,
stat: web::Data<AppState>,
) -> Result<impl Responder> {
let (aid, rid) = id.into_inner();
let res = models::role::Entity::delete_many()
.filter(models::role::Column::AppId.eq(aid))
.filter(models::role::Column::Id.eq(rid))
.exec(stat.db())
.await?;
info!("{:#?}", res);
Ok("ok")
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct UpdateOpt {
pub des: Option<String>,
}
#[patch("/app/{aid}/role/{rid}")]
#[access_update("app", id = "&id.clone().0")]
#[crud_update(role, AppId = "_id", Id = "_id", des)]
pub async fn update(
id: web::Path<(String, String)>,
data: web::Json<UpdateOpt>,
stat: web::Data<AppState>,
) -> Result<impl Responder> {
Ok("")
}
#[get("/app/{aid}/role/{rid}/user/{uid}")]
#[access_delete("app", id = "&id.clone().0")]
pub async fn add(
id: web::Path<(String, String, String)>,
stat: web::Data<AppState>,
) -> Result<impl Responder> {
let (_, rid, uid) = id.into_inner();
let s = models::user_role::ActiveModel {
user_id: sea_orm::ActiveValue::Set(uid),
role_id: sea_orm::ActiveValue::Set(rid.clone()),
..Default::default()
};
let s = s.insert(stat.db()).await?;
let sql = format!(
"update role set user_count = user_count + 1 where id = '{}'",
rid,
);
stat.db()
.execute(sea_orm::Statement::from_string(
sea_orm::DatabaseBackend::MySql,
sql,
))
.await?;
Ok(web::Json(s))
}
#[delete("/app/{aid}/role/{rid}/user/{uid}")]
#[access_delete("app", id = "&id.clone().0")]
pub async fn drop(
id: web::Path<(String, String, String)>,
stat: web::Data<AppState>,
) -> Result<impl Responder> {
let (_, rid, uid) = id.into_inner();
models::user_role::Entity::delete_many()
.filter(models::user_role::Column::RoleId.eq(rid.clone()))
.filter(models::user_role::Column::UserId.eq(uid))
.exec(stat.db())
.await?;
let sql = format!(
"update role set user_count = user_count - 1 where id = '{}'",
rid,
);
stat.db()
.execute(sea_orm::Statement::from_string(
sea_orm::DatabaseBackend::MySql,
sql,
))
.await?;
Ok("ok")
}

@ -12,10 +12,10 @@ use crate::{
models::{self, app_user, user, AUStatus, UserPlugin}, models::{self, app_user, user, AUStatus, UserPlugin},
AppState, Error, Result, AppState, Error, Result,
}; };
use actix_web::{delete, get, head, http, post, web, HttpResponse, Responder}; use actix_web::{delete, get, head, http, patch, post, web, HttpResponse, Responder};
use base64; use base64;
use chrono::{DateTime, Local, NaiveDateTime}; use chrono::{DateTime, Local, NaiveDateTime};
use proc::access_read; use proc::{access_read, access_update, crud_update};
use rand::Rng; use rand::Rng;
use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, TransactionTrait}; use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, TransactionTrait};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -35,10 +35,46 @@ pub async fn get(id: web::Path<String>, stat: web::Data<AppState>) -> Result<imp
} }
} }
#[derive(Debug, Deserialize, Serialize)]
pub struct ListOptions {
name: Option<String>,
role_id: Option<String>,
app_id: Option<String>,
}
#[get("/user/")] #[get("/user/")]
#[access_read("user")] #[access_read("user")]
pub async fn list(stat: web::Data<AppState>) -> Result<impl Responder> { pub async fn list(
let res: Vec<user::Model> = user::Entity::find().all(stat.db()).await?; stat: web::Data<AppState>,
query: web::Query<ListOptions>,
) -> Result<impl Responder> {
let query = query.into_inner();
let res = if let Some(v) = query.name {
user::Entity::find()
.filter(user::Column::Username.contains(v))
.all(stat.db())
.await?
} else if let Some(v) = query.role_id {
models::user_role::Entity::find()
.filter(models::user_role::Column::RoleId.eq(v))
.find_also_related(user::Entity)
.all(stat.db())
.await?
.into_iter()
.filter_map(|(_, u)| return u)
.collect()
} else if let Some(v) = query.app_id {
models::app_user::Entity::find()
.filter(models::app_user::Column::AppId.eq(v))
.find_also_related(user::Entity)
.all(stat.db())
.await?
.into_iter()
.filter_map(|(_, u)| return u)
.collect()
} else {
user::Entity::find().all(stat.db()).await?
};
Ok(web::Json(res)) Ok(web::Json(res))
} }
@ -175,6 +211,26 @@ pub async fn register(
Ok(web::Json(u)) Ok(web::Json(u))
} }
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct UpdateOpt {
pub username: Option<String>,
pub icon: Option<String>,
pub nickname: Option<String>,
pub email: Option<String>,
pub phone: Option<String>,
}
#[patch("/user/{id}")]
#[access_update("user")]
#[crud_update(user, Id = "_id", username, icon, nickname, email, phone)]
pub async fn update(
id: web::Path<String>,
stat: web::Data<AppState>,
data: web::Json<UpdateOpt>,
) -> Result<impl Responder> {
Ok("")
}
#[delete("/user/")] #[delete("/user/")]
pub async fn delete() -> impl Responder { pub async fn delete() -> impl Responder {
"" ""

@ -1,69 +0,0 @@
//
// userapp.rs
// Copyright (C) 2023 veypi <i@veypi.com>
// 2023-10-09 03:12
// Distributed under terms of the MIT license.
//
use actix_web::{get, post, web, Responder};
use proc::access_read;
use sea_orm::{ColumnTrait, EntityTrait, QueryFilter, TransactionTrait};
use crate::{
libs,
models::{app, app_user},
AppState, Error, Result,
};
// 获取 app
#[get("/user/{uid}/user/{aid}")]
#[access_read("app")]
pub async fn get(
params: web::Path<(String, String)>,
stat: web::Data<AppState>,
) -> Result<impl Responder> {
let (mut aid, mut uid) = params.into_inner();
let mut q = app_user::Entity::find();
if uid == "-" {
uid = "".to_string();
} else {
q = q.filter(app_user::Column::UserId.eq(uid.clone()));
}
if aid == "-" {
aid = "".to_string();
} else {
q = q.filter(app_user::Column::AppId.eq(aid.clone()));
}
if uid.is_empty() && aid.is_empty() {
Err(Error::Missing("uid or aid".to_string()))
} else {
let s: Vec<(app_user::Model, Option<app::Model>)> =
q.find_also_related(app::Entity).all(stat.db()).await?;
let res: Vec<app::Model> = s
.into_iter()
.filter_map(|(l, a)| match a {
Some(a) => Some(app::Model {
status: l.status,
..a
}),
None => None,
})
.collect();
// let s: Vec<app_user::Model> = q.all(stat.db()).await?;
Ok(web::Json(res))
}
}
// 关联app
#[post("/user/{uid}/app/{aid}")]
#[access_read("app")]
pub async fn add(
params: web::Path<(String, String)>,
stat: web::Data<AppState>,
) -> Result<impl Responder> {
let (aid, uid) = params.into_inner();
let db = stat.db().begin().await?;
let res = libs::user::connect_to_app(uid, aid, &db, None).await?;
db.commit().await?;
Ok(web::Json(res))
}

@ -60,14 +60,23 @@ pub async fn after_connected_to_app(
obj: app::Model, obj: app::Model,
db: &DatabaseTransaction, db: &DatabaseTransaction,
) -> Result<()> { ) -> Result<()> {
if obj.role_id.is_some() { if let Some(role_id) = obj.role_id {
user_role::ActiveModel { user_role::ActiveModel {
user_id: sea_orm::ActiveValue::Set(uid.clone()), user_id: sea_orm::ActiveValue::Set(uid.clone()),
role_id: sea_orm::ActiveValue::Set(obj.role_id.unwrap().clone()), role_id: sea_orm::ActiveValue::Set(role_id.clone()),
..Default::default() ..Default::default()
} }
.insert(db) .insert(db)
.await?; .await?;
let sql = format!(
"update role set user_count = user_count + 1 where id = '{}'",
role_id,
);
db.execute(sea_orm::Statement::from_string(
sea_orm::DatabaseBackend::MySql,
sql,
))
.await?;
}; };
let sql = format!( let sql = format!(
"update app set user_count = user_count + 1 where id = '{}'", "update app set user_count = user_count + 1 where id = '{}'",

@ -11,9 +11,11 @@ use actix_web::{
http::StatusCode, http::StatusCode,
middleware::{self, ErrorHandlerResponse, ErrorHandlers}, middleware::{self, ErrorHandlerResponse, ErrorHandlers},
web::{self}, web::{self},
App, HttpServer, App, HttpResponse, HttpServer, Responder,
}; };
use futures_util::future::FutureExt; use futures_util::future::FutureExt;
use mime_guess::from_path;
use rust_embed::RustEmbed;
use http::{HeaderName, HeaderValue}; use http::{HeaderName, HeaderValue};
use oab::{api, init_log, libs, models, AppState, Clis, Result, CLI}; use oab::{api, init_log, libs, models, AppState, Clis, Result, CLI};
@ -100,6 +102,7 @@ async fn web(data: AppState) -> Result<()> {
.app_data(web::Data::new(dav.clone())) .app_data(web::Data::new(dav.clone()))
.service(web::resource("/{tail:.*}").to(libs::fs::dav_handler)), .service(web::resource("/{tail:.*}").to(libs::fs::dav_handler)),
) )
.service(index)
}); });
info!("listen to {}", url); info!("listen to {}", url);
serv.bind(url)?.run().await?; serv.bind(url)?.run().await?;
@ -113,3 +116,21 @@ fn add_error_header<B>(
Ok(ErrorHandlerResponse::Response(res.map_into_left_body())) Ok(ErrorHandlerResponse::Response(res.map_into_left_body()))
} }
#[derive(RustEmbed)]
#[folder = "dist/"]
struct Asset;
#[actix_web::get("/{_:.*}")]
async fn index(p: web::Path<String>) -> impl Responder {
info!("{}", p);
let p = &p.into_inner();
match Asset::get(p) {
Some(content) => HttpResponse::Ok()
.content_type(from_path(p).first_or_octet_stream().as_ref())
.body(content.data.into_owned()),
None => HttpResponse::Ok()
.content_type(from_path("index.html").first_or_octet_stream().as_ref())
.body(Asset::get("index.html").unwrap().data.into_owned()),
}
}

@ -15,7 +15,7 @@ use serde::{Deserialize, Serialize};
use tracing::info; use tracing::info;
pub use app_plugin::{AUStatus, AppJoin}; pub use app_plugin::{AUStatus, AppJoin};
pub use entity::{access, app, app_user, role, user, user_role}; pub use entity::{access, app, app_user, resource, role, user, user_role};
pub use user_plugin::{rand_str, AccessCore, AccessLevel, Token, UserPlugin}; pub use user_plugin::{rand_str, AccessCore, AccessLevel, Token, UserPlugin};
use crate::AppState; use crate::AppState;

@ -1,8 +1,6 @@
[[toc]]
# 例子 # 例子
# Markdown { 简明手册 | jiǎn míng shǒu cè } # Markdown { 简明手册 | jiǎn míng shǒu cè }
[[toc]]
# 基本语法 # 基本语法
--- ---

@ -76,7 +76,7 @@ module.exports = configure(function(/* ctx */) {
// rebuildCache: true, // rebuilds Vite/linter/etc cache on startup // rebuildCache: true, // rebuilds Vite/linter/etc cache on startup
// publicPath: '/', publicPath: '/',
// analyze: true, // analyze: true,
// env: {}, // env: {},
// rawDefine: {} // rawDefine: {}

@ -0,0 +1,30 @@
/*
* access.ts
* Copyright (C) 2023 veypi <i@veypi.com>
* 2023-10-12 19:32
* Distributed under terms of the MIT license.
*/
import ajax from './axios'
export default (app_id: string) => {
return {
local: `./app/${app_id}/access/`,
create(name: string, props?: { name?: string, level?: number, role_id?: string, user_id?: string, rid?: string }) {
return ajax.post(this.local, Object.assign({ name, level: 0 }, props))
},
get(id: string) {
return ajax.get(this.local + id)
},
list(props?: { name?: string, role_id?: string, user_id?: string }) {
return ajax.get(this.local, props)
},
update(uuid: string, props: any) {
return ajax.patch(this.local + uuid, props)
},
del(id: string) {
return ajax.delete(this.local + id)
},
}
}

@ -6,16 +6,21 @@
*/ */
import app from "./app"; import app from "./app";
import role from "./role";
import token from "./token"; import token from "./token";
import user from "./user"; import user from "./user";
import resource from "./resource";
import access from './access';
const api = { const api = {
user: user, user,
app: app, app,
token: token token,
role,
resource,
access,
} }

@ -0,0 +1,30 @@
/*
* resource.ts
* Copyright (C) 2023 veypi <i@veypi.com>
* 2023-10-12 18:03
* Distributed under terms of the MIT license.
*/
import ajax from './axios'
export default (app_id: string) => {
return {
local: `./app/${app_id}/resource/`,
create(name: string, props?: { des?: string }) {
return ajax.post(this.local, Object.assign({ name }, props))
},
get(id: string) {
return ajax.get(this.local + id)
},
list() {
return ajax.get(this.local)
},
update(uuid: string, props: any) {
return ajax.patch(this.local + uuid, props)
},
del(id: string) {
return ajax.delete(this.local + id)
},
}
}

@ -0,0 +1,36 @@
/*
* role.ts
* Copyright (C) 2023 veypi <i@veypi.com>
* 2023-10-12 15:40
* Distributed under terms of the MIT license.
*/
import ajax from './axios'
export default (app_id: string) => {
return {
local: `./app/${app_id}/role/`,
create(name: string, props?: { des?: string }) {
return ajax.post(this.local, Object.assign({ name }, props))
},
get(id: string) {
return ajax.get(this.local + id)
},
list() {
return ajax.get(this.local)
},
update(uuid: string, props: any) {
return ajax.patch(this.local + uuid, props)
},
del(id: string) {
return ajax.delete(this.local + id)
},
add(id: string, uid: string) {
return ajax.get(this.local + `${id}/user/${uid}`)
},
drop(id: string, uid: string) {
return ajax.delete(this.local + `${id}/user/${uid}`)
}
}
}

@ -31,10 +31,10 @@ export default {
get(id: number) { get(id: number) {
return ajax.get(this.local + id) return ajax.get(this.local + id)
}, },
list() { list(props?: { name?: string, role_id?: string, app_id?: string }) {
return ajax.get(this.local) return ajax.get(this.local, props)
}, },
update(id: number, props: any) { update(id: string, props: any) {
return ajax.patch(this.local + id, props) return ajax.patch(this.local + id, props)
}, },
} }

@ -9,7 +9,8 @@
<div class="v-crud" :vertical='modeV'> <div class="v-crud" :vertical='modeV'>
<div class="v-crud-keys"> <div class="v-crud-keys">
<template v-for="k of keys" :key="k.name"> <template v-for="k of keys" :key="k.name">
<div class="v-crud-cell" :style="Object.assign({}, cstyle[k.name], cstyle.k)"> <div class="v-crud-cell" :style="Object.assign({},
cstyle.width[k.name], cstyle.k, cstyle.kv[k.name])">
{{ k.label || k.name }} {{ k.label || k.name }}
</div> </div>
</template> </template>
@ -20,17 +21,19 @@
<div class="v-crud-cell" :changed="item.__changed[k.name]" :selected="selected <div class="v-crud-cell" :changed="item.__changed[k.name]" :selected="selected
=== `${item.__idx}.${k.name}`" @click="ifselect ? === `${item.__idx}.${k.name}`" @click="ifselect ?
selected = `${item.__idx}.${k.name}` : ''" :style="Object.assign({}, selected = `${item.__idx}.${k.name}` : ''" :style="Object.assign({},
cstyle[k.name], cstyle.v)"> cstyle.width[k.name], cstyle.v, cstyle.kv[k.name])">
<slot :name="`k_${k.name}`" :row="item" :value="item[k.name]" :set="setv(item, k.name)"> <slot :name="`k_${k.name}`" :row="item" :value="item[k.name]" :set="setv(item, k.name)">
<template v-if="k.editable === undefined ? editable : <template v-if="k.editable === undefined ? editable :
k.editable"> k.editable">
<vinput :model-value="item[k.name] === undefined ? <vinput :align="valign" :model-value="item[k.name] === undefined ?
k.default : item[k.name]" :type="k.typ" :options="k.options" @update:model-value="setv(item, k.default : item[k.name]" :type="k.typ" :options="k.options" @update:model-value="setv(item,
k.name)($event)"></vinput> k.name)($event)"></vinput>
</template> </template>
<template v-else> <template v-else>
<span class="truncate">
{{ item[k.name] === undefined ? k.default : {{ item[k.name] === undefined ? k.default :
item[k.name] }} item[k.name] }}
</span>
</template> </template>
</slot> </slot>
</div> </div>
@ -42,7 +45,7 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { computed, getCurrentInstance, onMounted, reactive, ref, watch } from 'vue'; import { computed, onMounted, ref, watch } from 'vue';
import vinput from 'src/components/vinput' import vinput from 'src/components/vinput'
import { ArgType, Dict } from 'src/models'; import { ArgType, Dict } from 'src/models';
@ -54,6 +57,7 @@ interface keyProp {
typ?: ArgType, typ?: ArgType,
editable?: boolean, editable?: boolean,
options?: any, options?: any,
style?: { [k: string]: string },
} }
let emits = defineEmits<{ let emits = defineEmits<{
(e: 'update', v: any): void (e: 'update', v: any): void
@ -77,6 +81,7 @@ let props = withDefaults(defineProps<{
vertical: false, vertical: false,
data: [] as any, data: [] as any,
hover: false, hover: false,
kvstyle: {} as any,
kstyle: {} as any, kstyle: {} as any,
vstyle: {} as any, vstyle: {} as any,
cstyle: {} as any, cstyle: {} as any,
@ -88,9 +93,13 @@ const modeV = ref(props.vertical)
watch(computed(() => props.vertical), (v) => modeV.value = v) watch(computed(() => props.vertical), (v) => modeV.value = v)
let items = ref<any[]>([]) let items = ref<any[]>([])
// watch(computed(() => props.data), (v) => { watch(computed(() => JSON.stringify(props.data)), (_) => {
syncItems()
// if (JSON.stringify(v) !== JSON.stringify(o)) {
// console.log(JSON.stringify(v))
// syncItems() // syncItems()
// }) // }
}, {})
const syncItems = () => { const syncItems = () => {
let res = props.data?.map((v: any, i: any) => { let res = props.data?.map((v: any, i: any) => {
return Object.assign({ __idx: i, __changed: {} }, v) return Object.assign({ __idx: i, __changed: {} }, v)
@ -105,21 +114,24 @@ const selected = ref()
let alignDic = { 'center': 'center', 'left': 'start', 'right': 'end' } let alignDic = { 'center': 'center', 'left': 'start', 'right': 'end' }
const cstyle = computed(() => { const cstyle = computed(() => {
let res = { line: [] } as any let res = { line: [], width: {}, kv: {} } as any
let l = props.keys?.length || 0 let l = props.keys?.length || 0
let w = 100 let w = 100
let style = modeV.value ? 'flex-basis' : 'height' let style = modeV.value ? 'flex-basis' : 'height'
props.keys?.forEach((k, i) => { props.keys?.forEach((k, i) => {
if (k.width && k.width > 0 && k.width < 100) { if (k.width && k.width > 0 && k.width < 100) {
res[k.name] = { [style]: (k.width || 1) + '%' } res.width[k.name] = { [style]: (k.width || 1) + '%' }
w = w - k.width w = w - k.width
l = l - 1 l = l - 1
} }
if (k.style) {
res.kv[k.name] = k.style
}
}) })
props.keys?.forEach((k, i) => { props.keys?.forEach((k, i) => {
if (k.width && k.width > 0 && k.width < 100) { if (k.width && k.width > 0 && k.width < 100) {
} else { } else {
res[k.name] = { [style]: w / l + '%' } res.width[k.name] = { [style]: w / l + '%' }
} }
}) })
res.k = Object.assign({ 'justify-content': alignDic[props.kalign] }, props.cstyle, props.kstyle) res.k = Object.assign({ 'justify-content': alignDic[props.kalign] }, props.cstyle, props.kstyle)
@ -185,6 +197,7 @@ defineExpose({
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
gap: 1rem;
overflow: auto; overflow: auto;
overflow-y: auto; overflow-y: auto;

@ -15,7 +15,6 @@
import Cherry from 'cherry-markdown'; import Cherry from 'cherry-markdown';
import options from './options' import options from './options'
import { computed, onMounted, ref, watch } from 'vue'; import { computed, onMounted, ref, watch } from 'vue';
import { CherryOptions } from 'cherry-markdown/types/cherry';
import oafs from 'src/libs/oafs'; import oafs from 'src/libs/oafs';
let editor = {} as Cherry; let editor = {} as Cherry;
@ -24,7 +23,7 @@ let emits = defineEmits<{
(e: 'update:modelValue', v: boolean): void (e: 'update:modelValue', v: boolean): void
}>() }>()
let props = withDefaults(defineProps<{ let props = withDefaults(defineProps<{
modelValue: boolean, modelValue?: boolean,
eid?: string, eid?: string,
content?: string, content?: string,
static_dir?: string, static_dir?: string,
@ -32,6 +31,7 @@ let props = withDefaults(defineProps<{
{ {
eid: 'v-editor', eid: 'v-editor',
content: '', content: '',
modelValue: true,
} }
) )

@ -51,7 +51,7 @@
<template v-else-if="type === ArgType.Select"> <template v-else-if="type === ArgType.Select">
<div class="noborder cursor-pointer w-full overflow-x-auto whitespace-nowrap" @click="showSelect" :title="title"> <div class="noborder cursor-pointer w-full overflow-x-auto whitespace-nowrap" @click="showSelect" :title="title">
<span v-if="!value"></span> <span v-if="value === undefined || value === null"></span>
<span v-else-if="!Array.isArray(value)">{{ transDic[value] || value }}</span> <span v-else-if="!Array.isArray(value)">{{ transDic[value] || value }}</span>
<template v-else> <template v-else>
<span class="mx-2" v-for=" iv in value " :key="iv">{{ transDic[iv] || iv }}</span> <span class="mx-2" v-for=" iv in value " :key="iv">{{ transDic[iv] || iv }}</span>
@ -61,10 +61,9 @@
:style="{ left: selectPos[0] + 'px', top: selectPos[1] + 'px', height: showSelectOpt ? '20rem' : '0rem' }" :style="{ left: selectPos[0] + 'px', top: selectPos[1] + 'px', height: showSelectOpt ? '20rem' : '0rem' }"
class="select-opt text-base text-white rounded-md overflow-y-auto" style="min-width: 10rem;" :title="title"> class="select-opt text-base text-white rounded-md overflow-y-auto" style="min-width: 10rem;" :title="title">
<div class="m-2 p-2" v-if="!options"></div> <div class="m-2 p-2" v-if="!options"></div>
<div :class="[ok === value ? 'bg-gray-500' : 'bg-gray-800']" <div :class="[ov.key === value ? 'bg-gray-500' : 'bg-gray-800']" class="cursor-pointer m-2 p-2 rounded-md
class="cursor-pointer m-2 p-2 rounded-md hover:bg-gray-500" @click="setSelect(ok)" hover:bg-gray-500" @click="setSelect(ov.key)" v-for="ov of options" :key="ov.key">
v-for="( ov, ok ) in transDic " :key="ok"> {{ ov.name }}
{{ ov }}
</div> </div>
<div class="w-full h-32"></div> <div class="w-full h-32"></div>
</div> </div>

@ -9,7 +9,17 @@ function padLeftZero(str: string): string {
const util = { const util = {
datetostr(d: string) { datetostr(d: string) {
return new Date(d + 'z').toLocaleString() let r = new Date(d + 'z')
let delta = (new Date().getTime() - r.getTime()) / 1000
if (delta < 0) {
} else if (delta < 60) {
return Math.floor(delta) + '秒前'
} else if (delta < 3600) {
return Math.floor(delta / 60) + '分钟前'
} else if (delta < 86400) {
return Math.floor(delta / 3600) + '小时前'
}
return r.toLocaleString()
}, },
randomNum(minNum: number, maxNum: number) { randomNum(minNum: number, maxNum: number) {
return Math.floor(Math.random() * maxNum) + minNum return Math.floor(Math.random() * maxNum) + minNum

@ -27,39 +27,48 @@ export const R = {
Auth: 'auth', Auth: 'auth',
} }
const level = {
None: 0, export enum AccessLevel {
Do: 1, None = 0,
Part: 1, Do = 1,
Read: 2, Read = 1,
Create: 3, Create = 2,
Update: 4, Update = 3,
Delete: 5, Delete = 4,
All: 6 All = 5
} }
export const LevelOptions = [
{ key: 0, name: '无权限' },
{ key: 1, name: '读取数据权限' },
{ key: 2, name: '创建数据权限' },
{ key: 3, name: '更新数据权限' },
{ key: 4, name: '删除数据权限' },
{ key: 5, name: '管理员权限' },
]
class authLevel { class authLevel {
level = level.None level = AccessLevel.None
constructor(level: number) { constructor(level: number) {
this.level = level this.level = level
} }
CanDo(): boolean { CanDo(): boolean {
return this.level >= level.Do return this.level >= AccessLevel.Do
} }
CanRead(): boolean { CanRead(): boolean {
return this.level >= level.Read return this.level >= AccessLevel.Read
} }
CanCreate(): boolean { CanCreate(): boolean {
return this.level >= level.Create return this.level >= AccessLevel.Create
} }
CanUpdate(): boolean { CanUpdate(): boolean {
return this.level >= level.Update return this.level >= AccessLevel.Update
} }
CanDelete(): boolean { CanDelete(): boolean {
return this.level >= level.Delete return this.level >= AccessLevel.Delete
} }
CanDoAny(): boolean { CanDoAny(): boolean {
return this.level >= level.All return this.level >= AccessLevel.All
} }
} }
@ -71,7 +80,7 @@ export class auths {
} }
Get(name: string, rid: string): authLevel { Get(name: string, rid: string): authLevel {
let l = level.None let l = AccessLevel.None
for (let i of this.list) { for (let i of this.list) {
if (i.name == name && (!i.rid || i.rid === rid) && i.level > l) { if (i.name == name && (!i.rid || i.rid === rid) && i.level > l) {
l = i.level l = i.level

@ -6,8 +6,9 @@
*/ */
import { RouteLocationRaw } from 'vue-router'; import { RouteLocationRaw } from 'vue-router';
import { AccessLevel } from './auth';
export { type Auths, type modelsSimpleAuth, NewAuths, R } from './auth' export { type Auths, type modelsSimpleAuth, NewAuths, R, AccessLevel, LevelOptions } from './auth'
export type Dict = { [key: string]: any } export type Dict = { [key: string]: any }
@ -88,27 +89,6 @@ export interface modelsApp {
user_count: number user_count: number
au: modelsAppUser au: modelsAppUser
// Creator: number
// Des: string
// EnableEmail: boolean
// EnablePhone: boolean
// EnableRegister: true
// EnableUser: boolean
// EnableUserKey: boolean
// EnableWx: boolean
// Hide: boolean
// Host: string
// Icon: string
// InitRole?: null
// InitRoleID: number
// Name: string
// UUID: string
// UserCount: number
// UserKeyUrl: string
// UserRefreshUrl: string
// UserStatus: string
// Users: null
} }
export enum AUStatus { export enum AUStatus {
@ -130,7 +110,6 @@ export interface modelsUser {
id: string id: string
created: string created: string
updated: string updated: string
delete_flag: boolean
username: string username: string
nickname: string nickname: string
email: string email: string
@ -140,63 +119,34 @@ export interface modelsUser {
used: number used: number
space: number space: number
au: AUStatus au: AUStatus
// Index 前端缓存
// Index?: number
// Apps: modelsApp[]
// Auths: null
// CreatedAt: string
// DeletedAt: null
// ID: number
// Icon: string
// Position: string
// Roles: null
// Status: string
// UpdatedAt: string
// Username: string
// Email: string
// Nickname: string
// Phone: string
} }
export interface modelsAuth { export interface modelsAccess {
App?: modelsApp id: number
AppUUID: string created: string
CreatedAt: string updated: string
DeletedAt: null name: string
ID: number app_id: string,
Level: number role_id?: string,
RID: string user_id?: string,
RUID: string level: AccessLevel,
Resource?: modelsResource rid?: string
ResourceID: number
Role?: modelsRole
RoleID: number
UpdatedAt: string
User?: modelsUser
UserID?: number
} }
export interface modelsRole { export interface modelsRole {
App?: modelsApp created: string
AppUUID: string updated: string
Auths: null app_id: string
CreatedAt: string id: string
DeletedAt: null name: string
ID: number des: string
Name: string user_count: number
Tag: string
UpdatedAt: string
UserCount: number
} }
export interface modelsResource { export interface modelsResource {
App?: modelsApp created: string
AppUUID: string updated: string
CreatedAt: string app_id: string
DeletedAt: null name: string
Des: string des: string
ID: number
Name: string
UpdatedAt: string
} }

@ -21,8 +21,7 @@ onMounted(() => {
timer.value = setInterval(() => { timer.value = setInterval(() => {
count.value-- count.value--
if (count.value === 0) { if (count.value === 0) {
router.back() router.push({ name: 'home' })
// router.push('/')
clearInterval(timer.value) clearInterval(timer.value)
} }
}, 1000) }, 1000)

@ -5,10 +5,312 @@
* Distributed under terms of the MIT license. * Distributed under terms of the MIT license.
--> -->
<template> <template>
<div></div> <div class="px-10">
<div class="flex justify-between">
<div class="text-3xl">角色管理</div>
<q-btn @click="created(0)"></q-btn>
</div>
<div class="w-full">
<crud vertical :keys="role_keys" :data="role" @update="update(0,
$event)">
<template #k_created="{ value }">{{ util.datetostr(value) }}</template>
<template #k__="{ row }">
<q-btn color="primary" size="sm" @click="show_dialog(0,
row.id)">权限</q-btn>
<q-btn color="secondary" size="sm" @click="show_dialog(1,
row.id)">用户</q-btn>
<q-btn color="negative" size="sm" @click="del(0, row.id)">删除</q-btn>
<template v-if="row.id === app.role_id">
<q-btn color="positive" disable size="sm">初始角色</q-btn>
</template>
<template v-else>
<q-btn size="sm" @click="api.app.update(app.id, {
role_id:
row.id
}).then(_ => app.role_id = row.id)">设置为初始角色</q-btn>
</template>
</template>
</crud>
</div>
<div class="flex mt-16 justify-between">
<div class="text-3xl">资源管理</div>
<q-btn @click="created(1)"></q-btn>
</div>
<div class="w-full">
<crud vertical :keys="resource_keys" :data="resource" @update="update(1,
$event)">
<template #k_created="{ value }">{{ util.datetostr(value) }}</template>
<template #k__="{ row }">
<q-btn color="negative" size="sm" @click="del(1, row.name)">删除</q-btn>
</template>
</crud>
</div>
<q-dialog v-model="dialog">
<q-card v-if="dialog_obj" class="mx-4 mt-4" style="width: 1000px;max-width: 90vw;">
<q-card-section class="row items-center q-pb-none">
<div class="text-h6">{{ dialog_obj.name }}
</div>
<q-space />
<div v-if="dialog_mode">
<q-select filled :model-value="''" @update:model-value="roleuser.add" use-input hide-selected fill-input
input-debounce="0" label="添加用户" :options="users_cache" @filter="filterFn" style="width: 20rem">
<template v-slot:no-option>
<q-item>
<q-item-section class="text-grey">
无结果
</q-item-section>
</q-item>
</template>
</q-select>
</div>
<q-btn v-else @click="created(2, { role_id: dialog_obj.id })">添加权限</q-btn>
</q-card-section>
<q-card-section v-if="dialog_mode">
<crud vertical :keys="users_keys" :data="users">
<template #k__="{ row }">
<q-btn color="negative" size="sm" @click="roleuser.drop(row)"></q-btn>
</template>
</crud>
</q-card-section>
<q-card-section v-else>
<crud vertical :keys="access_keys" :data="access" @update="update(2, $event)">
<template #k__="{ row }">
<q-btn color="negative" size="sm" @click="del(2, row.id)">删除</q-btn>
</template>
</crud>
</q-card-section>
</q-card>
</q-dialog>
</div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import msg from '@veypi/msg';
import { useQuasar } from 'quasar';
import api from 'src/boot/api';
import crud from 'src/components/crud.vue';
import { util } from 'src/libs';
import {
ArgType, modelsAccess, modelsApp, modelsRole, modelsUser,
LevelOptions
} from 'src/models';
import { computed, inject, onMounted, Ref, ref, watch } from 'vue';
let $q = useQuasar();
let dialog = ref(false)
let dialog_mode = ref(0)
let dialog_obj = ref<modelsRole>({} as any)
const role_keys = ref<any>([
{
name: 'id',
label: 'ID',
},
{ name: 'name', label: '标识符' },
{ name: 'des', label: '描述', editable: true },
{ name: 'created', label: '创建时间' },
{ name: 'user_count', label: '绑定用户数' },
{
name: '_', label: '操作', style: { 'justify-content': 'start' },
width: 40
},
])
const resource_keys = ref<any>([
{ name: 'name', label: '标识符' },
{ name: 'des', label: '描述', editable: true },
{ name: 'created', label: '创建时间' },
{
name: '_', label: '操作', style: { 'justify-content': 'start' },
width: 40
},
])
const access_keys = ref<any>([
{ name: 'id', label: 'ID' },
{ name: 'name', label: '标识符' },
{ name: 'rid', label: '特定子资源id', editable: true },
{
name: 'level', label: '权限等级', editable: true, typ:
ArgType.Select, options: LevelOptions,
},
{
name: '_', label: '操作', style: { 'justify-content': 'start' },
width: 40
},
])
const users_keys = ref<any>([
{ name: 'id', label: 'ID' },
{ name: 'username', label: '用户名' },
{ name: 'nickname', label: '昵称' },
{
name: '_', label: '操作', style: { 'justify-content': 'start' },
width: 40
},
])
let access = ref<modelsAccess[]>([])
let users = ref<modelsUser[]>([])
let users_cache = ref<any[]>([])
const resource = ref<any[]>([])
const role = ref<any[]>([])
const app = inject('app') as Ref<modelsApp>
watch(computed(() => app.value.id), (v) => {
sync(v)
})
const sync = (id: any) => {
if (!id) {
return
}
api.role(id).list().then(e => {
role.value = e
})
api.resource(id).list().then(e => {
resource.value = e
})
}
const show_dialog = (mode: number, idx: string) => {
dialog_obj.value = role.value.find(e => e.id === idx)
dialog.value = true
dialog_mode.value = mode
if (mode) {
api.user.list({ role_id: idx }).then(e => {
users.value = e
})
} else {
api.access(app.value.id).list({ role_id: idx }).then(e => {
access.value = e
})
}
}
// 0: role 1: resource 2: access 3: users
const crud_option = (mode: number) => {
let res = {
api: api.role(app.value.id),
lable: '角色',
obj: role,
}
if (mode === 1) {
res.api = api.resource(app.value.id) as any
res.lable = '资源'
res.obj = resource
} else if (mode === 2) {
res.api = api.access(app.value.id) as any
res.lable = '权限'
res.obj = access
} else if (mode === 3) {
res.api = api.user as any
res.lable = '用户'
res.obj = users
}
return res
}
const created = (k: number, props?: any) => {
let opt = crud_option(k)
let options;
let prompt;
if (k !== 2) {
prompt = { model: '', type: 'text' }
} else {
console.log(k)
options = {
model: '', type: 'radio', items: resource.value.map(e => {
return {
label: e.name,
value: e.name
}
})
}
}
$q.dialog({
title: '创建' + opt.lable,
message: '请输入标识码',
prompt: prompt as any,
options: options as any,
cancel: true,
persistent: true
}).onOk(data => {
opt.api.create(data, props).then(e => {
msg.Info('创建成功')
opt.obj.value.push(e)
}).catch(e => {
msg.Warn('创建失败: ' + e)
})
})
}
const update = (k: number, props: any[]) => {
let opt = crud_option(k)
console.log(props)
for (let i in props) {
let id = opt.obj.value[i][k === 1 ? 'name' : 'id']
console.log(id)
opt.api.update(id, props[i]).then(() => {
Object.assign(opt.obj.value[i], props[i])
}).catch(e => {
msg.Warn('更新失败: ' + e)
})
}
}
const del = (k: number, id: string) => {
let opt = crud_option(k)
$q.dialog({
title: '是否确定删除',
message: '',
cancel: true,
persistent: true
}).onOk(() => {
opt.api.del(id).then(e => {
msg.Info('删除成功')
opt.obj.value.splice(opt.obj.value.findIndex(e => e.name === id
|| e.id === id), 1)
}).catch(e => {
msg.Warn('删除失败: ' + e)
})
})
}
const roleuser = {
add: (u: any) => {
let idx = users.value.findIndex(e => e.id == u.id)
if (idx >= 0) {
return
}
api.role(app.value.id).add(dialog_obj.value?.id || '', u.id).then(e => {
users.value.push(u)
dialog_obj.value.user_count = dialog_obj.value?.user_count + 1
})
},
drop: (u: any) => {
api.role(app.value.id).drop(dialog_obj.value?.id || '', u.id).then(e => {
users.value.splice(users.value.findIndex(e => e.id === u.id), 1)
dialog_obj.value.user_count = dialog_obj.value?.user_count - 1
})
}
}
const filterFn = (val: string, cb: any) => {
if (val && val.length > 1) {
api.user.list({ name: val }).then((e: modelsUser[]) => {
cb(() => {
users_cache.value = e.map(i => {
return Object.assign({
label: i.username,
value: i.id,
}, i)
})
})
})
}
}
onMounted(() => {
sync(app.value.id)
})
</script> </script>
<style scoped></style> <style scoped></style>

@ -7,7 +7,7 @@
<template> <template>
<div> <div>
<div class="flex justify-center pt-10"> <div class="flex justify-center pt-10">
<CRUD ref="table" v-if="app.id" :keys="keys" :data="[app]" kalign="left" valign="left" editable <CRUD ref="table" :keys="keys" :data="app.id ? [app] : []" kalign="left" valign="left" editable
:vstyle="{ 'width': '50vw' }" @update="newApp = $event[0]" :kstyle="{ 'width': '10rem' }"> :vstyle="{ 'width': '50vw' }" @update="newApp = $event[0]" :kstyle="{ 'width': '10rem' }">
<template #k_icon="{ value, set }"> <template #k_icon="{ value, set }">
<div class="w-full flex justify-center"> <div class="w-full flex justify-center">

@ -5,8 +5,8 @@
* Distributed under terms of the MIT license. * Distributed under terms of the MIT license.
--> -->
<template> <template>
<div> <div class="px-10">
<q-table title="Treats" :rows="rows" :columns="columns" row-key="name"> <q-table title="用户管理" :rows="rows" :columns="columns" row-key="name">
<template #body-cell-created="props"> <template #body-cell-created="props">
<q-td :props="props"> <q-td :props="props">
{{ util.datetostr(props.value) }} {{ util.datetostr(props.value) }}

@ -7,12 +7,11 @@
<template> <template>
<div class="w-full h-full"> <div class="w-full h-full">
<h1 class="page-h1">文档中心</h1> <h1 class="page-h1">文档中心</h1>
<div>
{{ url }}
</div>
<q-inner-loading :showing="!visible" label="Please wait..." label-class="text-teal" label-style="font-size: 1.1em" /> <q-inner-loading :showing="!visible" label="Please wait..." label-class="text-teal" label-style="font-size: 1.1em" />
<div class="w-full px-8">
<Editor v-if='doc' eid='doc' preview :content="doc"></Editor> <Editor v-if='doc' eid='doc' preview :content="doc"></Editor>
</div> </div>
</div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>

@ -7,10 +7,55 @@
<template> <template>
<div> <div>
<h1 class="page-h1">账号设置</h1> <h1 class="page-h1">账号设置</h1>
<div class="flex justify-center pt-10">
<crud ref="table" editable :keys="keys" :data="[u.local]" @update="newData = $event[0]">
<template #k_icon="{ value, set }">
<div class="w-full flex justify-center">
<uploader class="" @success="set" dir="user_icon">
<q-avatar>
<img :src="value">
</q-avatar>
</uploader>
</div>
</template>
</crud>
</div>
<div v-if="newData" class="flex justify-center gap-8 mt-6">
<q-btn color="brown-5" label="回退" @click="table.reload" />
<q-btn color="deep-orange" glossy label="保存" @click="save" />
</div>
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import msg from '@veypi/msg';
import api from 'src/boot/api';
import crud from 'src/components/crud.vue';
import uploader from 'src/components/uploader';
import { useUserStore } from 'src/stores/user';
import { ref } from 'vue';
let u = useUserStore()
let table = ref()
const keys = ref<any>([
{ name: 'id', label: 'ID', editable: false },
{ name: 'username', label: '用户名' },
{ name: 'nickname', label: '昵称' },
{ name: 'icon', label: 'logo' },
{ name: 'email', label: '邮箱' },
{ name: 'phone', label: '手机号' },
])
const newData = ref()
const save = () => {
api.user.update(u.id, newData.value).then(e => {
msg.Info('更新成功')
Object.assign(u.local, newData.value)
newData.value = null
}).catch(e => {
msg.Warn('更新失败 ' + e)
})
}
</script> </script>
<style scoped></style> <style scoped></style>

@ -35,6 +35,9 @@ export const useUserStore = defineStore('user', {
} }
let data = JSON.parse(Base64.decode(token[1])) let data = JSON.parse(Base64.decode(token[1]))
if (data.id) { if (data.id) {
let l = 'access to'
data.access.map((e: any) => l = l + `\n${e.name}.${e.level}`)
console.log(l)
this.auth = NewAuths(data.access) this.auth = NewAuths(data.access)
api.user.get(data.id).then((e: modelsUser) => { api.user.get(data.id).then((e: modelsUser) => {
this.id = e.id this.id = e.id

Loading…
Cancel
Save