Compare commits

...

96 Commits

Author SHA1 Message Date
ca54f9d7a7 update text 2025-03-18 11:45:22 +08:00
dfa0842208 fix utc in frontend 2025-02-26 19:41:55 +08:00
b475b7fc99 Merge branch 'dev3' of https://dev.azure.com/alicorn-dev/KintoneAppBuilder/_git/KintoneAppBuilder into dev3 2025-02-26 17:28:04 +09:00
e4556a0d13 datetime -> utc 2025-02-26 17:27:55 +09:00
078929a254 change quasar locale to ja 2025-02-26 15:25:00 +09:00
a8027a05bb cache tenant database 2025-02-24 16:57:25 +09:00
c0672f2487 fix style && some warning 2025-02-05 17:33:11 +08:00
14191e4f1e fix ts lint warning: Strings must use singlequote 2025-02-05 17:14:34 +08:00
a7788c87be update funtion saving for request route template 2025-02-01 23:22:36 +09:00
b95d81405d update operation log 2025-02-01 21:22:14 +09:00
f70a2cfde6 update operation 2025-02-01 19:40:01 +09:00
f27c0728b7 add parameters in OperationLog 2025-02-01 18:26:19 +09:00
2627c57b30 bugfix LoggingMiddleware 2025-02-01 14:47:21 +09:00
af959469de bugfix LoggingMiddleware 2025-02-01 14:43:22 +09:00
b502a3ba8f add operation log 2025-02-01 14:23:43 +09:00
160367f91b requirement updated 2025-01-24 17:51:49 +09:00
dec42a505e bugfix kintoneFormat 2025-01-24 17:32:01 +09:00
65b82949e6 bugfix getkintoneformat db 2025-01-24 17:21:02 +09:00
95154907a4 bugfix createjstokintone 2025-01-24 17:10:12 +09:00
59bddd4421 Merge branch 'dev3' of https://dev.azure.com/alicorn-dev/KintoneAppBuilder/_git/KintoneAppBuilder into dev3 2025-01-24 17:06:18 +09:00
3f1accc32e bugfix createappjs 2025-01-24 17:06:13 +09:00
dcbfb851ec Merge branch 'dev3' of https://dev.azure.com/alicorn-dev/KintoneAppBuilder/_git/KintoneAppBuilder into dev3 2025-01-24 17:00:48 +09:00
4b6472f48e document changed 2025-01-24 17:00:18 +09:00
3eedbf7564 bugfix kintone createappjs 2025-01-24 16:57:24 +09:00
9c9b5aca95 bugfix kintone createappjs 2025-01-24 16:56:40 +09:00
d31d3d0910 fix login validator 2025-01-13 15:02:35 +08:00
xue jiahao
9d0cabcffa add action permissions 2024-12-30 14:14:54 +08:00
xue jiahao
a4d59de2bc fix bug 2024-12-26 14:14:56 +08:00
xue jiahao
b1c55e3c31 fix ui 2024-12-25 10:11:21 +08:00
xue jiahao
d254cb7e54 add roles label 2024-12-24 23:05:57 +08:00
xue jiahao
a92873b971 Add permissions 2024-12-24 22:17:18 +08:00
5ebfd22652 Merge branch 'dev3' of https://dev.azure.com/alicorn-dev/KintoneAppBuilder/_git/KintoneAppBuilder into dev3 2024-12-24 17:27:50 +09:00
be203cb715 permission table add link column for menu 2024-12-24 17:27:44 +09:00
xue jiahao
84ba118bb1 fix ts 2024-12-24 11:35:14 +08:00
xue jiahao
5d7ffa0138 remove roles in app manageement 2024-12-24 08:57:50 +08:00
xue jiahao
972bbf9013 fix UI & add unchanged 2024-12-23 23:09:08 +08:00
57af07ba73 app set is_saved=True when it has the version 2024-12-23 23:24:57 +09:00
8996a4c836 app set is_saved =True when flow has been edited 2024-12-23 23:18:10 +09:00
fb0674ecff Merge branch 'dev3' of https://dev.azure.com/alicorn-dev/KintoneAppBuilder/_git/KintoneAppBuilder into dev3 2024-12-23 22:59:54 +09:00
1da6a0c42b app add is_saved column 2024-12-23 22:59:45 +09:00
xue jiahao
e9fa013d7d Front end users refactoring 2024-12-23 17:32:20 +08:00
xue jiahao
354abf252b Add role page 2024-12-23 14:29:49 +08:00
8c481ecf4c bugfix assign_userrole 2024-12-23 15:28:25 +09:00
76784b2683 Merge branch 'dev3' of https://dev.azure.com/alicorn-dev/KintoneAppBuilder/_git/KintoneAppBuilder into dev3 2024-12-22 15:00:52 +09:00
a5f5b3fccf bugfix get roles 2024-12-22 15:00:44 +09:00
xue jiahao
ef9ed68468 ui fix for add column 2024-12-17 22:33:24 +08:00
1420773548 appversion add create&update user & time 2024-12-17 21:44:49 +09:00
27ae3e186a bugfix app versionname 2024-12-17 21:07:05 +09:00
d3d3aa2d18 Merge branch 'dev3' of https://dev.azure.com/alicorn-dev/KintoneAppBuilder/_git/KintoneAppBuilder into dev3 2024-12-17 19:56:35 +09:00
c2a7ead1e3 bugfix getallapps 2024-12-17 19:56:29 +09:00
xue jiahao
d7280d66b2 Fix プル -> 回復する 2024-12-17 14:13:33 +08:00
xue jiahao
e7f4078ca3 Fix UI 2024-12-16 23:12:21 +08:00
xue jiahao
7cac64ced8 add tooltip 2024-12-16 17:26:45 +08:00
xue jiahao
fef9e74ba1 fix ui 2024-12-16 16:58:57 +08:00
xue jiahao
736c722eb7 UI fix 2024-12-16 16:09:24 +08:00
51e15287f5 Merge branch 'dev3' of https://dev.azure.com/alicorn-dev/KintoneAppBuilder/_git/KintoneAppBuilder into dev3 2024-12-16 15:50:38 +09:00
0f639cdfa0 bugfix add_managedomain 2024-12-16 15:49:00 +09:00
xue jiahao
c0bda31353 Add share manage dialog 2024-12-16 14:47:03 +08:00
xue jiahao
78e7f1c840 Add role label when sharing 2024-12-16 11:12:50 +08:00
xue jiahao
35270e32f5 Revert assign role in same dialog 2024-12-16 10:42:37 +08:00
xue jiahao
6b94af76c1 [UI] some fix 2024-12-15 22:21:27 +08:00
xue jiahao
1135361b00 fix UI 2024-12-15 11:33:45 +08:00
39775a5179 add tenant logic 2024-12-15 12:28:22 +09:00
2823364148 add manage domain function 2024-12-14 10:32:32 +09:00
xue jiahao
40cadc82d0 UI fix 2024-12-10 21:19:16 +08:00
b928f2f3ef bugfix change_appversion 2024-12-10 21:33:00 +09:00
7b0b77dcb3 bugfix get_flows_by_appid again 2024-12-10 21:18:12 +09:00
64aa2de133 bugfix update_appversion 2024-12-10 21:11:54 +09:00
eea3761e52 Merge branch 'dev3' of https://dev.azure.com/alicorn-dev/KintoneAppBuilder/_git/KintoneAppBuilder into dev3 2024-12-10 21:08:51 +09:00
xiaozhe.ma
74e8b78f6d 文言修正 2024-12-10 21:05:50 +09:00
3c4766cdad bugfix get_flow_by_appid 2024-12-10 21:03:26 +09:00
xiaozhe.ma
163e14022a Merge branch 'dev3' of https://dev.azure.com/alicorn-dev/KintoneAppBuilder/_git/KintoneAppBuilder into dev3 2024-12-10 20:13:32 +09:00
xiaozhe.ma
8e0a9287e9 文言修正 2024-12-10 20:10:37 +09:00
xue jiahao
76643d280a Add VersionHistory page 2024-12-10 17:22:54 +08:00
xue jiahao
f33fd0c64b Remove related code in DomainSelector 2024-12-10 17:22:54 +08:00
d6bd8fdee0 add cache function 2024-12-10 15:46:40 +09:00
c684105c2c Merge branch 'dev3' of https://dev.azure.com/alicorn-dev/KintoneAppBuilder/_git/KintoneAppBuilder into dev3 2024-12-10 11:27:37 +09:00
9eec7e835d bugfix change_appversion Unique Error 2024-12-10 11:27:30 +09:00
xue jiahao
3ecb08b872 delete app 2024-12-09 21:46:23 +08:00
xue jiahao
b95548e7f7 version with backend 2024-12-09 18:48:14 +08:00
xue jiahao
305868f091 [UI] version page 2024-12-09 16:04:25 +08:00
xue jiahao
7221f97139 Add save version dialog
# Conflicts:
#	frontend/src/types/AppTypes.ts
2024-12-09 16:04:25 +08:00
a3df6c4b37 bugfix get flow -> get flow by appid 2024-12-09 16:54:51 +09:00
b874d0c776 bugfix edit_flow 2024-12-09 16:11:58 +09:00
21e0b9d6df bugfix kintone get apps 2024-12-09 15:33:47 +09:00
9b1ae3bb5b app history 2024-12-09 13:00:54 +09:00
8c4aa3119a add appversion & test case 2024-12-08 22:01:27 +09:00
62b6d7a878 sqlalchemy2.x 2024-12-08 17:23:51 +09:00
c5de6ace46 Merge branch 'dev3' of https://dev.azure.com/alicorn-dev/KintoneAppBuilder/_git/KintoneAppBuilder into dev3 2024-12-08 17:05:13 +09:00
198e442292 bugfix set user default domain 2024-12-08 17:05:04 +09:00
xiaozhe.ma
cba365af9c バージョン管理設計画面追加 2024-12-08 16:21:05 +09:00
91df7ed0fa SQLAlchemy1.0->SQLAlchemy2.x Column->mapped-column 2024-12-08 10:16:10 +09:00
3aec075927 SQLAlchemy 1.0->SQLAlchemy 2.x 2024-12-07 21:37:33 +09:00
29501f785f modify conftest & add testcase 2024-12-07 14:22:24 +09:00
7e9654ab4c Merge branch 'dev3' of https://dev.azure.com/alicorn-dev/KintoneAppBuilder/_git/KintoneAppBuilder into dev3 2024-12-06 17:21:56 +09:00
155cbd43e8 add pytest case 2024-12-06 17:21:47 +09:00
102 changed files with 6062 additions and 1165 deletions

File diff suppressed because one or more lines are too long

View File

@@ -1,26 +1,36 @@
from fastapi import Request
from fastapi.responses import JSONResponse
from fastapi.security import OAuth2PasswordRequestForm from fastapi.security import OAuth2PasswordRequestForm
from fastapi import APIRouter, Depends, HTTPException, status from fastapi import APIRouter, Depends, HTTPException, status
from datetime import timedelta from datetime import timedelta
from app.db.session import get_db
from app.core import security
from app.core.auth import authenticate_user, sign_up_new_user from app.core.auth import authenticate_user, sign_up_new_user
from app.core import security,tenantCacheService
from app.core.dbmanager import get_db
from sqlalchemy.orm import Session
auth_router = r = APIRouter() auth_router = r = APIRouter()
@r.post("/token") @r.post("/token")
async def login( async def login(request: Request,db:Session= Depends(get_db) ,form_data: OAuth2PasswordRequestForm = Depends()):
db=Depends(get_db), form_data: OAuth2PasswordRequestForm = Depends() if not db :
):
user = authenticate_user(db, form_data.username, form_data.password)
if not user:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password", detail="Incorrect username or password",
headers={"WWW-Authenticate": "Bearer"}, headers={"WWW-Authenticate": "Bearer"},
) )
user = authenticate_user(db, form_data.username, form_data.password)
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="abcIncorrect username or password",
headers={"WWW-Authenticate": "Bearer"},
)
access_token_expires = timedelta( access_token_expires = timedelta(
minutes=security.ACCESS_TOKEN_EXPIRE_MINUTES minutes=security.ACCESS_TOKEN_EXPIRE_MINUTES
) )
@@ -33,12 +43,16 @@ async def login(
permissions =";".join(list(set(perlst))) permissions =";".join(list(set(perlst)))
access_token = security.create_access_token( access_token = security.create_access_token(
data={"sub": user.id, "roles":roles,"permissions": permissions ,}, data={"sub": user.id,"roles":roles,"permissions": permissions,"tenant":user.tenantid,},
expires_delta=access_token_expires, expires_delta=access_token_expires,
) )
request.state.user = user.id
return {"access_token": access_token, "token_type": "bearer","user_name":user.first_name + " " + user.last_name} return JSONResponse(
status_code=200,
content={"access_token": access_token, "token_type": "bearer","user_name":user.first_name + " " + user.last_name}
)
@r.post("/signup") @r.post("/signup")
async def signup( async def signup(

View File

@@ -8,26 +8,26 @@ import deepdiff
import app.core.config as config import app.core.config as config
import os import os
from pathlib import Path from pathlib import Path
from app.db.session import SessionLocal from app.core.dbmanager import get_db
from app.db.crud import get_flows_by_app,get_kintoneformat from app.db.crud import get_flows_by_app,get_kintoneformat
from app.core.auth import get_current_active_user,get_current_user from app.core.auth import get_current_active_user,get_current_user
from app.core.apiexception import APIException from app.core.apiexception import APIException
from app.db.cruddb.dbdomain import dbdomain from app.db.cruddb import domainService,appService
kinton_router = r = APIRouter() kinton_router = r = APIRouter()
def getkintoneenv(user = Depends(get_current_user)): def getkintoneenv(user = Depends(get_current_user),db = Depends(get_db)):
db = SessionLocal() #db = SessionLocal()
domain = dbdomain.get_default_domain(db,user.id) #get_activedomain(db, user.id) domain = domainService.get_default_domain(db,user.id) #get_activedomain(db, user.id)
db.close() #db.close()
kintoneevn = config.KINTONE_ENV(domain) kintoneevn = config.KINTONE_ENV(domain)
return kintoneevn return kintoneevn
def getkintoneformat(): def getkintoneformat(db,user = Depends(get_current_user)):
db = SessionLocal() #db = SessionLocal()
formats = get_kintoneformat(db) formats = get_kintoneformat(db)
db.close() #db.close()
return formats return formats
@@ -452,10 +452,10 @@ def getTempPath(filename):
fpath = os.path.join(rootdir,"Temp",filename) fpath = os.path.join(rootdir,"Temp",filename)
return fpath return fpath
def createappjs(domain_url,app): def createappjs(domain_url,app,db):
db = SessionLocal() #db = SessionLocal()
flows = get_flows_by_app(db,domain_url,app) flows = appService.get_flow(db,domain_url,app) #get_flows_by_app(db,domain_url,app)
db.close() #db.close()
content={} content={}
for flow in flows: for flow in flows:
content[flow.eventid] = {'flowid':flow.flowid,'name':flow.name,'content':flow.content} content[flow.eventid] = {'flowid':flow.flowid,'name':flow.name,'content':flow.content}
@@ -627,9 +627,9 @@ async def createapp(request:Request,name:str,env:config.KINTONE_ENV=Depends(getk
@r.post("/createappfromexcel",) @r.post("/createappfromexcel",)
async def createappfromexcel(request:Request,files:t.List[UploadFile] = File(...),format:int = 0,env = Depends(getkintoneenv)): async def createappfromexcel(request:Request,files:t.List[UploadFile] = File(...),format:int = 0,env = Depends(getkintoneenv),db = Depends(get_db)):
try: try:
mapping = getkintoneformat()[format] mapping = getkintoneformat(db)[format]
except Exception as e: except Exception as e:
raise APIException('kintone:createappfromexcel',request.url._url, f"Error occurred while get kintone format:",e) raise APIException('kintone:createappfromexcel',request.url._url, f"Error occurred while get kintone format:",e)
@@ -666,9 +666,9 @@ async def createappfromexcel(request:Request,files:t.List[UploadFile] = File(...
@r.post("/updateappfromexcel") @r.post("/updateappfromexcel")
async def updateappfromexcel(request:Request,app:str,files:t.List[UploadFile] = File(...),format:int = 0,env = Depends(getkintoneenv)): async def updateappfromexcel(request:Request,app:str,files:t.List[UploadFile] = File(...),format:int = 0,env = Depends(getkintoneenv),db = Depends(get_db)):
try: try:
mapping = getkintoneformat()[format] mapping = getkintoneformat(db)[format]
except Exception as e: except Exception as e:
raise APIException('kintone:updateappfromexcel',request.url._url, f"Error occurred while get kintone format:",e) raise APIException('kintone:updateappfromexcel',request.url._url, f"Error occurred while get kintone format:",e)
@@ -758,11 +758,11 @@ async def updateprocessfromexcel(request:Request,app:str,env = Depends(getkinton
@r.post("/createjstokintone",) @r.post("/createjstokintone",)
async def createjstokintone(request:Request,app:str,env:config.KINTONE_ENV = Depends(getkintoneenv)): async def createjstokintone(request:Request,app:str,env:config.KINTONE_ENV = Depends(getkintoneenv),db = Depends(get_db)):
try: try:
jscs=[] jscs=[]
files=[] files=[]
files.append(createappjs(env.BASE_URL, app)) files.append(createappjs(env.BASE_URL, app, db))
files.append(getTempPath('alc_runtime.js')) files.append(getTempPath('alc_runtime.js'))
files.append(getTempPath('alc_runtime.css')) files.append(getTempPath('alc_runtime.css'))
for file in files: for file in files:

View File

@@ -2,8 +2,8 @@ from http import HTTPStatus
from fastapi import Query, Request,Depends, APIRouter, UploadFile,HTTPException,File from fastapi import Query, Request,Depends, APIRouter, UploadFile,HTTPException,File
from fastapi.responses import JSONResponse from fastapi.responses import JSONResponse
# from app.core.operation import log_operation # from app.core.operation import log_operation
from app.db import Base,engine # from app.db import Base,engine
from app.db.session import get_db from app.core.dbmanager import get_db
from app.db.crud import * from app.db.crud import *
from app.db.schemas import * from app.db.schemas import *
from typing import List, Optional from typing import List, Optional
@@ -11,10 +11,11 @@ from app.core.auth import get_current_active_user,get_current_user
from app.core.apiexception import APIException from app.core.apiexception import APIException
from app.core.common import ApiReturnModel,ApiReturnPage from app.core.common import ApiReturnModel,ApiReturnPage
#from fastapi_pagination import Page #from fastapi_pagination import Page
from app.db.cruddb import dbdomain from app.db.cruddb import domainService,appService
import httpx import httpx
import app.core.config as config import app.core.config as config
from app.core import domainCacheService,tenantCacheService
platform_router = r = APIRouter() platform_router = r = APIRouter()
@@ -27,7 +28,7 @@ async def test(
user = Depends(get_current_active_user), user = Depends(get_current_active_user),
db=Depends(get_db), db=Depends(get_db),
): ):
dbdomain.select(db,{"tenantid":1,"name":["b","c"]}) domainService.select(db,{"tenantid":1,"name":["b","c"]})
@r.get( @r.get(
@@ -42,11 +43,11 @@ async def apps_list(
): ):
try: try:
domain = dbdomain.get_default_domain(db,user.id) #get_activedomain(db, user.id) domain = domainService.get_default_domain(db,user.id) #get_activedomain(db, user.id)
if not domain: if not domain:
return ApiReturnModel(data = None) return ApiReturnModel(data = None)
filtered_apps = [] filtered_apps = []
platformapps = get_apps(db,domain.url) platformapps = appService.get_apps(db,domain.url)
kintoneevn = config.KINTONE_ENV(domain) kintoneevn = config.KINTONE_ENV(domain)
headers={config.API_V1_AUTH_KEY:kintoneevn.API_V1_AUTH_VALUE} headers={config.API_V1_AUTH_KEY:kintoneevn.API_V1_AUTH_VALUE}
url = f"{kintoneevn.BASE_URL}{config.API_V1_STR}/apps.json" url = f"{kintoneevn.BASE_URL}{config.API_V1_STR}/apps.json"
@@ -72,34 +73,80 @@ async def apps_list(
except Exception as e: except Exception as e:
raise APIException('platform:apps',request.url._url,f"Error occurred while get apps:",e) raise APIException('platform:apps',request.url._url,f"Error occurred while get apps:",e)
@r.post("/apps", tags=["App"],
@r.post("/apps", response_model=AppList, response_model_exclude_none=True) response_model=ApiReturnModel[AppList|None],
response_model_exclude_none=True)
async def apps_update( async def apps_update(
request: Request, request: Request,
app: AppVersion, app: VersionUpdate,
user=Depends(get_current_active_user), user=Depends(get_current_active_user),
db=Depends(get_db), db=Depends(get_db),
): ):
try: try:
return update_appversion(db, app,user.id) domainurl = domainCacheService.get_default_domainurl(db,user.id)
if not domainurl:
return ApiReturnModel(data = None)
return ApiReturnModel(data =appService.update_appversion(db, domainurl,app,user.id))
except Exception as e: except Exception as e:
raise APIException('platform:apps',request.url._url,f"Error occurred while get create app :",e) raise APIException('platform:apps',request.url._url,f"Error occurred while get create app :",e)
@r.delete( @r.delete("/apps/{appid}",tags=["App"],
"/apps/{domainurl}/{appid}", response_model_exclude_none=True response_model=ApiReturnModel[AppList|None],
response_model_exclude_none=True
) )
async def apps_delete( async def apps_delete(
request: Request, request: Request,
domainurl:str,
appid: str, appid: str,
user=Depends(get_current_active_user), user=Depends(get_current_active_user),
db=Depends(get_db), db=Depends(get_db),
): ):
try: try:
return delete_apps(db, domainurl,appid) domainurl = domainCacheService.get_default_domainurl(db,user.id) #get_activedomain(db, user.id)
if not domainurl:
return ApiReturnModel(data = None)
return ApiReturnModel(data =appService.delete_app(db, domainurl,appid))
except Exception as e: except Exception as e:
raise APIException('platform:apps',request.url._url,f"Error occurred while delete apps({domainurl}:{appid}):",e) raise APIException('platform:apps',request.url._url,f"Error occurred while delete app({appid}):",e)
@r.get(
"/appversions/{appid}",tags=["App"],
response_model=ApiReturnPage[AppVersion|None],
response_model_exclude_none=True,
)
async def appversions_list(
request: Request,
appid: str,
user = Depends(get_current_active_user),
db=Depends(get_db),
):
try:
domainurl = domainCacheService.get_default_domainurl(db,user.id) #get_activedomain(db, user.id)
if not domainurl:
return ApiReturnPage(data = None)
return appService.get_appversions(db,domainurl,appid)
except Exception as e:
raise APIException('platform:appversions',request.url._url,f"Error occurred while get app({appid}) version :",e)
@r.put(
"/appversions/{appid}/{version}",tags=["App"],
response_model=ApiReturnModel[AppList|None],
response_model_exclude_none=True
)
async def appversions_change(
request: Request,
appid: str,
version: int,
user = Depends(get_current_active_user),
db=Depends(get_db),
):
try:
domainurl = domainCacheService.get_default_domainurl(db,user.id) #get_activedomain(db, user.id)
if not domainurl:
ApiReturnModel(data = None)
return ApiReturnModel(data = appService.change_appversion(db, domainurl,appid,version,user.id))
except Exception as e:
raise APIException('platform:appversions',request.url._url,f"Error occurred while change app version:",e)
@r.get( @r.get(
"/appsettings/{id}", "/appsettings/{id}",
response_model=App, response_model=App,
@@ -190,24 +237,26 @@ async def action_data(
raise APIException('platform:actions',request.url._url,f"Error occurred while get actions:",e) raise APIException('platform:actions',request.url._url,f"Error occurred while get actions:",e)
@r.get( @r.get(
"/flow/{flowid}", "/flow/{appid}",tags=["App"],
response_model=Flow, response_model=ApiReturnModel[List[Flow]|None],
response_model_exclude_none=True, response_model_exclude_none=True,
) )
async def flow_details( async def flow_details(
request: Request, request: Request,
flowid: str, appid: str,
user=Depends(get_current_active_user),
db=Depends(get_db), db=Depends(get_db),
): ):
try: try:
app = get_flow(db, flowid) domainurl = domainCacheService.get_default_domainurl(db,user.id)
return app if not domainurl:
return ApiReturnModel(data = None)
return ApiReturnModel(data = appService.get_flow(db, domainurl, appid))
except Exception as e: except Exception as e:
raise APIException('platform:flow',request.url._url,f"Error occurred while get flow by flowid:",e) raise APIException('platform:flow',request.url._url,f"Error occurred while get flow by flowid:",e)
@r.get( @r.get(
"/flows/{appid}", "/flows/{appid}", tags=["App"],
response_model=List[Flow|None], response_model=List[Flow|None],
response_model_exclude_none=True, response_model_exclude_none=True,
) )
@@ -218,17 +267,19 @@ async def flow_list(
db=Depends(get_db), db=Depends(get_db),
): ):
try: try:
domain = dbdomain.get_default_domain(db, user.id) #get_activedomain(db, user.id) domainurl = domainCacheService.get_default_domainurl(db,user.id) #get_activedomain(db, user.id)
if not domain: if not domainurl:
return [] return []
print("domain=>",domain) #flows = get_flows_by_app(db, domainurl, appid)
flows = get_flows_by_app(db, domain.url, appid) flows = appService.get_flow(db,domainurl,appid)
return flows return flows
except Exception as e: except Exception as e:
raise APIException('platform:flow',request.url._url,f"Error occurred while get flow by appid:",e) raise APIException('platform:flow',request.url._url,f"Error occurred while get flow by appid:",e)
@r.post("/flow", response_model=Flow|None, response_model_exclude_none=True) @r.post("/flow", tags=["App"],
response_model=ApiReturnModel[Flow|None],
response_model_exclude_none=True)
async def flow_create( async def flow_create(
request: Request, request: Request,
flow: FlowIn, flow: FlowIn,
@@ -236,43 +287,50 @@ async def flow_create(
db=Depends(get_db), db=Depends(get_db),
): ):
try: try:
domain = dbdomain.get_default_domain(db, user.id) #get_activedomain(db, user.id) domainurl = domainCacheService.get_default_domainurl(db,user.id) #get_activedomain(db, user.id)
if not domain: if not domainurl:
return None return ApiReturnModel(data = None)
return create_flow(db, domain.url, flow) return ApiReturnModel(data = appService.create_flow(db, domainurl, flow,user.id))
except Exception as e: except Exception as e:
raise APIException('platform:flow',request.url._url,f"Error occurred while create flow:",e) raise APIException('platform:flow',request.url._url,f"Error occurred while create flow:",e)
@r.put( @r.put(
"/flow/{flowid}", response_model=Flow|None, response_model_exclude_none=True "/flow", tags=["App"],
response_model=ApiReturnModel[Flow|None],
response_model_exclude_none=True
) )
async def flow_edit( async def flow_edit(
request: Request, request: Request,
flowid: str,
flow: FlowIn, flow: FlowIn,
user=Depends(get_current_active_user), user=Depends(get_current_active_user),
db=Depends(get_db), db=Depends(get_db),
): ):
try: try:
domain = dbdomain.get_default_domain(db, user.id) #get_activedomain(db, user.id) domainurl = domainCacheService.get_default_domainurl(db,user.id)
if not domain: if not domainurl:
return None return ApiReturnModel(data = None)
return edit_flow(db,domain.url, flow,user.id) return ApiReturnModel(data = appService.edit_flow(db,domainurl, flow,user.id))
except Exception as e: except Exception as e:
raise APIException('platform:flow',request.url._url,f"Error occurred while edit flow:",e) raise APIException('platform:flow',request.url._url,f"Error occurred while edit flow:",e)
@r.delete( @r.delete(
"/flow/{flowid}", response_model=Flow, response_model_exclude_none=True "/flow/{flowid}", tags=["App"],
response_model=ApiReturnModel[Flow|None],
response_model_exclude_none=True
) )
async def flow_delete( async def flow_delete(
request: Request, request: Request,
flowid: str, flowid: str,
user=Depends(get_current_active_user),
db=Depends(get_db), db=Depends(get_db),
): ):
try: try:
return delete_flow(db, flowid) domainurl = domainCacheService.get_default_domainurl(db,user.id)
if not domainurl:
return ApiReturnModel(data = None)
return ApiReturnModel(data = appService.delete_flow(db, flowid))
except Exception as e: except Exception as e:
raise APIException('platform:flow',request.url._url,f"Error occurred while delete flow:",e) raise APIException('platform:flow',request.url._url,f"Error occurred while delete flow:",e)
@@ -281,20 +339,36 @@ async def flow_delete(
response_model=ApiReturnPage[Domain], response_model=ApiReturnPage[Domain],
response_model_exclude_none=True, response_model_exclude_none=True,
) )
async def domain_details( async def domain_list(
request: Request, request: Request,
user=Depends(get_current_active_user), user=Depends(get_current_active_user),
db=Depends(get_db), db=Depends(get_db),
): ):
try: try:
if user.is_superuser: if user.is_superuser:
domains = dbdomain.get_domains(db) domains = domainService.get_domains(db)
else: else:
domains = dbdomain.get_domains_by_owner(db,user.id) domains = domainService.get_domains_by_manage(db,user.id)
return domains return domains
except Exception as e: except Exception as e:
raise APIException('platform:domains',request.url._url,f"Error occurred while get domains:",e) raise APIException('platform:domains',request.url._url,f"Error occurred while get domains:",e)
@r.get(
"/domain/{domain_id}",tags=["Domain"],
response_model=ApiReturnModel[Domain|None],
response_model_exclude_none=True,
)
async def domain_detail(
request: Request,
domain_id:int,
user=Depends(get_current_active_user),
db=Depends(get_db),
):
try:
return ApiReturnModel(data = domainService.get(db,domain_id))
except Exception as e:
raise APIException('platform:domain',request.url._url,f"Error occurred while get domain detail:",e)
@r.post("/domain", tags=["Domain"], @r.post("/domain", tags=["Domain"],
response_model=ApiReturnModel[Domain], response_model=ApiReturnModel[Domain],
response_model_exclude_none=True) response_model_exclude_none=True)
@@ -305,7 +379,7 @@ async def domain_create(
db=Depends(get_db), db=Depends(get_db),
): ):
try: try:
return ApiReturnModel(data = dbdomain.create_domain(db, domain,user.id)) return ApiReturnModel(data = domainService.create_domain(db, domain,user.id))
except Exception as e: except Exception as e:
raise APIException('platform:domain',request.url._url,f"Error occurred while create domain:",e) raise APIException('platform:domain',request.url._url,f"Error occurred while create domain:",e)
@@ -322,7 +396,10 @@ async def domain_edit(
db=Depends(get_db), db=Depends(get_db),
): ):
try: try:
return ApiReturnModel(data = dbdomain.edit_domain(db, domain,user.id)) domain = domainService.edit_domain(db, domain,user.id)
if domain :
domainCacheService.clear_default_domainurl()
return ApiReturnModel(data = domain)
except Exception as e: except Exception as e:
raise APIException('platform:domain',request.url._url,f"Error occurred while edit domain:",e) raise APIException('platform:domain',request.url._url,f"Error occurred while edit domain:",e)
@@ -338,9 +415,9 @@ async def domain_delete(
db=Depends(get_db), db=Depends(get_db),
): ):
try: try:
return ApiReturnModel(data = dbdomain.delete_domain(db,id)) return ApiReturnModel(data = domainService.delete_domain(db,id))
except Exception as e: except Exception as e:
raise APIException('platform:domain',request.url._url,f"Error occurred while delete domain:",e) raise APIException('platform:domain',request.url._url,f"Error occurred while delete domain({id}):",e)
@r.get( @r.get(
"/domain", "/domain",
@@ -357,28 +434,29 @@ async def userdomain_details(
domains = get_domain(db, userId if userId is not None else user.id) domains = get_domain(db, userId if userId is not None else user.id)
return domains return domains
except Exception as e: except Exception as e:
raise APIException('platform:domain',request.url._url,f"Error occurred while get user({user.id}) domain:",e) raise APIException('platform:userdomain',request.url._url,f"Error occurred while get user({user.id}) domain:",e)
@r.post( @r.post(
"/domain/{userid}",tags=["Domain"], "/userdomain",tags=["Domain"],
response_model=ApiReturnModel[DomainOut|None], response_model=ApiReturnModel[DomainOut|None],
response_model_exclude_none=True, response_model_exclude_none=True,
) )
async def create_userdomain( async def create_userdomain(
request: Request, request: Request,
userid: int, userdomain:UserDomainParam,
domainid:int ,
user=Depends(get_current_active_user), user=Depends(get_current_active_user),
db=Depends(get_db), db=Depends(get_db),
): ):
try: try:
userid = userdomain.userid
domainid = userdomain.domainid
if user.is_superuser: if user.is_superuser:
domain = dbdomain.add_userdomain(db,user.id,userid,domainid) domain = domainService.add_userdomain(db,user.id,userid,domainid)
else: else:
domain = dbdomain.add_userdomain_by_owner(db,user.id,userid,domainid) domain = domainService.add_userdomain_by_owner(db,user.id,userid,domainid)
return ApiReturnModel(data = domain) return ApiReturnModel(data = domain)
except Exception as e: except Exception as e:
raise APIException('platform:domain',request.url._url,f"Error occurred while add user({userid}) domain:",e) raise APIException('platform:userdomain',request.url._url,f"Error occurred while add user({userid}) domain({domainid}):",e)
@r.delete( @r.delete(
"/domain/{domainid}/{userid}",tags=["Domain"], "/domain/{domainid}/{userid}",tags=["Domain"],
@@ -393,11 +471,70 @@ async def delete_userdomain(
db=Depends(get_db), db=Depends(get_db),
): ):
try: try:
return ApiReturnModel(data = dbdomain.delete_userdomain(db,userid,domainid)) return ApiReturnModel(data = domainService.delete_userdomain(db,userid,domainid))
except Exception as e: except Exception as e:
raise APIException('platform:delete',request.url._url,f"Error occurred while delete user({userid}) domain:",e) raise APIException('platform:userdomain',request.url._url,f"Error occurred while delete user({userid}) domain({domainid}):",e)
@r.get(
"/managedomainuser/{domainid}",tags=["Domain"],
response_model=ApiReturnPage[UserOut|None],
response_model_exclude_none=True,
)
async def get_managedomainuser(
request: Request,
domainid:int,
user=Depends(get_current_active_user),
db=Depends(get_db),
):
try:
return domainService.get_managedomain_users(db,domainid)
except Exception as e:
raise APIException('platform:managedomain',request.url._url,f"Error occurred while get managedomain({user.id}) user:",e)
@r.post(
"/managedomain",tags=["Domain"],
response_model=ApiReturnModel[DomainOut|None],
response_model_exclude_none=True,
)
async def create_managedomain(
request: Request,
userdomain:UserDomainParam,
user=Depends(get_current_active_user),
db=Depends(get_db),
):
try:
userid = userdomain.userid
domainid = userdomain.domainid
if user.is_superuser:
domain = domainService.add_managedomain(db,user.id,userid,domainid)
else:
domain = domainService.add_managedomain_by_owner(db,user.id,userid,domainid)
return ApiReturnModel(data = domain)
except Exception as e:
raise APIException('platform:managedomain',request.url._url,f"Error occurred while add manage({userid}) domain({domainid}):",e)
@r.delete(
"/managedomain/{domainid}/{userid}",tags=["Domain"],
response_model=ApiReturnModel[DomainOut|None],
response_model_exclude_none=True,
)
async def delete_managedomain(
request: Request,
domainid:int,
userid: int,
user=Depends(get_current_active_user),
db=Depends(get_db),
):
try:
return ApiReturnModel(data = domainService.delete_managedomain(db,userid,domainid))
except Exception as e:
raise APIException('platform:managedomain',request.url._url,f"Error occurred while delete managedomain({userid}) domain({domainid}):",e)
@r.get( @r.get(
"/defaultdomain",tags=["Domain"], "/defaultdomain",tags=["Domain"],
response_model=ApiReturnModel[DomainOut|None], response_model=ApiReturnModel[DomainOut|None],
@@ -409,7 +546,7 @@ async def get_defaultuserdomain(
db=Depends(get_db), db=Depends(get_db),
): ):
try: try:
return ApiReturnModel(data =dbdomain.get_default_domain(db, user.id)) return ApiReturnModel(data =domainService.get_default_domain(db,user.id))
except Exception as e: except Exception as e:
raise APIException('platform:defaultdomain',request.url._url,f"Error occurred while get user({user.id}) defaultdomain:",e) raise APIException('platform:defaultdomain',request.url._url,f"Error occurred while get user({user.id}) defaultdomain:",e)
@@ -418,14 +555,14 @@ async def get_defaultuserdomain(
response_model=ApiReturnModel[DomainOut|None], response_model=ApiReturnModel[DomainOut|None],
response_model_exclude_none=True, response_model_exclude_none=True,
) )
async def update_activeuserdomain( async def set_defualtuserdomain(
request: Request, request: Request,
domainid:int, domainid:int,
user=Depends(get_current_active_user), user=Depends(get_current_active_user),
db=Depends(get_db), db=Depends(get_db),
): ):
try: try:
domain = dbdomain.set_default_domain(db,user.id,domainid) domain = domainCacheService.set_default_domain(db,user.id,domainid)
return ApiReturnModel(data= domain) return ApiReturnModel(data= domain)
except Exception as e: except Exception as e:
raise APIException('platform:defaultdomain',request.url._url,f"Error occurred while update user({user.id}) defaultdomain:",e) raise APIException('platform:defaultdomain',request.url._url,f"Error occurred while update user({user.id}) defaultdomain:",e)
@@ -443,7 +580,7 @@ async def get_domainshareduser(
db=Depends(get_db), db=Depends(get_db),
): ):
try: try:
return dbdomain.get_shareddomain_users(db,user.id,domainid) return domainService.get_shareddomain_users(db,domainid)
except Exception as e: except Exception as e:
raise APIException('platform:sharedomain',request.url._url,f"Error occurred while get user({user.id}) sharedomain:",e) raise APIException('platform:sharedomain',request.url._url,f"Error occurred while get user({user.id}) sharedomain:",e)

View File

@@ -2,7 +2,7 @@ from fastapi import APIRouter, Request, Depends, Response, Security, encoders
import typing as t import typing as t
from app.core.common import ApiReturnModel,ApiReturnPage from app.core.common import ApiReturnModel,ApiReturnPage
from app.core.apiexception import APIException from app.core.apiexception import APIException
from app.db.session import get_db from app.core.dbmanager import get_db
from app.db.crud import ( from app.db.crud import (
get_allusers, get_allusers,
get_users, get_users,
@@ -13,9 +13,10 @@ from app.db.crud import (
assign_userrole, assign_userrole,
get_roles, get_roles,
) )
from app.db.schemas import UserCreate, UserEdit, User, UserOut,RoleBase,Permission from app.db.schemas import UserCreate, UserEdit, User, UserOut,RoleBase,AssignUserRoles,Permission
from app.core.auth import get_current_user,get_current_active_user, get_current_active_superuser from app.core.auth import get_current_user,get_current_active_user, get_current_active_superuser
from app.db.cruddb import dbuser from app.db.cruddb import userService
from app.core import tenantCacheService
users_router = r = APIRouter() users_router = r = APIRouter()
@@ -32,9 +33,9 @@ async def users_list(
): ):
try: try:
if current_user.is_superuser: if current_user.is_superuser:
users = dbuser.get_users(db) users = userService.get_users(db)
else: else:
users = dbuser.get_users_not_admin(db) users = userService.get_users_not_admin(db)
return users return users
except Exception as e: except Exception as e:
raise APIException('user:users',request.url._url,f"Error occurred while get user list",e) raise APIException('user:users',request.url._url,f"Error occurred while get user list",e)
@@ -59,7 +60,7 @@ async def user_details(
current_user=Depends(get_current_active_user), current_user=Depends(get_current_active_user),
): ):
try: try:
user = dbuser.get(db, user_id) user = userService.get(db, user_id)
if user: if user:
if user.is_superuser and not current_user.is_superuser: if user.is_superuser and not current_user.is_superuser:
user = None user = None
@@ -81,7 +82,7 @@ async def user_create(
try: try:
if user.is_superuser and not current_user.is_superuser: if user.is_superuser and not current_user.is_superuser:
return ApiReturnModel(data = None) return ApiReturnModel(data = None)
return ApiReturnModel(data =dbuser.create_user(db, user,current_user.id)) return ApiReturnModel(data =userService.create_user(db, user,current_user.id))
except Exception as e: except Exception as e:
raise APIException('user:users',request.url._url,f"Error occurred while create user({user.email}):",e) raise APIException('user:users',request.url._url,f"Error occurred while create user({user.email}):",e)
@@ -101,7 +102,7 @@ async def user_edit(
try: try:
if user.is_superuser and not current_user.is_superuser: if user.is_superuser and not current_user.is_superuser:
return ApiReturnModel(data = None) return ApiReturnModel(data = None)
return ApiReturnModel(data = dbuser.edit_user(db,user_id,user,current_user.id)) return ApiReturnModel(data = userService.edit_user(db,user_id,user,current_user.id))
except Exception as e: except Exception as e:
raise APIException('user:users',request.url._url,f"Error occurred while edit user({user_id}):",e) raise APIException('user:users',request.url._url,f"Error occurred while edit user({user_id}):",e)
@@ -117,10 +118,10 @@ async def user_delete(
current_user=Depends(get_current_active_user), current_user=Depends(get_current_active_user),
): ):
try: try:
user = dbuser.get(db,user_id) user = userService.get(db,user_id)
if user.is_superuser and not current_user.is_superuser: if user.is_superuser and not current_user.is_superuser:
return ApiReturnModel(data = None) return ApiReturnModel(data = None)
return ApiReturnModel(data = dbuser.delete_user(db, user_id)) return ApiReturnModel(data = userService.delete_user(db, user_id))
except Exception as e: except Exception as e:
raise APIException('user:users',request.url._url,f"Error occurred while delete user({user_id}):",e) raise APIException('user:users',request.url._url,f"Error occurred while delete user({user_id}):",e)
@@ -130,14 +131,13 @@ async def user_delete(
response_model_exclude_none=True,) response_model_exclude_none=True,)
async def assign_role( async def assign_role(
request: Request, request: Request,
user_id:int, userroles:AssignUserRoles,
roles:t.List[int],
db=Depends(get_db) db=Depends(get_db)
): ):
try: try:
return ApiReturnModel(data = dbuser.assign_userrole(db,user_id,roles)) return ApiReturnModel(data = userService.assign_userrole(db,userroles.userid,userroles.roleids))
except Exception as e: except Exception as e:
raise APIException('user:userrole',request.url._url,f"Error occurred while assign user({user_id}) roles({roles}):",e) raise APIException('user:userrole',request.url._url,f"Error occurred while assign user({userroles.userid}) roles({userroles.roleids}):",e)
@r.get( @r.get(
"/roles",tags=["User"], "/roles",tags=["User"],
@@ -151,11 +151,12 @@ async def roles_list(
#current_user=Security(get_current_active_user, scopes=["role_list"]), #current_user=Security(get_current_active_user, scopes=["role_list"]),
): ):
try: try:
if current_user.is_superuser: if current_user.is_superuser:
roles = dbuser.get_roles(db) roles = userService.get_roles(db)
else: else:
if len(current_user.roles)>0: if len(current_user.roles)>0:
roles = dbuser.get_roles_by_level(db,current_user.roles[0].level) roles = userService.get_roles_by_level(db,current_user.roles)
else: else:
roles = [] roles = []
return ApiReturnModel(data = roles) return ApiReturnModel(data = roles)
@@ -175,10 +176,10 @@ async def permssions_list(
): ):
try: try:
if current_user.is_superuser: if current_user.is_superuser:
permissions = dbuser.get_permissions(db) permissions = userService.get_permissions(db)
else: else:
if len(current_user.roles)>0: if len(current_user.roles)>0:
permissions = dbuser.get_user_permissions(db,current_user.id) permissions = userService.get_user_permissions(db,current_user.id)
else: else:
permissions = [] permissions = []
return ApiReturnModel(data = permissions) return ApiReturnModel(data = permissions)

View File

@@ -0,0 +1,2 @@
from app.core.cache import domainCacheService
from app.core.cache import tenantCacheService

View File

@@ -1,7 +1,7 @@
from fastapi import HTTPException, status from fastapi import HTTPException, status,Depends
import httpx import httpx
from app.db.schemas import ErrorCreate from app.db.schemas import ErrorCreate
from app.db.session import SessionLocal from app.core.dbmanager import get_log_db
from app.db.crud import create_log from app.db.crud import create_log
class APIException(Exception): class APIException(Exception):
@@ -31,9 +31,10 @@ class APIException(Exception):
self.error = ErrorCreate(location=location, title=title, content=content) self.error = ErrorCreate(location=location, title=title, content=content)
super().__init__(self.error) super().__init__(self.error)
def writedblog(exc: APIException): def writedblog(exc: APIException,):
db = SessionLocal() #db = SessionLocal()
db = get_log_db()
try: try:
create_log(db,exc.error) create_log(db,exc.error)
finally: finally:
db.close() db.close()

View File

@@ -3,13 +3,14 @@ import jwt
from fastapi import Depends, HTTPException, Request, Security, status from fastapi import Depends, HTTPException, Request, Security, status
from jwt import PyJWTError from jwt import PyJWTError
from app.db import models, schemas, session from app.db import models, schemas
from app.db.crud import get_user_by_email, create_user,get_user from app.db.crud import get_user_by_email, create_user,get_user
from app.core import security from app.core import security
from app.db.cruddb import dbuser from app.db.cruddb import userService
from app.core.dbmanager import get_db
async def get_current_user(security_scopes: SecurityScopes, async def get_current_user(request: Request,security_scopes: SecurityScopes,
db=Depends(session.get_db), token: str = Depends(security.oauth2_scheme) db=Depends(get_db), token: str = Depends(security.oauth2_scheme)
): ):
credentials_exception = HTTPException( credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, status_code=status.HTTP_401_UNAUTHORIZED,
@@ -17,7 +18,6 @@ async def get_current_user(security_scopes: SecurityScopes,
headers={"WWW-Authenticate": "Bearer"}, headers={"WWW-Authenticate": "Bearer"},
) )
try: try:
payload = jwt.decode( payload = jwt.decode(
token, security.SECRET_KEY, algorithms=[security.ALGORITHM] token, security.SECRET_KEY, algorithms=[security.ALGORITHM]
) )
@@ -25,6 +25,10 @@ async def get_current_user(security_scopes: SecurityScopes,
if id is None: if id is None:
raise credentials_exception raise credentials_exception
tenant:str = payload.get("tenant")
if tenant is None:
raise credentials_exception
permissions: str = payload.get("permissions") permissions: str = payload.get("permissions")
if not permissions =="ALL": if not permissions =="ALL":
for scope in security_scopes.scopes: for scope in security_scopes.scopes:
@@ -35,9 +39,10 @@ async def get_current_user(security_scopes: SecurityScopes,
token_data = schemas.TokenData(id = id, permissions=permissions) token_data = schemas.TokenData(id = id, permissions=permissions)
except PyJWTError: except PyJWTError:
raise credentials_exception raise credentials_exception
user = dbuser.get_user(db, token_data.id) user = userService.get_user(db, token_data.id)
if user is None: if user is None:
raise credentials_exception raise credentials_exception
request.state.user = user.id
return user return user
async def get_current_active_user( async def get_current_active_user(
@@ -59,11 +64,11 @@ async def get_current_active_superuser(
def authenticate_user(db, email: str, password: str): def authenticate_user(db, email: str, password: str):
user = get_user_by_email(db, email) user = userService.get_user_by_email(db,email) #get_user_by_email(db, email)
if not user: if not user:
return False return None
if not security.verify_password(password, user.hashed_password): if not security.verify_password(password, user.hashed_password):
return False return None
return user return user

72
backend/app/core/cache.py Normal file
View File

@@ -0,0 +1,72 @@
import time
from typing import Any
from sqlalchemy.orm import Session
from app.db.cruddb import domainService,tenantService
from app.db.session import Database
class MemoryCache:
def __init__(self, max_cache_size: int = 100, ttl: int = 60):
self.cache = {}
self.max_cache_size = max_cache_size
self.ttl = ttl
def get(self, key: str) -> Any:
item = self.cache.get(key)
if item:
if time.time() - item['timestamp'] > self.ttl:
self.cache.pop(key)
return None
return item['value']
return None
def set(self, key: str, value: Any) -> None:
if len(self.cache) >= self.max_cache_size:
self.cache.pop(next(iter(self.cache)))
self.cache[key] = {'value': value, 'timestamp': time.time()}
# def clear(self,key) -> None:
# self.cache.pop(key,None)
def clear(self) -> None:
self.cache.clear()
class domainCache:
def __init__(self):
self.memoryCache = MemoryCache(max_cache_size=50, ttl=120)
def set_default_domain(self, db: Session,userid: int,domainid:str):
domain = domainService.set_default_domain(db,userid,domainid)
if domain:
self.memoryCache.set(f"DOMAIN_{userid}",domain.url)
return domain
def get_default_domainurl(self,db: Session, userid: int):
if not self.memoryCache.get(f"DOMAIN_{userid}"):
domain = domainService.get_default_domain(db,userid)
if domain:
self.memoryCache.set(f"DOMAIN_{userid}",domain.url)
return self.memoryCache.get(f"DOMAIN_{userid}")
def clear_default_domainurl(self):
self.memoryCache.clear()
domainCacheService =domainCache()
class tenantCache:
def __init__(self):
self.memoryCache = MemoryCache(max_cache_size=50, ttl=120)
def get_tenant_db(self,db: Session, tenantid: str):
if not self.memoryCache.get(f"TENANT_{tenantid}"):
tenant = tenantService.get_tenant(db,tenantid)
if tenant:
database = Database(tenant.db)
self.memoryCache.set(f"TENANT_{tenantid}",database)
return self.memoryCache.get(f"TENANT_{tenantid}")
tenantCacheService =tenantCache()

View File

@@ -0,0 +1,20 @@
from fastapi import Depends,Request
from app.db.session import get_tenant_db
from app.core import tenantCacheService
from app.db.session import tenantdb
def get_db(request: Request,tenant:str = "1",tenantdb = Depends(get_tenant_db)):
database = tenantCacheService.get_tenant_db(tenantdb,tenant)
try:
db = database.get_db()
request.state.tenant = tenant
request.state.db = db
yield db
finally:
db.close()
def get_log_db():
db = tenantdb.get_db()
return db

View File

@@ -0,0 +1,75 @@
from fastapi import Request
from fastapi.responses import JSONResponse
from starlette.middleware.base import BaseHTTPMiddleware
from sqlalchemy.orm import Session
from app.db.models import OperationLog,User
from app.core.apiexception import APIException
from app.core.dbmanager import get_log_db
from app.db.crud import create_log
import json
class LoggingMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next):
if request.method in ("POST", "PUT", "PATCH","DELETE"):
try:
request.state.body = await request.json()
except json.JSONDecodeError:
request.state.body = await request.body()
else:
request.state.body = None
try:
response = await call_next(request)
state = request.state
except Exception as e:
await self.log_error(request, e)
response = JSONResponse(
content={"detail": "Internal Server Error"},
status_code=500
)
if hasattr(request.state, "user") and hasattr(request.state, "tenant"):
await self.log_request(request, response,state)
return response
async def log_request(self, request: Request, response,state):
try:
headers = dict(request.headers)
route = request.scope.get("route")
if route:
path_template = route.path
else:
path_template = request.url.path
db_operation = OperationLog(tenantid =request.state.tenant,
clientip = request.client.host if request.client else None,
useragent =headers.get("user-agent", ""),
userid = request.state.user,
operation = request.method,
function = path_template,
parameters = str({"path": request.path_params,"query": dict(request.query_params),"body": request.state.body}),
response = f"status_code:{response.status_code }" )
db = request.state.db
if db:
await self.write_log_to_db(db_operation,db)
except Exception as e:
print(f"Logging failed: {str(e)}")
async def log_error(self, request: Request, e: Exception):
exc = APIException('operation:dispatch',request.url._url,f"Error occurred while writting operation log:",e)
db = get_log_db()
try:
create_log(db,exc.error)
finally:
db.close()
async def write_log_to_db(self, db_operation,db):
db.add(db_operation)
db.commit()

View File

@@ -1,7 +1,7 @@
import jwt import jwt
from fastapi.security import OAuth2PasswordBearer from fastapi.security import OAuth2PasswordBearer
from passlib.context import CryptContext from passlib.context import CryptContext
from datetime import datetime, timedelta from datetime import datetime, timedelta,timezone
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms from cryptography.hazmat.primitives.ciphers import Cipher, algorithms
import os import os
import base64 import base64
@@ -27,9 +27,9 @@ def verify_password(plain_password: str, hashed_password: str) -> bool:
def create_access_token(*, data: dict, expires_delta: timedelta = None): def create_access_token(*, data: dict, expires_delta: timedelta = None):
to_encode = data.copy() to_encode = data.copy()
if expires_delta: if expires_delta:
expire = datetime.utcnow() + expires_delta expire = datetime.now(timezone.utc) + expires_delta
else: else:
expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) expire = datetime.now(timezone.utc) + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
to_encode.update({"exp": expire}) to_encode.update({"exp": expire})
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
return encoded_jwt return encoded_jwt

View File

@@ -1,2 +1,4 @@
from app.db.cruddb.dbuser import dbuser from app.db.cruddb.dbuser import userService
from app.db.cruddb.dbdomain import dbdomain from app.db.cruddb.dbdomain import domainService
from app.db.cruddb.dbapp import appService
from app.db.cruddb.dbtenant import tenantService

View File

@@ -1,4 +1,4 @@
from sqlalchemy import asc, desc from sqlalchemy import asc, desc, select
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from sqlalchemy.orm.query import Query from sqlalchemy.orm.query import Query
from typing import Type, List, Optional from typing import Type, List, Optional
@@ -46,11 +46,12 @@ class crudbase:
and_conditions.append(column == value) and_conditions.append(column == value)
if and_conditions: if and_conditions:
query = query.filter(*and_conditions) query = query.where(and_(*and_conditions))
if or_conditions: if or_conditions:
query = query.filter(or_(*or_conditions)) query = query.where(or_(*or_conditions))
return query
return query
def _apply_sorting(self, query: Query, sort_by: Optional[str], sort_order: Optional[str]) -> Query: def _apply_sorting(self, query: Query, sort_by: Optional[str], sort_order: Optional[str]) -> Query:
if sort_by: if sort_by:
column = getattr(self.model, sort_by, None) column = getattr(self.model, sort_by, None)
@@ -61,12 +62,11 @@ class crudbase:
query = query.order_by(asc(column)) query = query.order_by(asc(column))
return query return query
def get_all(self, db: Session) -> Query: def get_all(self) -> Query:
return db.query(self.model) return select(self.model)
def get(self, db: Session, item_id: int) -> Optional[models.Base]: def get(self, db: Session, item_id: int) -> Optional[models.Base]:
return db.query(self.model).get(item_id) return db.execute(select(self.model).filter(self.model.id == item_id)).scalar_one_or_none()
def create(self, db: Session, obj_in: BaseModel) -> models.Base: def create(self, db: Session, obj_in: BaseModel) -> models.Base:
db_obj = self.model(**obj_in.model_dump()) db_obj = self.model(**obj_in.model_dump())
@@ -76,7 +76,7 @@ class crudbase:
return db_obj return db_obj
def update(self, db: Session, item_id: int, obj_in: BaseModel) -> Optional[models.Base]: def update(self, db: Session, item_id: int, obj_in: BaseModel) -> Optional[models.Base]:
db_obj = db.query(self.model).filter(self.model.id == item_id).first() db_obj = self.get(db,item_id)
if db_obj: if db_obj:
for key, value in obj_in.model_dump(exclude_unset=True).items(): for key, value in obj_in.model_dump(exclude_unset=True).items():
setattr(db_obj, key, value) setattr(db_obj, key, value)
@@ -86,16 +86,16 @@ class crudbase:
return None return None
def delete(self, db: Session, item_id: int) -> Optional[models.Base]: def delete(self, db: Session, item_id: int) -> Optional[models.Base]:
db_obj = db.query(self.model).get(item_id) db_obj = self.get(db,item_id)
if db_obj: if db_obj:
db.delete(db_obj) db.delete(db_obj)
db.commit() db.commit()
return db_obj return db_obj
return None return None
def get_by_conditions(self, db: Session, filters: Optional[dict] = None, sort_by: Optional[str] = None, def get_by_conditions(self, filters: Optional[dict] = None, sort_by: Optional[str] = None,
sort_order: Optional[str] = "asc") -> Query: sort_order: Optional[str] = "asc") -> Query:
query = db.query(self.model) query = select(self.model)
if filters: if filters:
query = self._apply_filters(query, filters) query = self._apply_filters(query, filters)
if sort_by: if sort_by:

View File

@@ -0,0 +1,216 @@
from datetime import datetime
from fastapi import HTTPException, status
from sqlalchemy.orm import Session
from sqlalchemy import select,and_
import typing as t
from app.db.cruddb.crudbase import crudbase
from fastapi_pagination.ext.sqlalchemy import paginate
from app.core.common import ApiReturnPage
from app.db import models, schemas
from app.core.security import chacha20Decrypt, get_password_hash
class dbflowhistory(crudbase):
def __init__(self):
super().__init__(model=models.FlowHistory)
def get_flows_by_appid_version(self,db: Session,domainurl:str,appid:str,version:int):
return db.execute(super().get_by_conditions({"domainurl":domainurl,"appid":appid, "version":version})).scalars().all()
dbflowhistory = dbflowhistory()
class dbflow(crudbase):
def __init__(self):
super().__init__(model=models.Flow)
def get_domain_apps(self):
return None
def get_flow_by_flowid(self,db: Session,flowid:str):
return db.execute(super().get_by_conditions({"flowid":flowid})).scalars().first()
def get_flows_by_appid(self,db: Session,domainurl:str,appid:str):
return db.execute(select(models.Flow).filter(and_(models.Flow.domainurl == domainurl,models.Flow.appid == appid))).scalars().all()
def create_flow(self,db: Session, domainurl: str, flow: schemas.FlowIn,userid:int):
db_flow = models.Flow(
flowid=flow.flowid,
appid=flow.appid,
eventid=flow.eventid,
domainurl=domainurl,
name=flow.name,
content=flow.content,
createuserid = userid,
updateuserid = userid
)
db.add(db_flow)
db_app = db.execute(select(models.App).where(and_(models.App.domainurl == domainurl,models.App.appid == flow.appid))).scalars().first()
if not db_app:
db_app = models.App(
domainurl = domainurl,
appid=flow.appid,
appname=flow.appname,
version = 0,
createuserid= userid,
updateuserid = userid
)
db.add(db_app)
db.commit()
db.refresh(db_flow)
return db_flow
dbflow = dbflow()
class dbappversion(crudbase):
def __init__(self):
super().__init__(model=models.AppVersion)
def get_appversions(self,domainurl:str,appid:str):
return super().get_by_conditions({"domainurl":domainurl,"appid":appid})
def get_app_by_version(self,db: Session,domainurl:str,appid:str,version:int):
return db.execute(super().get_by_conditions({"domainurl":domainurl,"appid":appid,"version":version})).scalars().first()
def get_app_latestversion(self,db: Session,domainurl:str,appid:str):
appversion = db.execute(super().get_by_conditions({"domainurl":domainurl,"appid":appid},"version","desc")).scalars().first()
if appversion:
return appversion.version
else:
return 0
dbappversion = dbappversion()
class dbapp(crudbase):
def __init__(self):
super().__init__(model=models.App)
def get_app(self,db: Session,domainurl:str,appid:str):
return db.execute(super().get_by_conditions({"domainurl":domainurl,"appid":appid})).scalars().first()
def get_apps(self,db: Session,domainurl:str):
return db.execute(super().get_by_conditions({"domainurl":domainurl})).scalars().all()
def update_appversion(self,db: Session,domainurl, newversion: schemas.VersionUpdate,userid:int):
db_app = self.get_app(db,domainurl,newversion.appid)
if db_app:
db_app.version = dbappversion.get_app_latestversion(db,domainurl,newversion.appid)+1
db_app.updateuserid = userid,
db_app.versionname = newversion.versionname
db_app.is_saved = False
appversion = models.AppVersion(
domainurl = db_app.domainurl,
appid=db_app.appid,
appname=db_app.appname,
version = db_app.version,
versionname = newversion.versionname,
comment = newversion.comment,
updateuserid = userid,
createuserid = userid
)
db.add(appversion)
db.add(db_app)
flows = dbflow.get_flows_by_appid(db,domainurl,newversion.appid)
for flow in flows:
db_flowhistory = models.FlowHistory(
flowid = flow.flowid,
appid = flow.appid,
eventid = flow.eventid,
domainurl = flow.domainurl,
name = flow.name,
content = flow.content,
version = db_app.version,
updateuserid = userid,
createuserid = userid
)
db.add(db_flowhistory)
db.add(db_flowhistory)
db.commit()
db.refresh(db_app)
return db_app
return None
def change_appversion(self,db: Session, domainurl:str,appid:str,version:int,userid:int):
db_app = self.get_app(db, domainurl, appid)
if not db_app:
return None
db_appversion = dbappversion.get_app_by_version(db, domainurl, appid, version)
if not db_appversion:
return None
db_app.version = version
db_app.versionname = db_appversion.versionname
db_app.updateuserid = userid
db_app.is_saved = False
db.add(db_app)
flows = dbflow.get_flows_by_appid(db, domainurl, appid)
for flow in flows:
db.delete(flow)
db.flush()
flowhistorys = dbflowhistory.get_flows_by_appid_version(db, domainurl, appid, version)
for flow in flowhistorys:
db_flow = models.Flow(
flowid = flow.flowid,
appid = flow.appid,
eventid = flow.eventid,
domainurl = flow.domainurl,
name = flow.name,
content = flow.content,
updateuserid = userid,
createuserid = userid
)
db.add(db_flow)
db.commit()
db.refresh(db_app)
return db_app
def delete_app(self,db: Session, domainurl: str,appid: str ):
db_app =self.get_app(db,domainurl,appid)
if db_app:
flows = dbflow.get_flows_by_appid(db,domainurl,appid)
for flow in flows:
db.delete(flow)
db.delete(db_app)
db.commit()
return db_app
return None
def get_appversions(self,db: Session, domainurl:str,appid:str):
return paginate(db,dbappversion.get_appversions(domainurl,appid))
def get_flow(self,db: Session, domainurl: str, appid:str):
return dbflow.get_flows_by_appid(db,domainurl,appid)
def create_flow(self,db: Session, domainurl: str, flow: schemas.FlowIn,userid:int):
return dbflow.create_flow(db,domainurl,flow,userid)
def edit_flow(self,db: Session, domainurl: str, flow: schemas.FlowIn,userid:int):
db_flow = dbflow.get_flow_by_flowid(db, flow.flowid)
if not db_flow:
return dbflow.create_flow(db,domainurl,flow,userid)
db_flow.appid =flow.appid
db_flow.eventid=flow.eventid
db_flow.domainurl=domainurl
db_flow.name=flow.name
db_flow.content=flow.content
db_flow.updateuserid = userid
db.add(db_flow)
db_app = self.get_app(db, domainurl, flow.appid)
if db_app and db_app.version > 0:
db_app.is_saved = True
db.add(db_app)
db.commit()
db.refresh(db_flow)
return db_flow
def delete_flow(self,db: Session, flowid: str):
db_flow = dbflow.get_flow_by_flowid(db,flowid)
if db_flow:
return dbflow.delete(db,db_flow.id)
return None
appService = dbapp()

View File

@@ -1,7 +1,7 @@
from datetime import datetime from datetime import datetime
from fastapi import HTTPException, status from fastapi import HTTPException, status
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from sqlalchemy import and_ from sqlalchemy import select,and_
import typing as t import typing as t
from app.db.cruddb.crudbase import crudbase from app.db.cruddb.crudbase import crudbase
@@ -16,43 +16,76 @@ class dbuserdomain(crudbase):
super().__init__(model=models.UserDomain) super().__init__(model=models.UserDomain)
def get_userdomain(self,db: Session,userid:int,domainid:int): def get_userdomain(self,db: Session,userid:int,domainid:int):
return super().get_by_conditions(db,{"userid":userid,"domainid":domainid}).first() return db.execute(super().get_by_conditions({"userid":userid,"domainid":domainid})).scalars().first()
def get_userdomain_by_domainid(self,db: Session,ownerid:int,domainid:int): def get_userdomain_by_domainid(self,db: Session,ownerid:int,domainid:int):
return super().get_by_conditions(db,{"domainid":domainid}) return super().get_by_conditions({"domainid":domainid})
def get_default_domains(self,db: Session,domainid:int): def get_default_domains(self,db: Session,domainid:int):
return super().get_by_conditions(db,{"domainid":domainid,"is_default":True}).all() return db.execute(super().get_by_conditions({"domainid":domainid,"is_default":True})).scalars().all()
def get_user_default_domain(self,db: Session,userid:int): def get_user_default_domain(self,db: Session,userid:int):
return super().get_by_conditions(db,{"userid":userid,"is_default":True}).first() return db.execute(super().get_by_conditions({"userid":userid,"is_default":True})).scalars().first()
dbuserdomain = dbuserdomain() dbuserdomain = dbuserdomain()
class dbmanagedomain(crudbase):
def __init__(self):
super().__init__(model=models.ManageDomain)
def get_managedomain(self,db: Session,userid:int,domainid:int):
return db.execute(super().get_by_conditions({"userid":userid,"domainid":domainid})).scalars().first()
def get_managedomain_by_domain(self,db: Session,domainid:int):
return db.execute(super().get_by_conditions({"domainid":domainid})).scalars().all()
dbmanagedomain = dbmanagedomain()
class dbdomain(crudbase): class dbdomain(crudbase):
def __init__(self): def __init__(self):
super().__init__(model=models.Domain) super().__init__(model=models.Domain)
def get_domains(self,db: Session)-> ApiReturnPage[models.Base]: def get_domains(self,db: Session)-> ApiReturnPage[models.Base]:
return paginate(super().get_all(db)) return paginate(db,super().get_all())
def get_domains_by_manage(self,db: Session,userid:int)-> ApiReturnPage[models.Base]:
query = select(models.Domain).join(models.ManageDomain,models.ManageDomain.domainid == models.Domain.id).where(models.ManageDomain.userid == userid)
return paginate(db,query)
def get_domains_by_owner(self,db: Session,ownerid:int)-> ApiReturnPage[models.Base]: def get_domains_by_owner(self,db: Session,ownerid:int)-> ApiReturnPage[models.Base]:
return paginate( super().get_by_conditions(db,{"ownerid":ownerid})) return paginate(db,super().get_by_conditions({"ownerid":ownerid}))
def create_domain(self,db: Session, domain: schemas.DomainIn,userid:int): def create_domain(self,db: Session, domain: schemas.DomainIn,userid:int):
#db_domain = super().get_by_conditions(db,{"url":domain.url,"kintoneuser":domain.kintoneuser,"onwerid":userid}).first() #db_domain = super().get_by_conditions(db,{"url":domain.url,"kintoneuser":domain.kintoneuser,"onwerid":userid}).first()
#if not db_domain: #if not db_domain:
domain.encrypt_kintonepwd() domain.encrypt_kintonepwd()
domain.id = None db_domain = models.Domain(
domain.createuserid = userid tenantid = domain.tenantid,
domain.updateuserid = userid name = domain.name,
domain.ownerid = userid url = domain.url,
return super().create(db,domain) kintoneuser = domain.kintoneuser,
#return db_domain kintonepwd = domain.kintonepwd,
is_active = domain.is_active,
createuserid = userid,
updateuserid = userid,
ownerid = userid
)
db.add(db_domain)
db.flush()
user_domain = models.UserDomain(userid = userid, domainid = db_domain.id ,createuserid = userid,updateuserid = userid)
db.add(user_domain)
manage_domain = models.ManageDomain(userid = userid, domainid = db_domain.id ,createuserid = userid,updateuserid = userid)
db.add(manage_domain)
db.commit()
db.refresh(db_domain)
return db_domain
def delete_domain(self,db: Session,id: int): def delete_domain(self,db: Session,id: int):
db_managedomains = dbmanagedomain.get_managedomain_by_domain(db,id)
for manage in db_managedomains:
db.delete(manage)
return super().delete(db,id) return super().delete(db,id)
def edit_domain(self,db: Session, domain: schemas.DomainIn,userid:int) -> schemas.DomainOut: def edit_domain(self,db: Session, domain: schemas.DomainIn,userid:int) -> schemas.DomainOut:
@@ -79,8 +112,8 @@ class dbdomain(crudbase):
return None return None
def add_userdomain(self,db: Session,ownerid:int,userid:int,domainid:int) -> schemas.DomainOut: def add_userdomain(self,db: Session,ownerid:int,userid:int,domainid:int) -> schemas.DomainOut:
db_domain = super().get_by_conditions(db,{"id":domainid,"is_active":True}).first() db_domain = super().get(db,domainid)
if db_domain: if db_domain and db_domain.is_active:
db_userdomain = dbuserdomain.get_userdomain(db,userid,domainid) db_userdomain = dbuserdomain.get_userdomain(db,userid,domainid)
if not db_userdomain: if not db_userdomain:
user_domain = models.UserDomain(userid = userid, domainid = domainid ,createuserid = ownerid,updateuserid = ownerid) user_domain = models.UserDomain(userid = userid, domainid = domainid ,createuserid = ownerid,updateuserid = ownerid)
@@ -90,7 +123,7 @@ class dbdomain(crudbase):
return None return None
def add_userdomain_by_owner(self,db: Session,ownerid:int, userid:int,domainid:int) -> schemas.DomainOut: def add_userdomain_by_owner(self,db: Session,ownerid:int, userid:int,domainid:int) -> schemas.DomainOut:
db_domain = super().get_by_conditions(db,{"id":domainid,"ownerid":ownerid,"is_active":True}).first() db_domain = db.execute(super().get_by_conditions({"id":domainid,"is_active":True})).scalars().first()
if db_domain: if db_domain:
db_userdomain = dbuserdomain.get_userdomain(db,userid,domainid) db_userdomain = dbuserdomain.get_userdomain(db,userid,domainid)
if not db_userdomain: if not db_userdomain:
@@ -117,23 +150,69 @@ class dbdomain(crudbase):
return None return None
def set_default_domain(self,db: Session, userid: int,domainid: int): def set_default_domain(self,db: Session, userid: int,domainid: int):
db_domain =super().get_by_conditions(db,{"id":domainid,"is_active":True}).first() db_domain =db.execute(super().get_by_conditions({"id":domainid,"is_active":True})).scalars().first()
if db_domain: if db_domain:
db_default_domain = dbuserdomain.get_user_default_domain(db,userid) db_default_domain = dbuserdomain.get_user_default_domain(db,userid)
db_userdomain =dbuserdomain.get_userdomain(db,userid,domainid) db_userdomain =dbuserdomain.get_userdomain(db,userid,domainid)
if db_default_domain: if db_default_domain:
db_default_domain.is_default = False if db_default_domain.domainid != domainid:
db_default_domain.updateuserid = userid db_default_domain.is_default = False
db.add(db_default_domain) db_default_domain.updateuserid = userid
db.add(db_default_domain)
else:
return db_domain
if db_userdomain: if db_userdomain:
db_userdomain.is_default = True db_userdomain.is_default = True
db_userdomain.updateuserid = userid db_userdomain.updateuserid = userid
db.add(db_userdomain) db.add(db_userdomain)
else:
db_userdomain = dbuserdomain.create(db,schemas.UserDomainIn(domainid=domainid,userid=userid,is_default = True))
db.add(db_userdomain)
db.commit() db.commit()
return db_domain return db_domain
else:
return None
def get_shareddomain_users(self,db: Session,ownerid:int,domainid: int) -> ApiReturnPage[models.Base]:
users = db.query(models.User).join(models.UserDomain,models.UserDomain.userid == models.User.id).filter(models.UserDomain.domainid ==domainid) def get_shareddomain_users(self,db: Session,domainid: int) -> ApiReturnPage[models.Base]:
return paginate(users) users = select(models.User).join(models.UserDomain,models.UserDomain.userid == models.User.id).filter(models.UserDomain.domainid ==domainid)
return paginate(db,users)
dbdomain = dbdomain()
def add_managedomain(self,db: Session,ownerid:int,userid:int,domainid:int) -> schemas.DomainOut:
db_domain = self.get(db,domainid)
if db_domain:
db_managedomain = dbmanagedomain.get_managedomain(db,userid,domainid)
if not db_managedomain:
manage_domain = models.ManageDomain(userid = userid, domainid = domainid ,createuserid = ownerid,updateuserid = ownerid)
db.add(manage_domain)
db.commit()
return db_domain
return None
def add_managedomain_by_owner(self,db: Session,ownerid:int, userid:int,domainid:int) -> schemas.DomainOut:
db_domain = db.execute(super().get_by_conditions({"id":domainid,"ownerid":ownerid,})).scalars().first()
if db_domain:
db_managedomain = dbmanagedomain.get_managedomain(db,userid,domainid)
if not db_managedomain:
manage_domain = models.ManageDomain(userid = userid, domainid = domainid ,createuserid =ownerid,updateuserid = ownerid)
db.add(manage_domain)
db.commit()
return db_domain
return None
def delete_managedomain(self,db: Session, userid: int,domainid: int) -> schemas.DomainOut:
db_managedomain = dbmanagedomain.get_managedomain(db,userid,domainid)
if db_managedomain:
domain = db_managedomain.domain
if domain.ownerid != userid:
db.delete(db_managedomain)
db.commit()
return domain
return None
def get_managedomain_users(self,db: Session,domainid: int) -> ApiReturnPage[models.Base]:
users = select(models.User).join(models.ManageDomain,models.ManageDomain.userid == models.User.id).where(models.ManageDomain.domainid ==domainid)
return paginate(db,users)
domainService = dbdomain()

View File

@@ -0,0 +1,13 @@
from app.db.cruddb.crudbase import crudbase
from app.db import models, schemas
from sqlalchemy.orm import Session
class dbtenant(crudbase):
def __init__(self):
super().__init__(model=models.Tenant)
def get_tenant(sefl,db:Session,tenantid: str):
tenant = db.execute(super().get_by_conditions({"tenantid":tenantid})).scalars().first()
return tenant
tenantService = dbtenant()

View File

@@ -32,13 +32,13 @@ class dbuser(crudbase):
return super().get(db,user_id) return super().get(db,user_id)
def get_user_by_email(self,db: Session, email: str) -> schemas.User: def get_user_by_email(self,db: Session, email: str) -> schemas.User:
return super().get_by_conditions(db,{"email":email}).first() return db.execute(super().get_by_conditions({"email":email})).scalars().first()
def get_users(self,db: Session) -> ApiReturnPage[models.Base]: def get_users(self,db: Session) -> ApiReturnPage[models.Base]:
return paginate(super().get_all(db)) return paginate(db,super().get_all())
def get_users_not_admin(self,db: Session) -> ApiReturnPage[models.Base]: def get_users_not_admin(self,db: Session) -> ApiReturnPage[models.Base]:
return paginate(super().get_by_conditions(db,{"is_superuser":False})) return paginate(db,super().get_by_conditions({"is_superuser":False}))
def create_user(self,db: Session, user: schemas.UserCreate,userid:int): def create_user(self,db: Session, user: schemas.UserCreate,userid:int):
hashed_password = get_password_hash(user.password) hashed_password = get_password_hash(user.password)
@@ -60,26 +60,32 @@ class dbuser(crudbase):
return super().update(db,user_id,user) return super().update(db,user_id,user)
def get_roles(self,db: Session) -> t.List[schemas.RoleBase]: def get_roles(self,db: Session) -> t.List[schemas.RoleBase]:
return dbrole.get_all(db).all() return db.execute(dbrole.get_all()).scalars().all()
#return dbrole.get_all().all()
def get_roles_by_level(self,db: Session,level:int) -> t.List[schemas.RoleBase]: def get_roles_by_level(self,db: Session,roles:t.List[models.Role]) -> t.List[schemas.RoleBase]:
return dbrole.get_by_conditions(db,{"level":{"operator":">=","value":level}}).all() level = 99999
for role in roles:
if role.level < level:
level = role.level
return db.execute(dbrole.get_by_conditions({"level":{"operator":">","value":level}})).scalars().all()
def assign_userrole(self,db: Session, user_id: int, roles: t.List[int]): def assign_userrole(self,db: Session, user_id: int, roles: t.List[int]):
db_user = super().get(db,user_id) db_user = super().get(db,user_id)
if db_user: if db_user:
for role in db_user.roles: for role in db_user.roles:
db_user.roles.remove(role) if role.id not in roles:
db_user.roles.remove(role)
for roleid in roles: for roleid in roles:
role = dbrole.get(db,roleid) role = dbrole.get(db,roleid)
if role: if role not in db_user.roles:
db_user.roles.append(role) db_user.roles.append(role)
db.commit() db.commit()
db.refresh(db_user) db.refresh(db_user)
return db_user return db_user
def get_permissions(self,db: Session,user_id: int) -> t.List[schemas.Permission]: def get_permissions(self,db: Session) -> t.List[schemas.Permission]:
return dbpermission.get_all(db).all() return db.execute(dbpermission.get_all()).scalars().all()
def get_user_permissions(self,db: Session,user_id: int) -> t.List[schemas.Permission]: def get_user_permissions(self,db: Session,user_id: int) -> t.List[schemas.Permission]:
permissions =[] permissions =[]
@@ -89,4 +95,4 @@ class dbuser(crudbase):
permissions += role.permissions permissions += role.permissions
return list(set(permissions)) return list(set(permissions))
dbuser = dbuser() userService = dbuser()

View File

@@ -1,14 +1,14 @@
from sqlalchemy import Boolean, Column, Integer, String, DateTime,ForeignKey,Table from sqlalchemy import Boolean, Column, Integer, String, DateTime,ForeignKey,Table
from sqlalchemy.orm import relationship,as_declarative from sqlalchemy.orm import Mapped,relationship,as_declarative,mapped_column
from datetime import datetime from datetime import datetime,timezone
from app.db.session import Base from app.db import Base
from app.core.security import chacha20Decrypt from app.core.security import chacha20Decrypt
@as_declarative() @as_declarative()
class Base: class Base:
id = Column(Integer, primary_key=True, index=True) id = Column(Integer, primary_key=True, index=True)
create_time = Column(DateTime, default=datetime.now) create_time = Column(DateTime, default=datetime.now(timezone.utc))
update_time = Column(DateTime, default=datetime.now, onupdate=datetime.now) update_time = Column(DateTime, default=datetime.now(timezone.utc), onupdate=datetime.now(timezone.utc))
userrole = Table( userrole = Table(
@@ -28,14 +28,15 @@ rolepermission = Table(
class User(Base): class User(Base):
__tablename__ = "user" __tablename__ = "user"
email = Column(String(50), unique=True, index=True, nullable=False) email = mapped_column(String(50), unique=True, index=True, nullable=False)
first_name = Column(String(100)) first_name = mapped_column(String(100))
last_name = Column(String(100)) last_name = mapped_column(String(100))
hashed_password = Column(String(200), nullable=False) hashed_password = mapped_column(String(200), nullable=False)
is_active = Column(Boolean, default=True) is_active = mapped_column(Boolean, default=True)
is_superuser = Column(Boolean, default=False) is_superuser = mapped_column(Boolean, default=False)
createuserid = Column(Integer,ForeignKey("user.id")) tenantid = mapped_column(String(100))
updateuserid = Column(Integer,ForeignKey("user.id")) createuserid = mapped_column(Integer,ForeignKey("user.id"))
updateuserid = mapped_column(Integer,ForeignKey("user.id"))
createuser = relationship('User',foreign_keys=[createuserid]) createuser = relationship('User',foreign_keys=[createuserid])
updateuser = relationship('User',foreign_keys=[updateuserid]) updateuser = relationship('User',foreign_keys=[updateuserid])
roles = relationship("Role",secondary=userrole,back_populates="users") roles = relationship("Role",secondary=userrole,back_populates="users")
@@ -44,127 +45,132 @@ class User(Base):
class Role(Base): class Role(Base):
__tablename__ = "role" __tablename__ = "role"
name = Column(String(100)) name = mapped_column(String(100))
description = Column(String(255)) description = mapped_column(String(255))
level = Column(Integer) level = mapped_column(Integer)
users = relationship("User",secondary=userrole,back_populates="roles") users = relationship("User",secondary=userrole,back_populates="roles")
permissions = relationship("Permission",secondary=rolepermission,back_populates="roles") permissions = relationship("Permission",secondary=rolepermission,back_populates="roles")
class Permission(Base): class Permission(Base):
__tablename__ = "permission" __tablename__ = "permission"
menu = Column(String(100)) menu = mapped_column(String(100))
function = Column(String(255)) function = mapped_column(String(255))
privilege = Column(String(100)) link = mapped_column(String(100))
privilege = mapped_column(String(100))
roles = relationship("Role",secondary=rolepermission,back_populates="permissions") roles = relationship("Role",secondary=rolepermission,back_populates="permissions")
class App(Base): class App(Base):
__tablename__ = "app" __tablename__ = "app"
domainurl = Column(String(200), nullable=False) domainurl = mapped_column(String(200), nullable=False)
appname = Column(String(200), nullable=False) appname = mapped_column(String(200), nullable=False)
appid = Column(String(100), index=True, nullable=False) appid = mapped_column(String(100), index=True, nullable=False)
version = Column(Integer) version = mapped_column(Integer)
createuserid = Column(Integer,ForeignKey("user.id")) versionname = mapped_column(String(200), nullable=False)
updateuserid = Column(Integer,ForeignKey("user.id")) is_saved = mapped_column(Boolean, default=False)
createuserid = mapped_column(Integer,ForeignKey("user.id"))
updateuserid = mapped_column(Integer,ForeignKey("user.id"))
createuser = relationship('User',foreign_keys=[createuserid]) createuser = relationship('User',foreign_keys=[createuserid])
updateuser = relationship('User',foreign_keys=[updateuserid]) updateuser = relationship('User',foreign_keys=[updateuserid])
class AppVersion(Base): class AppVersion(Base):
__tablename__ = "appversion" __tablename__ = "appversion"
domainurl = Column(String(200), nullable=False) domainurl = mapped_column(String(200), nullable=False)
appname = Column(String(200), nullable=False) appname = mapped_column(String(200), nullable=False)
appid = Column(String(100), index=True, nullable=False) appid = mapped_column(String(100), index=True, nullable=False)
version = Column(Integer) version = mapped_column(Integer)
versionname = Column(String(200), nullable=False) versionname = mapped_column(String(200), nullable=False)
comment = Column(String(200), nullable=False) comment = mapped_column(String(200), nullable=False)
createuserid = Column(Integer,ForeignKey("user.id")) createuserid = mapped_column(Integer,ForeignKey("user.id"))
updateuserid = Column(Integer,ForeignKey("user.id")) updateuserid = mapped_column(Integer,ForeignKey("user.id"))
createuser = relationship('User',foreign_keys=[createuserid]) createuser = relationship('User',foreign_keys=[createuserid])
updateuser = relationship('User',foreign_keys=[updateuserid]) updateuser = relationship('User',foreign_keys=[updateuserid])
class AppSetting(Base): class AppSetting(Base):
__tablename__ = "appsetting" __tablename__ = "appsetting"
appid = Column(String(100), index=True, nullable=False) appid = mapped_column(String(100), index=True, nullable=False)
setting = Column(String(1000)) setting = mapped_column(String(1000))
class Kintone(Base): class Kintone(Base):
__tablename__ = "kintone" __tablename__ = "kintone"
type = Column(Integer, index=True, nullable=False) type = mapped_column(Integer, index=True, nullable=False)
name = Column(String(100), nullable=False) name = mapped_column(String(100), nullable=False)
desc = Column(String) desc = mapped_column(String)
content = Column(String) content = mapped_column(String)
class Action(Base): class Action(Base):
__tablename__ = "action" __tablename__ = "action"
name = Column(String(100), index=True, nullable=False) name = mapped_column(String(100), index=True, nullable=False)
title = Column(String(200)) title = mapped_column(String(200))
subtitle = Column(String(500)) subtitle = mapped_column(String(500))
outputpoints = Column(String) outputpoints = mapped_column(String)
property = Column(String) property = mapped_column(String)
categoryid = Column(Integer,ForeignKey("category.id")) categoryid = mapped_column(Integer,ForeignKey("category.id"))
nosort = Column(Integer) nosort = mapped_column(Integer)
class Flow(Base): class Flow(Base):
__tablename__ = "flow" __tablename__ = "flow"
flowid = Column(String(100), index=True, nullable=False) flowid = mapped_column(String(100), index=True, nullable=False)
appid = Column(String(100), index=True, nullable=False) appid = mapped_column(String(100), index=True, nullable=False)
eventid = Column(String(100), index=True, nullable=False) eventid = mapped_column(String(100), index=True, nullable=False)
domainurl = Column(String(200)) domainurl = mapped_column(String(200))
name = Column(String(200)) name = mapped_column(String(200))
content = Column(String) content = mapped_column(String)
createuserid = Column(Integer,ForeignKey("user.id")) createuserid = mapped_column(Integer,ForeignKey("user.id"))
updateuserid = Column(Integer,ForeignKey("user.id")) updateuserid = mapped_column(Integer,ForeignKey("user.id"))
createuser = relationship('User',foreign_keys=[createuserid]) createuser = relationship('User',foreign_keys=[createuserid])
updateuser = relationship('User',foreign_keys=[updateuserid]) updateuser = relationship('User',foreign_keys=[updateuserid])
class FlowHistory(Base): class FlowHistory(Base):
__tablename__ = "flowhistory" __tablename__ = "flowhistory"
flowid = Column(String(100), index=True, nullable=False) flowid = mapped_column(String(100), index=True, nullable=False)
appid = Column(String(100), index=True, nullable=False) appid = mapped_column(String(100), index=True, nullable=False)
eventid = Column(String(100), index=True, nullable=False) eventid = mapped_column(String(100), index=True, nullable=False)
domainurl = Column(String(200)) domainurl = mapped_column(String(200))
name = Column(String(200)) name = mapped_column(String(200))
content = Column(String) content = mapped_column(String)
version = Column(Integer) version = mapped_column(Integer)
createuserid = Column(Integer,ForeignKey("user.id")) createuserid = mapped_column(Integer,ForeignKey("user.id"))
updateuserid = Column(Integer,ForeignKey("user.id")) updateuserid = mapped_column(Integer,ForeignKey("user.id"))
createuser = relationship('User',foreign_keys=[createuserid]) createuser = relationship('User',foreign_keys=[createuserid])
updateuser = relationship('User',foreign_keys=[updateuserid]) updateuser = relationship('User',foreign_keys=[updateuserid])
class Tenant(Base): class Tenant(Base):
__tablename__ = "tenant" __tablename__ = "tenant"
tenantid = Column(String(100), index=True, nullable=False) tenantid = mapped_column(String(100), index=True, nullable=False)
name = Column(String(200)) name = mapped_column(String(200))
licence = Column(String(200)) licence = mapped_column(String(200))
startdate = Column(DateTime) startdate = mapped_column(DateTime)
enddate = Column(DateTime) enddate = mapped_column(DateTime)
db = mapped_column(String(200))
class Domain(Base): class Domain(Base):
__tablename__ = "domain" __tablename__ = "domain"
tenantid = Column(String(100), index=True, nullable=False) tenantid = mapped_column(String(100), index=True, nullable=False)
name = Column(String(100), nullable=False) name = mapped_column(String(100), nullable=False)
url = Column(String(200), nullable=False) url = mapped_column(String(200), nullable=False)
kintoneuser = Column(String(100), nullable=False) kintoneuser = mapped_column(String(100), nullable=False)
kintonepwd = Column(String(100), nullable=False) kintonepwd = mapped_column(String(100), nullable=False)
is_active = Column(Boolean, default=True) is_active = mapped_column(Boolean, default=True)
def decrypt_kintonepwd(self): def decrypt_kintonepwd(self):
decrypted_pwd = chacha20Decrypt(self.kintonepwd) decrypted_pwd = chacha20Decrypt(self.kintonepwd)
return decrypted_pwd return decrypted_pwd
createuserid = Column(Integer,ForeignKey("user.id")) createuserid = mapped_column(Integer,ForeignKey("user.id"))
updateuserid = Column(Integer,ForeignKey("user.id")) updateuserid = mapped_column(Integer,ForeignKey("user.id"))
ownerid = Column(Integer,ForeignKey("user.id")) ownerid = mapped_column(Integer,ForeignKey("user.id"))
createuser = relationship('User',foreign_keys=[createuserid]) createuser = relationship('User',foreign_keys=[createuserid])
updateuser = relationship('User',foreign_keys=[updateuserid]) updateuser = relationship('User',foreign_keys=[updateuserid])
owner = relationship('User',foreign_keys=[ownerid]) owner = relationship('User',foreign_keys=[ownerid])
@@ -173,11 +179,23 @@ class Domain(Base):
class UserDomain(Base): class UserDomain(Base):
__tablename__ = "userdomain" __tablename__ = "userdomain"
userid = Column(Integer,ForeignKey("user.id")) userid = mapped_column(Integer,ForeignKey("user.id"))
domainid = Column(Integer,ForeignKey("domain.id")) domainid = mapped_column(Integer,ForeignKey("domain.id"))
is_default = Column(Boolean, default=False) is_default = mapped_column(Boolean, default=False)
createuserid = Column(Integer,ForeignKey("user.id")) createuserid = mapped_column(Integer,ForeignKey("user.id"))
updateuserid = Column(Integer,ForeignKey("user.id")) updateuserid = mapped_column(Integer,ForeignKey("user.id"))
domain = relationship("Domain")
user = relationship("User",foreign_keys=[userid])
createuser = relationship('User',foreign_keys=[createuserid])
updateuser = relationship('User',foreign_keys=[updateuserid])
class ManageDomain(Base):
__tablename__ = "managedomain"
userid = mapped_column(Integer,ForeignKey("user.id"))
domainid = mapped_column(Integer,ForeignKey("domain.id"))
createuserid = mapped_column(Integer,ForeignKey("user.id"))
updateuserid = mapped_column(Integer,ForeignKey("user.id"))
domain = relationship("Domain") domain = relationship("Domain")
user = relationship("User",foreign_keys=[userid]) user = relationship("User",foreign_keys=[userid])
createuser = relationship('User',foreign_keys=[createuserid]) createuser = relationship('User',foreign_keys=[createuserid])
@@ -186,51 +204,53 @@ class UserDomain(Base):
class Event(Base): class Event(Base):
__tablename__ = "event" __tablename__ = "event"
category = Column(String(100), nullable=False) category = mapped_column(String(100), nullable=False)
type = Column(String(100), nullable=False) type = mapped_column(String(100), nullable=False)
eventid= Column(String(100), nullable=False) eventid= mapped_column(String(100), nullable=False)
function = Column(String(500), nullable=False) function = mapped_column(String(500), nullable=False)
mobile = Column(Boolean, default=False) mobile = mapped_column(Boolean, default=False)
eventgroup = Column(Boolean, default=False) eventgroup = mapped_column(Boolean, default=False)
class EventAction(Base): class EventAction(Base):
__tablename__ = "eventaction" __tablename__ = "eventaction"
eventid = Column(String(100),ForeignKey("event.eventid")) eventid = mapped_column(String(100),ForeignKey("event.eventid"))
actionid = Column(Integer,ForeignKey("action.id")) actionid = mapped_column(Integer,ForeignKey("action.id"))
class ErrorLog(Base): class ErrorLog(Base):
__tablename__ = "errorlog" __tablename__ = "errorlog"
title = Column(String(50)) title = mapped_column(String(50))
location = Column(String(500)) location = mapped_column(String(500))
content = Column(String(5000)) content = mapped_column(String(5000))
class OperationLog(Base): class OperationLog(Base):
__tablename__ = "operationlog" __tablename__ = "operationlog"
tenantid = Column(String(100)) tenantid = mapped_column(String(100))
domainurl = Column(String(200)) clientip = mapped_column(String(200))
userid = Column(Integer,ForeignKey("user.id")) useragent = mapped_column(String(200))
operation = Column(String(200)) userid = mapped_column(Integer,ForeignKey("user.id"))
function = Column(String(200)) operation = mapped_column(String(200))
detail = Column(String(200)) function = mapped_column(String(200))
parameters = mapped_column(String(200))
response = mapped_column(String(200))
user = relationship('User') user = relationship('User')
class KintoneFormat(Base): class KintoneFormat(Base):
__tablename__ = "kintoneformat" __tablename__ = "kintoneformat"
name = Column(String(50)) name = mapped_column(String(50))
startrow =Column(Integer) startrow =mapped_column(Integer)
startcolumn =Column(Integer) startcolumn =mapped_column(Integer)
typecolumn =Column(Integer) typecolumn =mapped_column(Integer)
codecolumn =Column(Integer) codecolumn =mapped_column(Integer)
field = Column(String(5000)) field = mapped_column(String(5000))
trueformat = Column(String(10)) trueformat = mapped_column(String(10))
class Category(Base): class Category(Base):
__tablename__ = "category" __tablename__ = "category"
categoryname = Column(String(20)) categoryname = mapped_column(String(20))
nosort = Column(Integer) nosort = mapped_column(Integer)

View File

@@ -13,6 +13,7 @@ class Permission(BaseModel):
id: int id: int
menu:str menu:str
function:str function:str
link:str
privilege:str privilege:str
class RoleBase(BaseModel): class RoleBase(BaseModel):
@@ -24,6 +25,10 @@ class RoleBase(BaseModel):
class RoleWithPermission(RoleBase): class RoleWithPermission(RoleBase):
permissions:t.List[Permission] = [] permissions:t.List[Permission] = []
class AssignUserRoles(BaseModel):
userid:int
roleids:t.List[int]
class UserBase(BaseModel): class UserBase(BaseModel):
email: str email: str
@@ -51,6 +56,7 @@ class UserCreate(UserBase):
last_name: str last_name: str
is_active:bool is_active:bool
is_superuser:bool is_superuser:bool
tenantid:t.Optional[str] = "1"
createuserid:t.Optional[int] = None createuserid:t.Optional[int] = None
updateuserid:t.Optional[int] = None updateuserid:t.Optional[int] = None
@@ -82,16 +88,28 @@ class AppList(Base):
domainurl: str domainurl: str
appname: str appname: str
appid:str appid:str
updateuser: UserOut
version:int version:int
is_saved:bool
versionname: t.Optional[str] = None
updateuser: UserOut
createuser: UserOut
class AppVersion(BaseModel): class AppVersion(Base):
domainurl: str domainurl: str
appname: str appname: str
versionname: str versionname: str
comment:str comment:str
appid:str appid:str
version:t.Optional[int] = None
updateuser: UserOut
createuser: UserOut
class VersionUpdate(BaseModel):
appid:str
versionname: str
comment:str
class TokenData(BaseModel): class TokenData(BaseModel):
id:int = 0 id:int = 0
@@ -186,12 +204,21 @@ class DomainOut(BaseModel):
class ConfigDict: class ConfigDict:
orm_mode = True orm_mode = True
class UserDomainParam(BaseModel):
userid:int
domainid:int
class UserDomain(BaseModel): class UserDomain(BaseModel):
id: int id: int
is_default: bool is_default: bool
domain:DomainOut domain:DomainOut
user:UserOut user:UserOut
class UserDomainIn(BaseModel):
is_default: bool
domainid:int
userid:int
class Domain(Base): class Domain(Base):
id: int id: int
tenantid: str tenantid: str

View File

@@ -1,20 +1,37 @@
from sqlalchemy import create_engine from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker,declarative_base from sqlalchemy.orm import sessionmaker, declarative_base, Session
from app.core import config from app.core import config
engine = create_engine(
config.SQLALCHEMY_DATABASE_URI,
)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base() Base = declarative_base()
# engine = create_engine(
# config.SQLALCHEMY_DATABASE_URI,
# )
# SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
# Dependency class Database:
def get_db(): def __init__(self, database_url: str):
db = SessionLocal() self.database_url = database_url
self.engine = create_engine(self.database_url)
self.SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=self.engine)
self.Base = declarative_base()
def get_db(self):
db =self.SessionLocal()
return db
tenantdb = Database(config.SQLALCHEMY_DATABASE_URI)
def get_tenant_db():
db = tenantdb.get_db()
try: try:
yield db yield db
finally: finally:
db.close() db.close()
def get_user_db(database_url: str):
database = Database(database_url)
db = database.get_db()
return db

View File

@@ -8,7 +8,7 @@ from app.api.api_v1.routers.users import users_router
from app.api.api_v1.routers.auth import auth_router from app.api.api_v1.routers.auth import auth_router
from app.api.api_v1.routers.platform import platform_router from app.api.api_v1.routers.platform import platform_router
from app.core import config from app.core import config
from app.db import Base,engine #from app.db import Base,engine
from app.core.auth import get_current_active_user from app.core.auth import get_current_active_user
from app.core.celery_app import celery_app from app.core.celery_app import celery_app
from app import tasks from app import tasks
@@ -20,9 +20,9 @@ from app.db.crud import create_log
from fastapi.responses import JSONResponse from fastapi.responses import JSONResponse
import asyncio import asyncio
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
from app.core.operation import LoggingMiddleware
#Base.metadata.create_all(bind=engine)
Base.metadata.create_all(bind=engine)
@asynccontextmanager @asynccontextmanager
async def lifespan(app: FastAPI): async def lifespan(app: FastAPI):
@@ -45,6 +45,8 @@ app.add_middleware(
allow_headers=["*"], allow_headers=["*"],
) )
app.add_middleware(LoggingMiddleware)
add_pagination(app) add_pagination(app)
# @app.middleware("http") # @app.middleware("http")

View File

@@ -5,63 +5,170 @@ from fastapi.testclient import TestClient
import typing as t import typing as t
from app.core import config, security from app.core import config, security
from app.db.session import Base, get_db from app.core.dbmanager import get_db
from app.db import models from app.db import models,schemas
from app.main import app from app.main import app
def get_test_db_url() -> str: from app.core import security
return f"{config.SQLALCHEMY_DATABASE_URI}" import jwt
SQLALCHEMY_DATABASE_URI = f"postgresql+psycopg2://kabAdmin:P%40ssw0rd!@kintonetooldb.postgres.database.azure.com/test"
@pytest.fixture engine = create_engine(SQLALCHEMY_DATABASE_URI,echo=True)
test_session_maker = sessionmaker(autocommit=False, autoflush=False, bind=engine)
@pytest.fixture(scope="session")
def test_db(): def test_db():
"""
Modify the db session to automatically roll back after each test.
This is to avoid tests affecting the database state of other tests.
"""
# Connect to the test database
engine = create_engine(
get_test_db_url(),
)
connection = engine.connect() connection = engine.connect()
trans = connection.begin() transaction = connection.begin()
test_session = test_session_maker(bind=connection)
# Run a parent transaction that can roll back all changes
test_session_maker = sessionmaker(
autocommit=False, autoflush=False, bind=engine
)
test_session = test_session_maker()
#test_session.begin_nested()
# @event.listens_for(test_session, "after_transaction_end")
# def restart_savepoint(s, transaction):
# if transaction.nested and not transaction._parent.nested:
# s.expire_all()
# s.begin_nested()
yield test_session yield test_session
# Roll back the parent transaction after the test is complete
test_session.close() test_session.close()
trans.rollback() transaction.rollback()
#transaction.commit()
connection.close() connection.close()
@pytest.fixture(scope="session")
@pytest.fixture(scope="function")
def test_client(test_db): def test_client(test_db):
"""
Get a TestClient instance that reads/write to the test database.
"""
def get_test_db(): def get_test_db():
yield test_db try:
yield test_db
finally:
test_db.close()
app.dependency_overrides[get_db] = get_test_db app.dependency_overrides[get_db] = get_test_db
with TestClient(app) as test_client: with TestClient(app) as test_client:
yield test_client yield test_client
@pytest.fixture(scope="session")
def test_tenant_id():
return "1"
@pytest.fixture(scope="session")
def test_user(test_db,test_tenant_id):
password ="test"
user = models.User(
email = "test@test.com",
first_name = "test",
last_name = "abc",
hashed_password = security.get_password_hash(password),
is_active = True,
is_superuser = False,
tenantid = test_tenant_id
)
test_db.add(user)
test_db.commit()
test_db.refresh(user)
dicUser = user.__dict__
dicUser["password"] = password
return dicUser
@pytest.fixture(scope="session")
def password():
return "password"
@pytest.fixture(scope="session")
def user(test_db,password,test_tenant_id):
user = models.User(
email = "user@test.com",
first_name = "user",
last_name = "abc",
hashed_password = security.get_password_hash(password),
is_active = True,
is_superuser = False,
tenantid = test_tenant_id
)
test_db.add(user)
test_db.commit()
test_db.refresh(user)
return user.__dict__
@pytest.fixture(scope="session")
def admin(test_db,password,test_tenant_id):
user = models.User(
email = "admin@test.com",
first_name = "admin",
last_name = "abc",
hashed_password = security.get_password_hash(password),
is_active = True,
is_superuser = True,
tenantid =test_tenant_id
)
test_db.add(user)
test_db.commit()
test_db.refresh(user)
return user.__dict__
@pytest.fixture(scope="session")
def login_user(test_db,test_client,user,password):
# test_db.add(user)
# test_db.commit()
#test_db.refresh(user)
response = test_client.post("/api/token", data={"username": user["email"], "password":password })
return response.json()["access_token"]
@pytest.fixture(scope="session")
def login_admin(test_db,test_client,admin,password):
# test_db.add(admin)
# test_db.commit()
#test_db.refresh(admin)
response = test_client.post("/api/token", data={"username": admin["email"], "password":password })
return response.json()["access_token"]
@pytest.fixture(scope="session")
def login_user_id(login_user):
payload = jwt.decode(login_user, security.SECRET_KEY, algorithms=[security.ALGORITHM])
id = payload.get("sub")
return id
@pytest.fixture(scope="session")
def login_admin_id(login_admin):
payload = jwt.decode(login_admin, security.SECRET_KEY, algorithms=[security.ALGORITHM])
id = payload.get("sub")
return id
@pytest.fixture(scope="session")
def test_role(test_db):
role = models.Role(
name = "test",
description = "test",
level = 1
)
test_db.add(role)
test_db.commit()
test_db.refresh(role)
return role.__dict__
@pytest.fixture(scope="session")
def test_domain(test_db,login_user_id):
domain = models.Domain(
tenantid = "1",
name = "テスト環境",
url = "https://mfu07rkgnb7c.cybozu.com",
kintoneuser = "MXZ",
kintonepwd = security.chacha20Encrypt("maxz1205"),
is_active = True,
createuserid =login_user_id,
updateuserid =login_user_id,
ownerid = login_user_id
)
test_db.add(domain)
test_db.flush()
user_domain = models.UserDomain(userid = login_user_id, domainid = domain.id ,createuserid = login_user_id,updateuserid = login_user_id)
test_db.add(user_domain)
manage_domain = models.ManageDomain(userid = login_user_id, domainid = domain.id ,createuserid = login_user_id,updateuserid = login_user_id)
test_db.add(manage_domain)
test_db.commit()
test_db.refresh(domain)
return domain
@pytest.fixture(scope="session")
def test_app_id():
return "132"
# @pytest.fixture # @pytest.fixture
# def test_password() -> str: # def test_password() -> str:

View File

@@ -0,0 +1,5 @@
[pytest]
log_cli = 1
log_cli_level = CRITICAL
log_cli_format = %(asctime)s [%(levelname)8s] %(message)s (%(filename)s:%(lineno)s)
log_cli_date_format=%Y-%m-%d %H:%M:%S

View File

@@ -0,0 +1,11 @@
import logging
def test_usr_login(test_client,test_user):
response = test_client.post("/api/token", data={"username": test_user["email"], "password": test_user["password"]})
data = response.json()
logging.error(data)
assert response.status_code == 200
assert "access_token" in response.json()
assert "token_type" in response.json()
assert response.json()["user_name"] == test_user["first_name"]+ " " + test_user["last_name"]

View File

@@ -0,0 +1,175 @@
import logging
def test_get_domains(test_client,test_domain,login_user):
response = test_client.get("/api/domains",headers={"Authorization": "Bearer " + login_user})
data = response.json()
logging.error(data)
assert response.status_code == 200
assert "data" in data
assert data["data"] is not None
assert len(data["data"]) == 1
assert data["data"][0]["name"] == test_domain.name
def test_create_domain(test_client, login_user,login_user_id):
create_domain ={
"id": 0,
"tenantid": "1",
"name": "abc",
"url": "efg",
"kintoneuser": "eee",
"kintonepwd": "fff",
"is_active": True,
}
response = test_client.post("/api/domain", json=create_domain,headers={"Authorization": "Bearer " + login_user})
data = response.json()
logging.error(data)
assert response.status_code == 200
assert "data" in data
assert data["data"] is not None
assert data["data"]["name"] == create_domain["name"]
assert data["data"]["url"] == create_domain["url"]
assert data["data"]["kintoneuser"] == create_domain["kintoneuser"]
assert data["data"]["is_active"] == create_domain["is_active"]
assert data["data"]["owner"]["id"] == login_user_id
def test_get_managedomainuser(test_client,test_domain,login_user,login_user_id):
response = test_client.get("/api/managedomainuser/" + str(test_domain.id),headers={"Authorization": "Bearer " + login_user})
data = response.json()
logging.error(data)
assert response.status_code == 200
assert "data" in data
assert len(data["data"]) == 1
assert data["data"][0]["id"] == login_user_id
def test_add_delete_userdomain(test_client,test_domain,test_user,login_user,login_user_id):
userdomain ={
"userid":test_user["id"],
"domainid":test_domain.id
}
response = test_client.post("/api/userdomain" , json=userdomain,headers={"Authorization": "Bearer " + login_user})
data = response.json()
logging.error(data)
assert response.status_code == 200
assert "data" in data
assert data["data"] is not None
assert data["data"]["name"] == test_domain.name
assert data["data"]["url"] == test_domain.url
response = test_client.delete(f"/api/domain/{test_domain.id}/{test_user["id"]}" , headers={"Authorization": "Bearer " + login_user})
data = response.json()
logging.error(data)
assert "data" in data
assert data["data"] is not None
assert data["data"]["name"] == test_domain.name
assert data["data"]["url"] == test_domain.url
def test_add_delete_managedomain(test_client,test_domain,test_user,login_user,login_user_id):
userdomain ={
"userid":test_user["id"],
"domainid":test_domain.id
}
response = test_client.post("/api/managedomain" , json=userdomain,headers={"Authorization": "Bearer " + login_user})
data = response.json()
logging.error(data)
assert response.status_code == 200
assert "data" in data
assert data["data"] is not None
assert data["data"]["name"] == test_domain.name
assert data["data"]["url"] == test_domain.url
response = test_client.delete(f"/api/managedomain/{test_domain.id}/{test_user["id"]}" , headers={"Authorization": "Bearer " + login_user})
data = response.json()
logging.error(data)
assert "data" in data
assert data["data"] is not None
assert data["data"]["name"] == test_domain.name
assert data["data"]["url"] == test_domain.url
def test_delete_domain(test_client, login_user,login_user_id):
delete_domain ={
"id": 0,
"tenantid": "1",
"name": "delete",
"url": "delete",
"kintoneuser": "delete",
"kintonepwd": "delete",
"is_active": True,
}
response = test_client.post("/api/domain", json=delete_domain,headers={"Authorization": "Bearer " + login_user})
data = response.json()
assert response.status_code == 200
assert "data" in data
assert data["data"] is not None
id = data["data"]["id"]
response = test_client.delete(f"/api/domain/{id}/{login_user_id}",headers={"Authorization": "Bearer " + login_user})
data = response.json()
logging.error(data)
response = test_client.delete(f"/api/domain/{id}",headers={"Authorization": "Bearer " + login_user})
data = response.json()
logging.error(data)
assert response.status_code == 200
assert response.json()["data"]["name"] == delete_domain["name"]
response = test_client.get(f"/api/domain/{id}", headers={"Authorization": "Bearer " + login_user})
assert response.status_code == 200
assert "data" not in response.json()
def test_set_defaultuserdomain(test_client, test_domain,login_user):
response = test_client.put("/api/defaultdomain/"+str(test_domain.id), headers={"Authorization": "Bearer " + login_user})
data = response.json()
logging.error(data)
assert response.status_code == 200
assert "data" in data
assert data["data"] is not None
assert data["data"]["name"] == test_domain.name
assert data["data"]["url"] == test_domain.url
assert data["data"]["kintoneuser"] == test_domain.kintoneuser
assert data["data"]["is_active"] == test_domain.is_active
def test_get_defaultuserdomain(test_client, test_domain,login_user):
response = test_client.get("/api/defaultdomain", headers={"Authorization": "Bearer " + login_user})
assert response.status_code == 200
data = response.json()
assert "data" in data
assert data["data"] is not None
assert data["data"]["name"] == test_domain.name
assert data["data"]["url"] == test_domain.url
assert data["data"]["kintoneuser"] == test_domain.kintoneuser
assert data["data"]["is_active"] == test_domain.is_active
def test_get_domainshareduser(test_client, test_domain,login_user,login_user_id):
response = test_client.get("/api/domainshareduser/"+str(test_domain.id), headers={"Authorization": "Bearer " + login_user})
data = response.json()
logging.error(data)
assert response.status_code == 200
assert "data" in data
assert data["data"] is not None
assert len(data["data"]) == 1
assert data["data"][0]["id"] == login_user_id
def test_edit_domain(test_client, test_domain, login_user):
update_domain ={
"id": test_domain.id,
"tenantid": "1",
"name": "テスト環境abc",
"url": test_domain.url,
"kintoneuser": test_domain.kintoneuser,
"is_active": True
}
response = test_client.put("/api/domain", json=update_domain,headers={"Authorization": "Bearer " + login_user})
data = response.json()
logging.error(data)
assert response.status_code == 200
assert "data" in data
assert data["data"] is not None
assert data["data"]["name"] == update_domain["name"]
assert data["data"]["url"] == update_domain["url"]
assert data["data"]["kintoneuser"] == update_domain["kintoneuser"]
assert data["data"]["is_active"] == update_domain["is_active"]

View File

@@ -0,0 +1,156 @@
import logging
def test_users_list(test_client,login_user):
response = test_client.get("/api/v1/users", headers={"Authorization": "Bearer " + login_user})
assert response.status_code == 200
data = response.json()
assert len(data["data"]) == 2
def test_users_list_for_admin(test_client,login_admin):
response = test_client.get("/api/v1/users", headers={"Authorization": "Bearer " + login_admin})
assert response.status_code == 200
data = response.json()
assert "data" in data
assert len(data["data"]) == 3
def test_user_create(test_client,login_user):
user_data = {
"email": "newuser1@example.com",
"password": "password123",
"first_name": "New",
"last_name": "User",
"is_active": True,
"is_superuser": False
}
response = test_client.post("/api/v1/users", json=user_data, headers={"Authorization": "Bearer " + login_user})
data = response.json()
logging.error(data)
assert response.status_code == 200
assert "data" in data
assert data["data"] is not None
assert data["data"]["id"] > 0
assert data["data"]["email"] == user_data["email"]
assert data["data"]["first_name"] == user_data["first_name"]
assert data["data"]["last_name"] == user_data["last_name"]
assert data["data"]["is_active"] == user_data["is_active"]
assert data["data"]["is_superuser"] == user_data["is_superuser"]
def test_admin_create(test_client,login_user):
user_data = {
"email": "newuser2@example.com",
"password": "password123",
"first_name": "New",
"last_name": "User",
"is_active": True,
"is_superuser": True
}
response = test_client.post("/api/v1/users", json=user_data, headers={"Authorization": "Bearer " + login_user})
assert response.status_code == 200
data = response.json()
assert "data" not in data
def test_admin_create_for_admin(test_client,login_admin):
user_data = {
"email": "admin@example.com",
"password": "password123",
"first_name": "New",
"last_name": "User",
"is_active": True,
"is_superuser": True
}
response = test_client.post("/api/v1/users", json=user_data, headers={"Authorization": "Bearer " + login_admin})
data = response.json()
logging.error(data)
assert response.status_code == 200
assert "data" in data
assert data["data"] is not None
assert data["data"]["id"] > 0
assert data["data"]["email"] == user_data["email"]
assert data["data"]["first_name"] == user_data["first_name"]
assert data["data"]["last_name"] == user_data["last_name"]
assert data["data"]["is_active"] == user_data["is_active"]
assert data["data"]["is_superuser"] == user_data["is_superuser"]
def test_user_details(test_client,login_user_id, login_user,user):
id = login_user_id
response = test_client.get("/api/v1/users/"+ str(id), headers={"Authorization": "Bearer " + login_user})
data = response.json()
logging.error(data)
assert response.status_code == 200
assert data["data"]["email"] == user["email"]
assert data["data"]["first_name"] == user["first_name"]
assert data["data"]["last_name"] == user["last_name"]
assert data["data"]["is_active"] == user["is_active"]
assert data["data"]["is_superuser"] == user["is_superuser"]
assert data["data"]["id"] == id
def test_user_edit(test_client, login_user_id,login_user,user):
id = login_user_id
user_data = {
"email": user["email"],
"first_name": "Updated",
"last_name": "test",
"is_active": True,
"is_superuser": False
}
response = test_client.put("/api/v1/users/" + str(id), json=user_data, headers={"Authorization": "Bearer " + login_user})
assert response.status_code == 200
data = response.json()
assert data["data"]["email"] == user["email"]
assert data["data"]["first_name"] == user_data["first_name"]
assert data["data"]["last_name"] == user_data["last_name"]
assert data["data"]["is_active"] == user["is_active"]
assert data["data"]["id"] == id
def test_user_delete(test_client, login_user):
user_data = {
"email": "delete@example.com",
"password": "password123",
"first_name": "delete",
"last_name": "User",
"is_active": True,
"is_superuser": False
}
response = test_client.post("/api/v1/users", json=user_data, headers={"Authorization": "Bearer " + login_user})
assert response.status_code == 200
data = response.json()
id = data["data"]["id"]
response = test_client.delete("/api/v1/users/"+ str(id),headers={"Authorization": "Bearer " + login_user})
assert response.status_code == 200
assert response.json()["data"]["email"] == "delete@example.com"
response = test_client.get("/api/v1/users/"+ str(id), headers={"Authorization": "Bearer " + login_user})
assert response.status_code == 200
assert "data" not in response.json()
def test_role_assign(test_client, login_user_id,login_user,test_role):
userroles ={
"userid":login_user_id,
"roleids":[test_role["id"]]
}
response = test_client.post("/api/v1/userrole", json=userroles, headers={"Authorization": "Bearer " + login_user})
data = response.json()
logging.error(data)
assert response.status_code == 200
response = test_client.get("/api/v1/users/"+ str(login_user_id), headers={"Authorization": "Bearer " + login_user})
data = response.json()
logging.error(data)
assert response.status_code == 200
assert "data" in data
assert len(data["data"]["roles"]) == 1
def test_roles_get(test_client,login_user):
response = test_client.get("/api/v1/roles", headers={"Authorization": "Bearer " + login_user})
data = response.json()
logging.error(data)
assert response.status_code == 200
assert len(data["data"]) == 0
def test_roles_admin_get(test_client,login_admin):
response = test_client.get("/api/v1/roles", headers={"Authorization": "Bearer " + login_admin})
data = response.json()
logging.error(data)
assert response.status_code == 200
assert len(data["data"]) == 1

View File

@@ -0,0 +1,121 @@
import logging
def test_create_flow(test_client,test_domain,test_app_id,login_user):
test_flow={
"flowid": "73e82bee-76a2-4347-a069-e21bf5e21111",
"appid": test_app_id,
"appname": "test_app",
"eventid": "a",
"name": "保存をクリックしたとき",
"content": ""
}
response = test_client.post("/api/flow", json=test_flow,headers={"Authorization": "Bearer " + login_user})
assert response.status_code == 200
data = response.json()
assert "data" in data
assert data["data"] is not None
assert data["data"]["domainurl"] == test_domain.url
assert data["data"]["flowid"] == test_flow["flowid"]
assert data["data"]["appid"] == test_flow["appid"]
assert data["data"]["eventid"] == test_flow["eventid"]
assert data["data"]["content"] == test_flow["content"]
def test_delete_flow(test_client,test_domain,test_app_id,login_user):
response = test_client.delete("/api/flow/73e82bee-76a2-4347-a069-e21bf5e21111",headers={"Authorization": "Bearer " + login_user})
data = response.json()
logging.error(data)
assert response.status_code == 200
assert "data" in data
assert data["data"] is not None
def test_edit_flow(test_client,test_domain,test_app_id,login_user):
test_flow={
"flowid": "73e82bee-76a2-4347-a069-e21bf5e21111",
"appid": test_app_id,
"appname": "test_app_new",
"eventid": "abc",
"name": "保存をクリックしたとき",
"content": ""
}
response = test_client.put("/api/flow", json=test_flow,headers={"Authorization": "Bearer " + login_user})
data = response.json()
logging.error(data)
assert response.status_code == 200
assert "data" in data
assert data["data"] is not None
assert data["data"]["domainurl"] == test_domain.url
assert data["data"]["flowid"] == test_flow["flowid"]
assert data["data"]["appid"] == test_flow["appid"]
assert data["data"]["eventid"] == test_flow["eventid"]
assert data["data"]["content"] == test_flow["content"]
def test_appversions_update(test_client,test_domain,test_app_id,login_user):
app_version ={
"versionname": "version1",
"comment": "save version1",
"appid": test_app_id
}
response = test_client.post("/api/apps", json=app_version,headers={"Authorization": "Bearer " + login_user})
data = response.json()
logging.error(data)
assert response.status_code == 200
assert "data" in data
assert data["data"] is not None
assert data["data"]["domainurl"] == test_domain.url
assert data["data"]["version"] == 1
assert data["data"]["appid"] == app_version["appid"]
assert data["data"]["versionname"] == app_version["versionname"]
assert data["data"]["is_saved"] == False
def test_apps_list(test_client,login_user):
response = test_client.get("/api/apps", headers={"Authorization": "Bearer " + login_user})
assert response.status_code == 200
data = response.json()
assert "data" in data
assert data["data"] is not None
assert len(data["data"]) == 1
def test_appversions_list(test_client,test_domain,test_app_id,login_user):
response = test_client.get("/api/appversions/" + test_app_id , headers={"Authorization": "Bearer " + login_user})
assert response.status_code == 200
data = response.json()
assert "data" in data
assert data["data"] is not None
assert len(data["data"]) == 1
assert "versionname" in data["data"][0]
def test_appversions_change(test_client,test_domain,test_app_id,login_user):
app_version ={
"versionname": "version2",
"comment": "test",
"appid": test_app_id
}
response = test_client.post("/api/apps", json=app_version,headers={"Authorization": "Bearer " + login_user})
data = response.json()
logging.error(data)
assert response.status_code == 200
assert "data" in data
assert data["data"] is not None
assert data["data"]["version"] == 2
assert data["data"]["versionname"] == app_version["versionname"]
assert data["data"]["is_saved"] == False
response = test_client.put("/api/appversions/" + test_app_id +"/1", headers={"Authorization": "Bearer " + login_user})
data = response.json()
logging.error(data)
assert response.status_code == 200
assert "data" in data
assert data["data"] is not None
assert data["data"]["domainurl"] == test_domain.url
assert data["data"]["version"] == 1
assert data["data"]["appid"] == test_app_id
def test_delete_app(test_client,test_app_id,login_user):
response = test_client.delete("/api/apps/"+ test_app_id, headers={"Authorization": "Bearer " + login_user})
data = response.json()
logging.error(data)
assert response.status_code == 200
assert "data" in data
assert data["data"] is not None

View File

@@ -0,0 +1,9 @@
import logging
def test_get_allapps(test_client,test_domain,login_user):
response = test_client.get("/api/v1/allapps", headers={"Authorization": "Bearer " + login_user})
data = response.json()
logging.error(data)
assert response.status_code == 200
assert "apps" in data
assert data["apps"] is not None
assert len(data["apps"]) > 0

View File

@@ -29,3 +29,11 @@ python -m venv env
```bash ```bash
uvicorn app.main:app --reload uvicorn app.main:app --reload
``` ```
# ZCC対応
1. ENV環境中Pythone現在使用している証明書(cacert.pem)のパス確認
```
python -m certifi
# C:\Projects\AI-IOT\AppBuilderforkintone\backend\env\Scripts\python.exe: No module named certifi
```
2. 上記のコマンドを実行すると、証明書までのパスが出てくるので、どこかにメモしてください。次のコマンドで使います。

Binary file not shown.

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -37,7 +37,8 @@ module.exports = configure(function (/* ctx */) {
// https://v2.quasar.dev/quasar-cli-vite/boot-files // https://v2.quasar.dev/quasar-cli-vite/boot-files
boot: [ boot: [
'axios', 'axios',
'error-handler' 'error-handler',
'permissions'
], ],
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#css // https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#css
@@ -104,7 +105,7 @@ module.exports = configure(function (/* ctx */) {
config: {}, config: {},
// iconSet: 'material-icons', // Quasar icon set // iconSet: 'material-icons', // Quasar icon set
// lang: 'en-US', // Quasar language pack lang: 'ja', // Quasar language pack
// For special cases outside of where the auto-import strategy can have an impact // For special cases outside of where the auto-import strategy can have an impact
// (like functional components as one of the examples), // (like functional components as one of the examples),

View File

@@ -1,6 +1,7 @@
import { boot } from 'quasar/wrappers'; import { boot } from 'quasar/wrappers';
import axios, { AxiosInstance } from 'axios'; import axios, { AxiosInstance, AxiosResponse } from 'axios';
import {router} from 'src/router'; import {router} from 'src/router';
import { IResponse } from 'src/types/BaseTypes';
declare module '@vue/runtime-core' { declare module '@vue/runtime-core' {

View File

@@ -4,14 +4,14 @@ import { Router } from 'vue-router';
import { App } from 'vue'; import { App } from 'vue';
export default boot(({ app, router }: { app: App<Element>; router: Router }) => { export default boot(({ app, router }: { app: App<Element>; router: Router }) => {
document.documentElement.lang="ja-JP"; document.documentElement.lang='ja-JP';
app.config.errorHandler = (err: any, instance: any, info: string) => { app.config.errorHandler = (err: any, instance: any, info: string) => {
if (err.response && err.response.status === 401) { if (err.response && err.response.status === 401) {
// 認証エラーの場合再ログインする // 認証エラーの場合再ログインする
console.error('(; ゚Д゚)/認証エラー(401)', err, info); console.error('(; ゚Д゚)/認証エラー(401)', err, info);
localStorage.removeItem('token'); localStorage.removeItem('token');
router.replace({ router.replace({
path:"/login", path:'/login',
query:{redirect:router.currentRoute.value.fullPath} query:{redirect:router.currentRoute.value.fullPath}
}); });
} else { } else {

View File

@@ -0,0 +1,75 @@
// src/boot/permissions.ts
import { boot } from 'quasar/wrappers';
import { useAuthStore } from 'src/stores/useAuthStore';
import { DirectiveBinding } from 'vue';
export const MenuMapping = {
home: null,
app: null,
version: null,
user: 'user',
role: 'role',
domain: null,
userDomain: null,
};
export const Actions = {
user: {
show: 'user_list',
add: 'user_add',
edit: 'user_edit',
delete: 'user_delete',
},
role: {
show: 'role_list',
},
domain: {
show: 'domain_list',
add: 'domain_add',
edit: 'domain_edit',
delete: 'domain_delete',
grantUse: {
list: 'domain_grant_use_list',
edit: 'domain_grant_use_edit',
},
grantManage: {
list: 'domain_grant_manage_list',
edit: 'domain_grant_manage_edit',
},
},
};
const store = useAuthStore();
export default boot(({ app }) => {
app.directive('permissions', {
mounted(el: HTMLElement, binding: DirectiveBinding) {
if (!hasPermission(binding.value)) {
hideElement(el);
}
},
});
app.config.globalProperties.$hasPermission = hasPermission;
});
function hasPermission(value: any) {
if (!value || store.isSuperAdmin) {
return true;
}
if (typeof value === 'string') {
return store.permissions[value];
} else if (typeof value === 'object') {
return Object.values(value).some((permission: any) => store.permissions[permission]);
} else if (Array.isArray(value)) {
return value.some((permission) => store.permissions[permission]);
}
}
function hideElement(el: HTMLElement) {
if (el.parentNode) {
el.parentNode.removeChild(el);
} else {
el.style.display = 'none';
}
}

View File

@@ -52,7 +52,7 @@ export default {
filter:String filter:String
}, },
emits:[ emits:[
"clearFilter" 'clearFilter'
], ],
setup(props,{emit}) { setup(props,{emit}) {
const isLoaded=ref(false); const isLoaded=ref(false);

View File

@@ -46,9 +46,9 @@ export default defineComponent({
const { app } = toRefs(props); const { app } = toRefs(props);
const authStore = useAuthStore(); const authStore = useAuthStore();
const appinfo = ref<AppInfo>({ const appinfo = ref<AppInfo>({
appId: "", appId: '',
name: "", name: '',
description: "" description: ''
}); });
const link= ref(`${authStore.currentDomain.kintoneUrl}/k/${app.value}`); const link= ref(`${authStore.currentDomain.kintoneUrl}/k/${app.value}`);
const getAppInfo = async (appId:string|undefined) => { const getAppInfo = async (appId:string|undefined) => {
@@ -56,7 +56,7 @@ export default defineComponent({
return; return;
} }
let result : any ={appId:"",name:""}; let result : any ={appId:'',name:''};
let retry =0; let retry =0;
while(retry<=3 && result && result.appId!==appId){ while(retry<=3 && result && result.appId!==appId){
await new Promise(resolve => setTimeout(resolve, 1000)); await new Promise(resolve => setTimeout(resolve, 1000));

View File

@@ -1,24 +1,19 @@
<template> <template>
<div class="q-px-xs"> <detail-field-table
<div v-if="!isLoaded" class="spinner flex flex-center"> detailField="description"
<q-spinner color="primary" size="3em" /> :name="name"
</div> :type="type"
<q-table v-else class="app-table" :selection="type" row-key="id" v-model:selected="selected" flat bordered :filter="filter"
virtual-scroll :columns="columns" :rows="rows" :pagination="pagination" :rows-per-page-options="[0]" :columns="columns"
:filter="filter" style="max-height: 65vh;"> :fetchData="fetchApps"
<template v-slot:body-cell-description="props"> @update:selected="(item) => { selected = item }"
<q-td :props="props"> />
<q-scroll-area class="description-cell">
<div v-html="props.row.description"></div>
</q-scroll-area>
</q-td>
</template>
</q-table>
</div>
</template> </template>
<script lang="ts"> <script lang="ts">
import { ref, onMounted, reactive, watchEffect, PropType } from 'vue' import { ref, PropType } from 'vue';
import { api } from 'boot/axios'; import { api } from 'boot/axios';
import DetailFieldTable from './dialog/DetailFieldTable.vue';
interface IAppDisplay { interface IAppDisplay {
id: string; id: string;
@@ -29,50 +24,35 @@ interface IAppDisplay {
export default { export default {
name: 'AppSelectBox', name: 'AppSelectBox',
components: {
DetailFieldTable
},
props: { props: {
name: String, name: String,
type: String, type: String,
filter: String, filter: String,
filterInitRowsFunc: { filterInitRowsFunc: {
type: Function as PropType<(app: IAppDisplay) => boolean>, type: Function as PropType<(app: IAppDisplay) => boolean>,
},
updateSelectApp: {
type: Function
} }
}, },
setup(props) { setup(props) {
const selected = ref<IAppDisplay[]>([]);
const columns = [ const columns = [
{ name: 'id', required: true, label: 'ID', align: 'left', field: 'id', sortable: true, sort: (a: string, b: string) => parseInt(a, 10) - parseInt(b, 10) }, { name: 'id', required: true, label: 'ID', align: 'left', field: 'id', sortable: true, sort: (a: string, b: string) => parseInt(a, 10) - parseInt(b, 10) },
{ name: 'name', label: 'アプリ名', field: 'name', sortable: true, align: 'left' }, { name: 'name', label: 'アプリ名', field: 'name', sortable: true, align: 'left' },
{ name: 'description', label: '概要', field: 'description', align: 'left', sortable: false }, { name: 'description', label: '概要', field: 'description', align: 'left', sortable: false },
{ name: 'createdate', label: '作成日時', field: 'createdate', align: 'left' } { name: 'createdate', label: '作成日時', field: 'createdate', align: 'left' }
] ];
const isLoaded = ref(false);
const rows = reactive<IAppDisplay[]>([]);
const selected = ref([])
watchEffect(()=>{ const fetchApps = async () => {
if (selected.value && selected.value[0] && props.updateSelectApp) { const res = await api.get('api/v1/allapps');
props.updateSelectApp(selected.value[0]) return res.data.apps.map((item: any) => ({
} id: item.appId,
}); name: item.name,
onMounted(() => { description: item.description,
api.get('api/v1/allapps').then(res => { createdate: dateFormat(item.createdAt)
res.data.apps.forEach((item: any) => { })).filter(app => !props.filterInitRowsFunc || props.filterInitRowsFunc(app));
const row : IAppDisplay = { };
id: item.appId,
name: item.name,
description: item.description,
createdate: dateFormat(item.createdAt)
}
if (props.filterInitRowsFunc && !props.filterInitRowsFunc(row)) {
return;
}
rows.push(row);
});
isLoaded.value = true;
});
});
const dateFormat = (dateStr: string) => { const dateFormat = (dateStr: string) => {
const date = new Date(dateStr); const date = new Date(dateStr);
@@ -84,31 +64,13 @@ export default {
const minutes = pad(date.getMinutes()); const minutes = pad(date.getMinutes());
const seconds = pad(date.getSeconds()); const seconds = pad(date.getSeconds());
return `${year}/${month}/${day} ${hours}:${minutes}:${seconds}`; return `${year}/${month}/${day} ${hours}:${minutes}:${seconds}`;
} };
return { return {
columns, columns,
rows, fetchApps,
selected, selected
isLoaded, };
pagination: ref({ }
rowsPerPage: 10 };
}) </script>
}
},
}
</script>
<style lang="scss">
.description-cell {
height: 60px;
width: 300px;
max-height: 60px;
max-width: 300px;
white-space: break-spaces;
}
.spinner {
min-height: 300px;
min-width: 400px;
}
</style>

View File

@@ -175,7 +175,7 @@ export default defineComponent({
if (flowStore.appInfo?.appId === selected.id) { if (flowStore.appInfo?.appId === selected.id) {
$q.notify({ $q.notify({
type: 'negative', type: 'negative',
caption: "エラー", caption: 'エラー',
message: 'データソースを現在のアプリにすることはできません。' message: 'データソースを現在のアプリにすることはできません。'
}); });
} else if (selected.id !== data.value.sourceApp.id) { } else if (selected.id !== data.value.sourceApp.id) {
@@ -208,7 +208,7 @@ export default defineComponent({
if (isDuplicate) { if (isDuplicate) {
$q.notify({ $q.notify({
type: 'negative', type: 'negative',
caption: "エラー", caption: 'エラー',
message: '重複したフィールドは選択できません' message: '重複したフィールドは選択できません'
}); });
} else { } else {

View File

@@ -42,9 +42,9 @@ import { useQuasar } from 'quasar';
} }
}, },
emits:[ emits:[
"closed", 'closed',
"update:conditionTree", 'update:conditionTree',
"update:show" 'update:show'
], ],
setup(props,context) { setup(props,context) {
const appDg = ref(); const appDg = ref();
@@ -58,11 +58,11 @@ import { useQuasar } from 'quasar';
// message: `条件式を設定してください。` // message: `条件式を設定してください。`
// }); // });
// } // }
context.emit("update:conditionTree",tree.value); context.emit('update:conditionTree',tree.value);
} }
showflg.value=false; showflg.value=false;
context.emit("update:show",false); context.emit('update:show',false);
context.emit("closed",val); context.emit('closed',val);
}; };
const showflg =ref(props.show); const showflg =ref(props.show);
//条件式をコピーする //条件式をコピーする

View File

@@ -217,7 +217,7 @@ export default defineComponent( {
const canMerge =(node:INode)=>{ const canMerge =(node:INode)=>{
const checkedIndexs:number[] = ticked.value; const checkedIndexs:number[] = ticked.value;
const findNode = checkedIndexs.find(index=>node.index===index); const findNode = checkedIndexs.find(index=>node.index===index);
console.log("findNode=>",findNode!==undefined,findNode); console.log('findNode=>',findNode!==undefined,findNode);
return findNode!==undefined; return findNode!==undefined;
} }
//グループ化解散 //グループ化解散

View File

@@ -1,98 +1,105 @@
<template> <template>
<div class="q-pa-md"> <div class="q-pa-md">
<q-uploader <q-uploader
style="max-width: 400px" style="max-width: 400px"
:url="uploadUrl" :url="uploadUrl"
:label="title" :label="title"
:headers="headers" :headers="headers"
accept=".xlsx" accept=".xlsx"
v-on:rejected="onRejected" v-on:rejected="onRejected"
v-on:uploaded="onUploadFinished" v-on:uploaded="onUploadFinished"
v-on:failed="onFailed" v-on:failed="onFailed"
field-name="files" field-name="files"
></q-uploader> ></q-uploader>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { createUploaderComponent, useQuasar } from 'quasar'; import { useQuasar } from 'quasar';
import { useAuthStore } from 'src/stores/useAuthStore'; import { useAuthStore } from 'src/stores/useAuthStore';
import { ref } from 'vue'; import { ref } from 'vue';
const $q=useQuasar();
const authStore = useAuthStore();
const emit =defineEmits(['uploaded']);
/**
* ファイルアップロードを拒否する時の処理
* @param rejectedEntries
*/
function onRejected (rejectedEntries:any) {
// Notify plugin needs to be installed
// https://quasar.dev/quasar-plugins/notify#Installation
$q.notify({
type: 'negative',
message: `Excelファイルを選択してください。`
})
}
/** const $q = useQuasar();
* ファイルアップロード成功時の処理 const authStore = useAuthStore();
*/ const emit = defineEmits(['uploaded']);
function onUploadFinished({xhr}:{xhr:XMLHttpRequest}){
let msg="ファイルのアップロードが完了しました。";
if(xhr && xhr.response){
msg=`${msg} (${xhr.responseText})`;
}
$q.notify({
type: 'positive',
caption:"通知",
message: msg
});
setTimeout(() => {
emit('uploaded',xhr.responseText);
}, 2000);
}
/** interface Props {
* 例外発生時、responseからエラー情報を取得する title?: string;
* @param xhr uploadUrl?: string;
*/ }
function getResponseError(xhr:XMLHttpRequest){
try{
const resp = JSON.parse(xhr.responseText);
return 'detail' in resp ? resp.detail:'';
}catch(err){
return xhr.responseText;
}
}
/** const headers = ref([
* { name: 'Authorization', value: 'Bearer ' + authStore.token },
* @param info ファイルアップロード失敗時の処理 ]);
*/
function onFailed({files,xhr}:{files: readonly any[],xhr:XMLHttpRequest}){
let msg ="ファイルアップロードが失敗しました。";
if(xhr && xhr.status){
const detail = getResponseError(xhr);
msg=`${msg} (${xhr.status }:${detail})`
}
$q.notify({
type:"negative",
message:msg
});
}
interface Props { const props = withDefaults(defineProps<Props>(), {
title: string; title: '設計書から導入する(Excel)',
uploadUrl:string; uploadUrl: `${process.env.KAB_BACKEND_URL}api/v1/createappfromexcel?format=1`,
} });
const headers = ref([{name:"Authorization",value:'Bearer ' + authStore.token}]); /**
const props = withDefaults(defineProps<Props>(), { * ファイルアップロードを拒否する時の処理
title:"設計書から導入する(Excel)", * @param rejectedEntries
uploadUrl: `${process.env.KAB_BACKEND_URL}api/v1/createappfromexcel?format=1` */
function onRejected(rejectedEntries: any) {
// Notify plugin needs to be installed
// https://quasar.dev/quasar-plugins/notify#Installation
$q.notify({
type: 'negative',
message: 'Excelファイルを選択してください。',
});
}
}); /**
* ファイルアップロード成功時の処理
*/
function onUploadFinished({ xhr }: { xhr: XMLHttpRequest }) {
let msg = 'ファイルのアップロードが完了しました。';
if (xhr && xhr.response) {
msg = `${msg} (${xhr.responseText})`;
}
$q.notify({
type: 'positive',
caption: '通知',
message: msg,
});
setTimeout(() => {
emit('uploaded', xhr.responseText);
}, 2000);
}
/**
* 例外発生時、responseからエラー情報を取得する
* @param xhr
*/
function getResponseError(xhr: XMLHttpRequest) {
try {
const resp = JSON.parse(xhr.responseText);
return 'detail' in resp ? resp.detail : '';
} catch (err) {
return xhr.responseText;
}
}
/**
*
* @param info ファイルアップロード失敗時の処理
*/
function onFailed({
files,
xhr,
}: {
files: readonly any[];
xhr: XMLHttpRequest;
}) {
let msg = 'ファイルアップロードが失敗しました。';
if (xhr && xhr.status) {
const detail = getResponseError(xhr);
msg = `${msg} (${xhr.status}:${detail})`;
}
$q.notify({
type: 'negative',
message: msg,
});
}
</script> </script>
<style lang="scss"> <style lang="scss"></style>
</style>

View File

@@ -42,7 +42,7 @@ export default {
onMounted(() => { onMounted(() => {
loading.value = true; loading.value = true;
api.get(`api/domains`).then(res =>{ api.get('api/domains').then(res =>{
res.data.data.forEach((data) => { res.data.data.forEach((data) => {
const item = { const item = {
id: data.id, id: data.id,

View File

@@ -8,65 +8,25 @@
size="md" size="md"
:label="userStore.currentDomain.domainName" :label="userStore.currentDomain.domainName"
:disable-dropdown="true" :disable-dropdown="true"
dropdown-icon='none' dropdown-icon="none"
:disable="true" :disable="true"
> >
<q-list>
<q-item :active="isCurrentDomain(domain)" active-class="active-domain-item" v-for="domain in domains" :key="domain.domainName"
clickable v-close-popup @click="onItemClick(domain)">
<q-item-section side>
<q-icon name="share" size="sm" :color="isCurrentDomain(domain) ? 'orange': ''" text-color="white"></q-icon>
</q-item-section>
<q-item-section>
<q-item-label>{{domain.domainName}}</q-item-label>
<q-item-label caption>{{domain.kintoneUrl}}</q-item-label>
</q-item-section>
</q-item>
</q-list>
</q-btn-dropdown> </q-btn-dropdown>
</template> </template>
<script setup lang="ts" > <script setup lang="ts">
import { IDomainInfo } from 'src/types/DomainTypes';
import { useAuthStore } from 'stores/useAuthStore'; import { useAuthStore } from 'stores/useAuthStore';
import { useDomainStore } from 'src/stores/useDomainStore';
import { computed } from 'vue';
import { useRoute } from 'vue-router';
const userStore = useAuthStore(); const userStore = useAuthStore();
const domainStore = useDomainStore();
const route = useRoute()
const domains = computed(() => domainStore.userDomains);
(async ()=>{
await domainStore.loadUserDomains();
})();
const isUnclickable = computed(()=>{
return route.path.startsWith('/FlowChart/') || domains.value === undefined || domains.value.length === 0;
});
const isCurrentDomain=(domain:IDomainInfo)=>{
return domain.id === userStore.currentDomain.id;
}
const onItemClick=(domain:IDomainInfo)=>{
console.log(domain);
userStore.setCurrentDomain(domain);
}
</script> </script>
<style lang="scss"> <style lang="scss">
.q-btn.disabled.customized-disabled-btn { .q-btn.disabled.customized-disabled-btn {
opacity: 1 !important; opacity: 1 !important;
cursor: default !important; cursor: default !important;
} .q-icon.q-btn-dropdown__arrow {
display: none;
.q-item.active-domain-item { }
color: inherit; * {
background: #eee; cursor: default !important;
} }
.q-btn.disabled.customized-disabled-btn * {
cursor: default !important;
} }
</style> </style>

View File

@@ -1,5 +1,6 @@
<template> <template>
<q-item <q-item
v-permissions="permission"
clickable clickable
tag="a" tag="a"
:target="target?target:'_blank'" :target="target?target:'_blank'"
@@ -35,6 +36,7 @@ export interface EssentialLinkProps {
isSeparator?: boolean; isSeparator?: boolean;
target?:string; target?:string;
disable?:boolean; disable?:boolean;
permission?: string|null;
} }
withDefaults(defineProps<EssentialLinkProps>(), { withDefaults(defineProps<EssentialLinkProps>(), {
caption: '', caption: '',

View File

@@ -73,14 +73,14 @@ export default {
if(!props.blackListLabel.find(blackListItem => blackListItem === fld.label)){ if(!props.blackListLabel.find(blackListItem => blackListItem === fld.label)){
if(props.fieldTypes.length===0 || props.fieldTypes.includes(fld.type)){ if(props.fieldTypes.length===0 || props.fieldTypes.includes(fld.type)){
rows.push({id:index, name: fld.label || fld.code, ...fld }); rows.push({id:index, name: fld.label || fld.code, ...fld });
}else if(props.fieldTypes.includes("lookup") && ("lookup" in fld)){ }else if(props.fieldTypes.includes('lookup') && ('lookup' in fld)){
rows.push({id:index, name: fld.label || fld.code, ...fld }); rows.push({id:index, name: fld.label || fld.code, ...fld });
} }
} }
} else { } else {
if(props.fieldTypes.length===0 || props.fieldTypes.includes(fld.type)){ if(props.fieldTypes.length===0 || props.fieldTypes.includes(fld.type)){
rows.push({id:index, name: fld.label || fld.code, ...fld }); rows.push({id:index, name: fld.label || fld.code, ...fld });
}else if(props.fieldTypes.includes("lookup") && ("lookup" in fld)){ }else if(props.fieldTypes.includes('lookup') && ('lookup' in fld)){
rows.push({id:index, name: fld.label || fld.code, ...fld }); rows.push({id:index, name: fld.label || fld.code, ...fld });
} }
} }

View File

@@ -0,0 +1,24 @@
<template>
<q-badge class="q-mr-xs" v-if="isOwner" color="secondary">所有者</q-badge>
<!-- <q-badge v-else-if="isManager" color="primary">管理者</q-badge> -->
<q-badge v-if="isSelf" color="purple">自分</q-badge>
</template>
<script setup lang="ts">
import { computed } from 'vue';
import { IDomainOwnerDisplay } from '../../types/DomainTypes';
import { useAuthStore } from 'stores/useAuthStore';
const authStore = useAuthStore();
interface Props {
user: { id: number };
domain: IDomainOwnerDisplay;
}
const props = defineProps<Props>();
const isSelf = computed(() => props.user.id === (Number)(authStore.userId));
const isOwner = computed(() => props.user.id === props.domain.owner.id);
const isManager = computed(() => props.user.id === props.domain.owner.id); // TODO
</script>

View File

@@ -2,12 +2,13 @@
<q-dialog :auto-close="false" :model-value="visible" persistent bordered> <q-dialog :auto-close="false" :model-value="visible" persistent bordered>
<q-card class="dialog-content" > <q-card class="dialog-content" >
<q-toolbar class="bg-grey-4"> <q-toolbar class="bg-grey-4">
<q-toolbar-title>{{domain.name}}のドメイン利用権限設定</q-toolbar-title> <q-toolbar-title>{{ dialogTitle }}</q-toolbar-title>
<q-btn flat round dense icon="close" @click="close" /> <q-btn flat round dense icon="close" @click="close" />
</q-toolbar> </q-toolbar>
<q-card-section class="q-mx-md " > <q-card-section class="q-mx-md " >
<q-select <q-select
v-permissions="props.actionPermissions.edit"
class="q-mt-md" class="q-mt-md"
:disable="loading||!domain.domainActive" :disable="loading||!domain.domainActive"
filled filled
@@ -18,11 +19,17 @@
:options="canSharedUserFilteredOptions" :options="canSharedUserFilteredOptions"
clearable clearable
:placeholder="canSharedUserFilter ? '' : domain.domainActive ? '権限を付与するユーザーを選択' : 'ドメインが無効なため、権限を付与できません'" :placeholder="canSharedUserFilter ? '' : domain.domainActive ? '権限を付与するユーザーを選択' : 'ドメインが無効なため、権限を付与できません'"
@filter="filterFn" @filter="filterFn">
:display-value="canSharedUserFilter?`${canSharedUserFilter.fullName} ${canSharedUserFilter.email}`:''">
<template v-slot:selected-item="scope">
<span v-if="canSharedUserFilter">
{{ canSharedUserFilter.fullName }} {{ canSharedUserFilter.email }}
<role-label :domain="domain" :user="scope.opt"></role-label>
</span>
</template>
<template v-slot:after> <template v-slot:after>
<q-btn :disable="!canSharedUserFilter" :loading="addLoading" label="付与" color="primary" @click="shareTo(canSharedUserFilter as IUserDisplay)" /> <q-btn :disable="!canSharedUserFilter" :loading="addLoading" label="付与" color="primary" @click="shareTo(canSharedUserFilter as IUserDisplayWithShareRole)" />
</template> </template>
<template v-slot:option="scope"> <template v-slot:option="scope">
@@ -30,18 +37,31 @@
<q-item-section avatar>{{scope.opt.id}}</q-item-section> <q-item-section avatar>{{scope.opt.id}}</q-item-section>
<q-item-section>{{scope.opt.fullName}}</q-item-section> <q-item-section>{{scope.opt.fullName}}</q-item-section>
<q-item-section>{{scope.opt.email}}</q-item-section> <q-item-section>{{scope.opt.email}}</q-item-section>
<q-item-section side>
<div style="width: 6.5em;">
<role-label :domain="domain" :user="scope.opt"></role-label>
</div>
</q-item-section>
</q-item> </q-item>
</template> </template>
</q-select> </q-select>
<sharing-user-list class="q-mt-md" style="height: 330px" :users="sharedUsers" :loading="loading" title="ドメイン利用権限を持つユーザー">
<sharing-user-list class="q-mt-md" style="height: 330px" :users="sharedUsers" :loading="loading" :title="userListTitle">
<template v-slot:body-cell-role="{ row }">
<q-td auto-width>
<role-label :domain="domain" :user="row"></role-label>
</q-td>
</template>
<template v-slot:actions="{ row }"> <template v-slot:actions="{ row }">
<q-btn title="解除" flat color="primary" padding="xs" size="1em" :loading="row.id == removingUser?.id" icon="person_off" @click="removeShareTo(row)" /> <q-btn v-permissions="props.actionPermissions.edit" round title="解除" flat color="primary" :disable="isActionDisable && isActionDisable(row)" padding="xs" size="1em" :loading="row.isRemoving" icon="person_off" @click="removeShareTo(row)" />
</template> </template>
</sharing-user-list> </sharing-user-list>
</q-card-section> </q-card-section>
<q-card-actions align="right" class="text-primary"> <q-card-actions align="right" class="text-primary">
<q-btn flat label="確定" @click="close" /> <q-btn flat label="確定" @click="checkClose" />
<q-btn flat label="キャンセル" @click="close" /> <q-btn flat label="キャンセル" @click="close" />
</q-card-actions> </q-card-actions>
</q-card> </q-card>
@@ -49,15 +69,32 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, watch, onMounted } from 'vue'; import { ref, watch } from 'vue';
import { IDomainOwnerDisplay } from '../../types/DomainTypes'; import { IDomainOwnerDisplay } from '../../types/DomainTypes';
import { IUser, IUserDisplay } from '../../types/UserTypes'; import { IUser, IUserDisplay } from '../../types/UserTypes';
import { api } from 'boot/axios'; import { api } from 'boot/axios';
import { useAuthStore } from 'stores/useAuthStore';
import SharingUserList from 'components/ShareDomain/SharingUserList.vue'; import SharingUserList from 'components/ShareDomain/SharingUserList.vue';
import RoleLabel from 'components/ShareDomain/RoleLabel.vue';
import { Dialog } from 'quasar'
const authStore = useAuthStore();
interface Props { interface Props {
modelValue: boolean; modelValue: boolean;
domain: IDomainOwnerDisplay; domain: IDomainOwnerDisplay;
dialogTitle: string;
userListTitle: string;
isActionDisable?: (user: IUserDisplay) => boolean;
shareApi: (user: IUserDisplay, domain: IDomainOwnerDisplay) => Promise<any>;
removeSharedApi: (user: IUserDisplay, domain: IDomainOwnerDisplay) => Promise<any>;
getSharedApi: (domain: IDomainOwnerDisplay) => Promise<any>;
actionPermissions: { 'list': string, 'edit': string };
}
interface IUserDisplayWithShareRole extends IUserDisplay {
isRemoving: boolean;
role: number;
} }
const props = defineProps<Props>(); const props = defineProps<Props>();
@@ -68,17 +105,16 @@ const emit = defineEmits<{
}>(); }>();
const addLoading = ref(false); const addLoading = ref(false);
const removingUser = ref<IUserDisplay>();
const loading = ref(true); const loading = ref(true);
const visible = ref(props.modelValue); const visible = ref(props.modelValue);
const allUsers = ref<IUserDisplay[]>([]); const allUsers = ref<IUserDisplayWithShareRole[]>([]);
const sharedUsers = ref<IUserDisplay[]>([]); const sharedUsers = ref<IUserDisplayWithShareRole[]>([]);
const sharedUsersIdSet = new Set<number>(); const sharedUsersIdSet = new Set<number>();
const canSharedUsers = ref<IUserDisplay[]>([]); const canSharedUsers = ref<IUserDisplayWithShareRole[]>([]);
const canSharedUserFilter = ref<IUserDisplay>(); const canSharedUserFilter = ref<IUserDisplayWithShareRole>();
const canSharedUserFilteredOptions = ref<IUserDisplay[]>([]); const canSharedUserFilteredOptions = ref<IUserDisplayWithShareRole[]>([]);
const filterFn = (val:string, update: (cb: () => void) => void) => { const filterFn = (val:string, update: (cb: () => void) => void) => {
update(() => { update(() => {
@@ -98,7 +134,12 @@ watch(
visible.value = newValue; visible.value = newValue;
sharedUsers.value = []; sharedUsers.value = [];
canSharedUserFilter.value = undefined canSharedUserFilter.value = undefined
loading.value = false;
addLoading.value = false;
if (newValue) { if (newValue) {
if (Object.keys(allUsers.value).length == 0) {
await getUsers();
}
await loadShared(); await loadShared();
} }
} }
@@ -111,39 +152,79 @@ watch(
} }
); );
const checkClose = () => {
if (!canSharedUserFilter.value) {
close();
return;
}
Dialog.create({
title: '注意',
message: '選択済だがまだ付与未完了のユーザーがあります。<br>必要な操作を選んでください。',
html: true,
persistent: true,
ok: {
color: 'primary',
label: '付与'
},
cancel: '直接閉じる',
}).onCancel(() => {
close();
}).onOk(() => {
shareTo(canSharedUserFilter.value as IUserDisplayWithShareRole);
});
};
const close = () => { const close = () => {
emit('close'); emit('close');
}; };
const shareTo = async (user: IUserDisplay) => { const shareTo = async (user: IUserDisplayWithShareRole) => {
addLoading.value = true; addLoading.value = true;
loading.value = true; loading.value = true;
await api.post(`api/domain/${user.id}?domainid=${props.domain.id}`) await props.shareApi(user, props.domain);
await loadShared(); await loadShared();
canSharedUserFilter.value = undefined; canSharedUserFilter.value = undefined;
loading.value = false; loading.value = false;
addLoading.value = false; addLoading.value = false;
} }
const removeShareTo = async (user: IUserDisplay) => { const removeShareTo = async (user: IUserDisplayWithShareRole) => {
removingUser.value = user;
loading.value = true; loading.value = true;
await api.delete(`api/domain/${props.domain.id}/${user.id}`) user.isRemoving = true;
await props.removeSharedApi(user, props.domain);
if (isCurrentDomain()) {
await authStore.loadCurrentDomain();
}
await loadShared(); await loadShared();
loading.value = false; loading.value = false;
removingUser.value = undefined;
}; };
const isCurrentDomain = () => {
return props.domain.id === authStore.currentDomain.id;
}
const loadShared = async () => { const loadShared = async () => {
loading.value = true; loading.value = true;
sharedUsersIdSet.clear(); sharedUsersIdSet.clear();
const { data } = await api.get(`/api/domainshareduser/${props.domain.id}`); const { data } = await props.getSharedApi(props.domain);
sharedUsers.value = data.data.map((item: IUser) => {
sharedUsers.value = data.data.reduce((arr: IUserDisplayWithShareRole[], item: IUser) => {
const val = itemToDisplay(item); const val = itemToDisplay(item);
sharedUsersIdSet.add(val.id); if(!sharedUsersIdSet.has(val.id)) {
return val; sharedUsersIdSet.add(val.id);
}); // for sort
if (isOwner(val.id)) {
val.role = 2;
} else if (isManager(val.id)) {
val.role = 1;
} else {
val.role = 0;
}
arr.push(val);
}
return arr;
}, []).sort((a: IUserDisplayWithShareRole, b: IUserDisplayWithShareRole) => b.role - a.role);
canSharedUsers.value = allUsers.value.filter((item) => !sharedUsersIdSet.has(item.id)); canSharedUsers.value = allUsers.value.filter((item) => !sharedUsersIdSet.has(item.id));
canSharedUserFilteredOptions.value = canSharedUsers.value; canSharedUserFilteredOptions.value = canSharedUsers.value;
@@ -151,16 +232,17 @@ const loadShared = async () => {
loading.value = false; loading.value = false;
} }
onMounted(async () => { function isOwner(userId: number) {
await getUsers(); return userId === props.domain?.owner?.id
}) }
function isManager(userId: number) {
return false // TODO
}
const getUsers = async () => { const getUsers = async () => {
if (Object.keys(allUsers.value).length > 0) {
return;
}
loading.value = true; loading.value = true;
const result = await api.get(`api/v1/users`); const result = await api.get('api/v1/users');
allUsers.value = result.data.data.map(itemToDisplay); allUsers.value = result.data.data.map(itemToDisplay);
loading.value = false; loading.value = false;
} }
@@ -175,13 +257,16 @@ const itemToDisplay = (item: IUser) => {
email: item.email, email: item.email,
isSuperuser: item.is_superuser, isSuperuser: item.is_superuser,
isActive: item.is_active, isActive: item.is_active,
} as IUserDisplay role: 0,
} as IUserDisplayWithShareRole
} }
</script> </script>
<style lang="scss"> <style lang="scss">
.dialog-content { .dialog-content {
width: 60vw; width: 700px !important;
max-width: 80vw !important;
max-height: 80vh; max-height: 80vh;
.q-select { .q-select {
min-width: 0 !important; min-width: 0 !important;

View File

@@ -0,0 +1,63 @@
<template>
<share-domain-dialog
:dialogTitle="`「${domain.name}」の接続先管理権限設定`"
userListTitle="接続先管理権限を持つユーザー"
:domain="domain"
:share-api="shareApi"
:remove-shared-api="removeSharedApi"
:get-shared-api="getSharedApi"
:is-action-disable="(row) => row.id === authStore.userId"
:model-value="modelValue"
:action-permissions="Actions.domain.grantManage"
@update:modelValue="updateModelValue"
@close="close"
/>
</template>
<script setup lang="ts">
import { defineProps, defineEmits } from 'vue';
import ShareDomainDialog from 'components/ShareDomain/ShareDomainDialog.vue';
import { IUserDisplay } from '../../types/UserTypes';
import { IDomainOwnerDisplay } from '../../types/DomainTypes';
import { api } from 'boot/axios';
import { Actions } from 'boot/permissions';
import { useAuthStore } from 'src/stores/useAuthStore';
import { IResponse } from 'src/types/BaseTypes';
const authStore = useAuthStore();
interface Props {
modelValue: boolean;
domain: IDomainOwnerDisplay;
}
const props = defineProps<Props>();
async function shareApi(user: IUserDisplay, domain: IDomainOwnerDisplay) {
return api.post('api/managedomain', {
userid: user.id,
domainid: domain.id,
});
}
async function removeSharedApi(user: IUserDisplay, domain: IDomainOwnerDisplay) {
return api.delete<IResponse>(`api/managedomain/${domain.id}/${user.id}`);
}
async function getSharedApi(domain: IDomainOwnerDisplay) {
return api.get<IResponse>(`/api/managedomainuser/${domain.id}`);
}
const emit = defineEmits<{
(e: 'update:modelValue', value: boolean): void;
(e: 'close'): void;
}>();
const updateModelValue = (value: boolean) => {
emit('update:modelValue', value);
};
const close = () => {
emit('close');
};
</script>

View File

@@ -0,0 +1,58 @@
<template>
<share-domain-dialog
:dialogTitle="`「${domain.name}」のドメイン利用権限設定`"
userListTitle="ドメイン利用権限を持つユーザー"
:domain="domain"
:share-api="shareApi"
:remove-shared-api="removeSharedApi"
:get-shared-api="getSharedApi"
:model-value="modelValue"
:action-permissions="Actions.domain.grantUse"
@update:modelValue="updateModelValue"
@close="close"
/>
</template>
<script setup lang="ts">
import { defineProps, defineEmits } from 'vue';
import ShareDomainDialog from 'components/ShareDomain/ShareDomainDialog.vue';
import { IUserDisplay } from '../../types/UserTypes';
import { IDomainOwnerDisplay } from '../../types/DomainTypes';
import { api } from 'boot/axios';
import { Actions } from 'boot/permissions';
interface Props {
modelValue: boolean;
domain: IDomainOwnerDisplay;
}
const props = defineProps<Props>();
async function shareApi(user: IUserDisplay, domain: IDomainOwnerDisplay) {
return api.post('api/userdomain', {
userid: user.id,
domainid: domain.id,
});
}
async function removeSharedApi(user: IUserDisplay, domain: IDomainOwnerDisplay) {
return api.delete(`api/domain/${domain.id}/${user.id}`);
}
async function getSharedApi(domain: IDomainOwnerDisplay) {
return api.get(`/api/domainshareduser/${domain.id}`);
}
const emit = defineEmits<{
(e: 'update:modelValue', value: boolean): void;
(e: 'close'): void;
}>();
const updateModelValue = (value: boolean) => {
emit('update:modelValue', value);
};
const close = () => {
emit('close');
};
</script>

View File

@@ -10,10 +10,17 @@
</q-input> </q-input>
</template> </template>
<template v-slot:body-cell-actions="props"> <template v-for="col in columns" :key="col.name" v-slot:[`body-cell-${col.name}`]="props">
<q-td :props="props"> <slot :name="`body-cell-${col.name}`" :row="props.row" :column="props.col">
<slot name="actions" :row="props.row"></slot> <!-- 默认内容 -->
</q-td> <q-td v-if="col.name !== 'actions'" :props="props" >
<span>{{ props.row[col.name] }}</span>
</q-td>
<!-- actions -->
<q-td v-else auto-width :props="props">
<slot name="actions" :row="props.row"></slot>
</q-td>
</slot>
</template> </template>
</q-table> </q-table>
</template> </template>
@@ -37,9 +44,10 @@ const columns = [
{ name: 'id', label: 'ID', field: 'id', align: 'left', sortable: true }, { name: 'id', label: 'ID', field: 'id', align: 'left', sortable: true },
{ name: 'fullName', label: '名前', field: 'fullName', align: 'left', sortable: true }, { name: 'fullName', label: '名前', field: 'fullName', align: 'left', sortable: true },
{ name: 'email', label: '電子メール', field: 'email', align: 'left', sortable: true }, { name: 'email', label: '電子メール', field: 'email', align: 'left', sortable: true },
{ name: 'role', label: '', field: 'role', align: 'left', sortable: false },
{ name: 'actions', label: '', field: 'actions', sortable: false }, { name: 'actions', label: '', field: 'actions', sortable: false },
]; ];
const filter = ref(''); const filter = ref('');
const pagination = ref({ sortBy: 'id', descending: true, rowsPerPage: 10 }); const pagination = ref({ rowsPerPage: 10 });
</script> </script>

View File

@@ -4,7 +4,7 @@
<q-card class="" style="min-width: 40vw; max-width: 80vw; max-height: 95vh;" :style="cardStyle"> <q-card class="" style="min-width: 40vw; max-width: 80vw; max-height: 95vh;" :style="cardStyle">
<q-toolbar class="bg-grey-4"> <q-toolbar class="bg-grey-4">
<q-toolbar-title>{{ name }}</q-toolbar-title> <q-toolbar-title>{{ name }}</q-toolbar-title>
<q-space></q-space> <q-space v-if="$slots.toolbar"></q-space>
<slot name="toolbar"></slot> <slot name="toolbar"></slot>
<q-btn flat round dense icon="close" @click="CloseDialogue('Cancel')" /> <q-btn flat round dense icon="close" @click="CloseDialogue('Cancel')" />
</q-toolbar> </q-toolbar>
@@ -12,7 +12,7 @@
<slot></slot> <slot></slot>
</q-card-section> </q-card-section>
<q-card-actions v-if="!disableBtn" align="right" class="text-primary"> <q-card-actions v-if="!disableBtn" align="right" class="text-primary">
<q-btn flat label="確定" :loading="okBtnLoading" :v-close-popup="okBtnAutoClose" @click="CloseDialogue('OK')" /> <q-btn flat :label="okBtnLabel || '確定'" :loading="okBtnLoading" :v-close-popup="okBtnAutoClose" @click="CloseDialogue('OK')" />
<q-btn flat label="キャンセル" :disable="okBtnLoading" v-close-popup @click="CloseDialogue('Cancel')" /> <q-btn flat label="キャンセル" :disable="okBtnLoading" v-close-popup @click="CloseDialogue('Cancel')" />
</q-card-actions> </q-card-actions>
</q-card> </q-card>
@@ -30,6 +30,7 @@ export default {
height:String, height:String,
minWidth:String, minWidth:String,
minHeight:String, minHeight:String,
okBtnLabel:String,
okBtnLoading:Boolean, okBtnLoading:Boolean,
okBtnAutoClose:{ okBtnAutoClose:{
type: Boolean, type: Boolean,
@@ -41,7 +42,8 @@ export default {
} }
}, },
emits: [ emits: [
'close' 'close',
'update:visible'
], ],
setup(props, context) { setup(props, context) {
const CloseDialogue = (val) => { const CloseDialogue = (val) => {

View File

@@ -1,13 +1,17 @@
<template> <template>
<q-btn flat padding="xs" round size="1em" icon="more_vert" class="action-menu"> <q-btn v-if="hasPermission()" flat padding="xs" round size="1em" icon="more_vert" class="action-menu">
<q-menu> <q-menu :max-width="maxWidth">
<q-list dense :style="'min-width:' + minWidth "> <q-list dense :style="{ 'min-width': minWidth }">
<template v-for="(item, index) in actions" :key="index" > <template v-for="(item, index) in actions" :key="index" >
<q-item v-if="isAction(item)" :class="item.class" clickable v-close-popup @click="item.action(row)"> <q-item v-if="isAction(item)" v-permissions="item.permission" :disable="isFunction(item.disable) ? item.disable(row) : item.disable"
:class="item.class" clickable v-close-popup @click="item.action(row)">
<q-item-section side style="color: inherit;"> <q-item-section side style="color: inherit;">
<q-icon size="1.2em" :name="item.icon" /> <q-icon size="1.2em" :name="item.icon" />
</q-item-section> </q-item-section>
<q-item-section>{{ item.label }}</q-item-section> <q-item-section>{{ item.label }}</q-item-section>
<q-tooltip v-if="item.tooltip && !isFunction(item.tooltip) || (isFunction(item.tooltip) && item.tooltip(row))" :delay="500" self="center middle">
{{ isFunction(item.tooltip) ? item.tooltip(row) : item.tooltip }}
</q-tooltip>
</q-item> </q-item>
<q-separator v-else /> <q-separator v-else />
</template> </template>
@@ -17,12 +21,15 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { PropType } from 'vue'; import { PropType, getCurrentInstance } from 'vue';
import { IDomainOwnerDisplay } from '../types/DomainTypes'; import { IDomainOwnerDisplay } from '../types/DomainTypes';
interface Action { interface Action {
label: string; label: string;
icon?: string; icon?: string;
tooltip?: string|((row: IDomainOwnerDisplay) => string);
disable?: boolean|((row: IDomainOwnerDisplay) => boolean);
permission?: string|object;
action: (row: any) => void|Promise<void>; action: (row: any) => void|Promise<void>;
class?: string; class?: string;
} }
@@ -38,6 +45,10 @@ export default {
type: Object as PropType<IDomainOwnerDisplay>, type: Object as PropType<IDomainOwnerDisplay>,
required: true required: true
}, },
maxWidth: {
type: String,
default: '150px'
},
minWidth: { minWidth: {
type: String, type: String,
default: '100px' default: '100px'
@@ -50,16 +61,32 @@ export default {
methods: { methods: {
isAction(item: MenuItem): item is Action { isAction(item: MenuItem): item is Action {
return !('separator' in item); return !('separator' in item);
},
isFunction(item: any): item is ((row: IDomainOwnerDisplay) => boolean|string) {
return typeof item === 'function';
},
hasPermission() {
const proxy = getCurrentInstance()?.proxy;
if (!proxy) {
return false;
}
for (const item of this.actions) {
if (this.isAction(item) && proxy.$hasPermission(item.permission)) {
return true;
}
}
} }
} }
}; };
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.q-table tr > td:last-child .action-menu { .q-table tr > td:last-child .action-menu {
opacity: 0.25; opacity: 0.25 !important;
} }
.q-table tr:hover > td:last-child .action-menu { .q-table tr:hover > td:last-child .action-menu:not([disabled]) {
opacity: 1; opacity: 1 !important;
} }
</style> </style>

View File

@@ -6,9 +6,14 @@
<div class="text-h6 ellipsis">{{ item.name }}</div> <div class="text-h6 ellipsis">{{ item.name }}</div>
<div class="text-subtitle2">{{ item.url }}</div> <div class="text-subtitle2">{{ item.url }}</div>
</div> </div>
<div v-if="!isOwnerFunc(item.owner.id)" class="col-auto"> <div class="col-auto">
<!-- <q-badge color="secondary" text-color="white" align="middle" class="q-mb-xs" label="他人の所有" /> --> <!-- <q-badge color="secondary" text-color="white" align="middle" class="q-mb-xs" label="他人の所有" /> -->
<q-chip square color="secondary" text-color="white" icon="people" label="他人の所有" size="sm" /> <q-chip v-if="!isOwnerFunc(item.owner.id)" square color="secondary" text-color="white" icon="people" label="他人の所有" size="sm" />
<q-chip v-else square color="purple" text-color="white" icon="people" label="自分" size="sm" />
<div class="text-right">
<!-- icon="add_moderator" -->
<!-- <q-chip square color="primary" text-color="white" label="管理者" size="sm" /> -->
</div>
</div> </div>
</div> </div>
@@ -25,7 +30,7 @@
<div class="text-grey-7 text-caption text-weight-medium"> <div class="text-grey-7 text-caption text-weight-medium">
所有者 所有者
</div> </div>
<div class="smaller-font-size">{{ item.owner.fullName }}</div> <div class="smaller-font-size">{{ !isOwnerFunc(item.owner.id) ? item.owner.fullName : '自分' }}</div>
</div> </div>
</div> </div>
</q-card-section> </q-card-section>

View File

@@ -0,0 +1,40 @@
<template>
<q-btn flat no-caps dense icon="account_circle" :label="userInfo.fullName">
<q-menu class="bar-user-menu">
<div class="row no-wrap q-px-md q-pt-sm ">
<div class="column items-center justify-center">
<q-icon name="account_circle" color="grey" size="3em" />
</div>
<div class="column q-ml-sm overflow-hidden">
<div class="text-subtitle1 ellipsis full-width">{{ userInfo.fullName }}</div>
<div class="text-grey-7 ellipsis text-caption q-mb-sm full-width">{{ userInfo.email }}</div>
</div>
</div>
<div class="row q-pb-sm q-px-md">
<q-chip v-if="authStore.isSuperAdmin" square color="accent" text-color="white" icon="admin_panel_settings"
label="システム管理者" size="sm" />
<q-chip v-else v-for="(item) in roles" square class="role-label" color="primary" text-color="white" :key="item.id" :label="item.name" size="sm" />
</div>
<div class="row q-pb-sm q-px-md">
<q-btn outline color="negative" icon="logout" label="Logout" @click="authStore.logout()" class="full-width" size="sm" v-close-popup />
</div>
</q-menu>
</q-btn>
</template>
<script setup lang="ts">
import { useAuthStore } from 'stores/useAuthStore';
import { computed } from 'vue';
const authStore = useAuthStore();
const userInfo = computed(() => authStore.userInfo);
const roles = computed(() => authStore.roles);
</script>
<style lang="scss" >
.bar-user-menu {
max-width: 230px !important;
}
.role-label {
margin: 2px;
}
</style>

View File

@@ -23,7 +23,7 @@ defineExpose({
}) })
const getUsers = async (filter = () => true) => { const getUsers = async (filter = () => true) => {
loading.value = true; loading.value = true;
const result = await api.get(`api/v1/users`); const result = await api.get('api/v1/users');
rows.value = result.data.data.map((item) => { rows.value = result.data.data.map((item) => {
return { id: item.id, firstName: item.first_name, lastName: item.last_name, email: item.email, isSuperuser: item.is_superuser, isActive: item.is_active } return { id: item.id, firstName: item.first_name, lastName: item.last_name, email: item.email, isSuperuser: item.is_superuser, isActive: item.is_active }
}).filter(filter); }).filter(filter);

View File

@@ -0,0 +1,105 @@
<template>
<div class="q-px-xs">
<div v-if="!isLoaded" class="spinner flex flex-center">
<q-spinner color="primary" size="3em" />
</div>
<q-table v-else class="app-table" :selection="type" row-key="id" v-model:selected="selected" flat bordered
virtual-scroll :columns="columns" :rows="rows" :pagination="pagination" :rows-per-page-options="[0]"
:filter="filter" style="max-height: 65vh;" @update:selected="emitSelected">
<template v-for="col in columns" :key="col.name" v-slot:[`body-cell-${col.name}`]="props">
<q-td :props="props">
<!-- 使用动态插槽名称 -->
<slot v-if="col.name !== detailField" :name="`body-cell-${col.name}`" :row="props.row" :column="props.col">
<!-- 默认内容 -->
<span>{{ props.row[col.name] }}</span>
</slot>
<q-scroll-area v-else class="description-cell">
<div v-html="props.row[detailField]"></div>
</q-scroll-area>
</q-td>
</template>
</q-table>
</div>
</template>
<script lang="ts">
import { ref, onMounted, reactive, PropType } from 'vue'
interface IRow {
[key: string]: any;
}
export default {
name: 'DetailFieldTable',
props: {
name: String,
type: String,
filter: String,
detailField: {
type: String,
required: true
},
columns: {
type: Array as PropType<any[]>,
required: true
},
fetchData: {
type: Function as PropType<() => Promise<IRow[]>>,
required: true
},
sortBy: {
type: String,
required: false
},
sortDesc: {
type: Boolean,
required: false
}
},
emits: ['update:selected'],
setup(props, { emit }) {
const isLoaded = ref(false);
const rows = reactive<IRow[]>([]);
const selected = ref([]);
onMounted(async () => {
const data = await props.fetchData();
rows.push(...data);
isLoaded.value = true;
});
const emitSelected = (selectedItems: any[]) => {
emit('update:selected', selectedItems);
};
return {
rows,
selected,
isLoaded,
pagination: ref({
sortBy: props.sortBy || undefined,
descending: props.sortDesc || undefined,
rowsPerPage: 10
}),
emitSelected
};
}
};
</script>
<style lang="scss">
.description-cell {
height: 60px;
width: 300px;
max-height: 60px;
max-width: 300px;
white-space: break-spaces;
}
.spinner {
min-height: 300px;
min-width: 400px;
}
</style>

View File

@@ -0,0 +1,71 @@
<template>
<detail-field-table
detailField="description"
:name="name"
:type="type"
:filter="filter"
:columns="columns"
:fetchData="fetchUsers"
@update:selected="(item) => { selected = item }">
<template v-slot:body-cell-status="props">
<div class="row">
<div v-if="props.row.isActive">
<q-chip square color="positive" text-color="white" icon="done" label="使用可能" size="sm" />
</div>
<div v-else>
<q-chip square color="negative" text-color="white" icon="block" label="使用不可" size="sm" />
</div>
<q-chip v-if="props.row.isSuperuser" square color="accent" text-color="white" icon="admin_panel_settings"
label="システム管理者" size="sm" />
</div>
</template>
</detail-field-table>
</template>
<script lang="ts">
import { ref, PropType } from 'vue';
import { IUser } from 'src/types/UserTypes';
import { api } from 'boot/axios';
import DetailFieldTable from './DetailFieldTable.vue';
export default {
name: 'UserSelectBox',
components: {
DetailFieldTable
},
props: {
name: String,
type: String,
filter: String,
filterInitRowsFunc: {
type: Function as PropType<(user: IUser) => boolean>,
}
},
setup(props) {
const selected = ref<IUser[]>([]);
const columns = [
{ name: 'id', label: 'ID', field: 'id', align: 'left', sortable: true },
{ name: 'lastName', label: '氏名', field: 'lastName', align: 'left', sortable: true },
{ name: 'firstName', label: '苗字', field: 'firstName', align: 'left', sortable: true },
{ name: 'email', label: '電子メール', field: 'email', align: 'left', sortable: true },
{ name: 'status', label: '状況', field: 'status', align: 'left' }
];
const fetchUsers = async () => {
const result = await api.get('api/v1/users');
return result.data.data.map((item: any) => {
return { id: item.id, firstName: item.first_name, lastName: item.last_name, email: item.email, isSuperuser: item.is_superuser, isActive: item.is_active, roles: item.roles.map(role => role.id) }
}).filter(user => !props.filterInitRowsFunc || props.filterInitRowsFunc(user));
};
return {
columns,
fetchUsers,
selected
};
}
};
</script>

View File

@@ -0,0 +1,100 @@
<template>
<detail-field-table
detailField="comment"
type="single"
:columns="columns"
sortBy="id"
:sortDesc="true"
:fetchData="fetchVersionHistory"
@update:selected="(item) => { selected = item }"
>
<template v-slot:body-cell-id="p">
<div class="flex justify-between">
<span>{{ p.row.id }}</span>
<q-badge v-if="p.row.isActive" color="primary">現在</q-badge>
</div>
</template>
</detail-field-table>
</template>
<script lang="ts">
import { defineComponent, PropType, ref } from 'vue';
import { IAppDisplay, IAppVersion, IAppVersionDisplay } from 'src/types/AppTypes';
import { date } from 'quasar';
import { api } from 'boot/axios';
import DetailFieldTable from './DetailFieldTable.vue';
import { IUser, IUserDisplay } from 'src/types/UserTypes';
interface IVersionDisplay extends IAppVersionDisplay {
isActive : boolean
}
export default defineComponent({
name: 'VersionHistory',
components: {
DetailFieldTable
},
props: {
app: {
type: Object as PropType<IAppDisplay>,
required: true,
},
},
setup(props, { emit }) {
const selected = ref<IVersionDisplay[]>();
const columns = [
{ name: 'id', label: 'バージョン', field: 'version', align: 'left', sortable: true },
{ name: 'name', label: 'バージョン名', field: 'name', align: 'left', sortable: true },
{ name: 'comment', label: 'コメント', field: 'comment', align: 'left', sortable: true },
// { name: 'creator', label: '作成者', field: (row: IVersionDisplay) => row.creator.fullName, align: 'left', sortable: true },
// { name: 'createTime', label: '作成日時', field: 'createTime', align: 'left', sortable: true },
// { name: 'updater', label: '更新者', field: (row: IVersionDisplay) => row.updater.fullName, align: 'left', sortable: true },
// { name: 'updateTime', label: '更新日時', field: 'updateTime', align: 'left', sortable: true },
];
const formatDate = (dateStr: string) => {
return date.formatDate(dateStr, 'YYYY/MM/DD HH:mm:ss');
};
const toUserDisplay = (user: IUser) => {
return {
id: user.id,
firstName: user.first_name,
lastName: user.last_name,
fullNameSearch: (user.last_name + user.first_name).toLowerCase(),
fullName: user.last_name + ' ' + user.first_name,
email: user.email,
isActive: user.is_active,
isSuperuser: user.is_superuser,
}
}
const fetchVersionHistory = async () => {
const { data } = await api.get(`api/appversions/${props.app.id}`);
return data.data.reduce((arr: IVersionDisplay[], item: any) => {
const val = {
id: item.version,
isActive: item.version === props.app.version,
version: item.version,
appid: item.appid,
name: item.versionname,
comment: item.comment,
// updater: toUserDisplay(item.updateuser),
// updateTime: formatDate(item.updatetime),
// creator: toUserDisplay(item.createuser),
// createTime: formatDate(item.createtime),
} as IVersionDisplay;
arr.push(val);
return arr;
}, []);
}
return {
fetchVersionHistory,
columns,
selected,
};
},
});
</script>

View File

@@ -0,0 +1,61 @@
<template>
<q-input
ref="nameRef"
v-model="versionInfo.name"
filled
autofocus
label="バージョン名"
:rules="[
val => !!val || 'バージョン名を入力してください。',
(val) => !val || val.length <= 80 || '80字以内で入力ください'
]"
/>
<q-input
ref="commentRef"
v-model="versionInfo.comment"
filled
type="textarea"
:rules="[(val) => !val || val.length <= 300 || '300字以内で入力ください']"
label="説明"
/>
</template>
<script setup lang="ts">
import { ref, watch, defineProps, defineEmits } from 'vue';
import { QInput } from 'quasar';
import { IVersionSubmit } from 'src/types/AppTypes';
const nameRef = ref();
const commentRef = ref();
const isValid = () => {
const nameHasError = nameRef.value?.hasError ?? false;
const commentHasError = commentRef.value?.hasError ?? false;
return !nameHasError && !commentHasError;
};
const props = defineProps<{
modelValue: IVersionSubmit;
}>();
const defaultTitle = `${new Date().toLocaleString()}`;
const versionInfo = ref({
...props.modelValue,
name: props.modelValue.name || defaultTitle,
comment: props.modelValue.comment || '',
});
const emit = defineEmits(['update:modelValue']);
defineExpose({
isValid
})
watch(
versionInfo,
() => {
emit('update:modelValue', { ...versionInfo.value });
},
{ immediate: true, deep: true }
);
</script>

View File

@@ -44,7 +44,7 @@ import { useAuthStore } from 'src/stores/useAuthStore';
export default defineComponent({ export default defineComponent({
name: 'AppSelector', name: 'AppSelector',
emits:[ emits:[
"appSelected" 'appSelected'
], ],
components:{ components:{
AppSelectBox, AppSelectBox,
@@ -59,7 +59,7 @@ export default defineComponent({
const closeDg=(val :any)=>{ const closeDg=(val :any)=>{
showSelectApp.value=false; showSelectApp.value=false;
console.log("Dialog closed->",val); console.log('Dialog closed->',val);
if (val == 'OK') { if (val == 'OK') {
const data = appDg.value.selected[0]; const data = appDg.value.selected[0];
console.log(data); console.log(data);

View File

@@ -74,7 +74,7 @@ export default defineComponent({
const selectedEvent = ref<IKintoneEvent | undefined>(store.selectedEvent); const selectedEvent = ref<IKintoneEvent | undefined>(store.selectedEvent);
const selectedChangeEvent = ref<IKintoneEventGroup | undefined>(undefined); const selectedChangeEvent = ref<IKintoneEventGroup | undefined>(undefined);
const isFieldChange = (node: IKintoneEventNode) => { const isFieldChange = (node: IKintoneEventNode) => {
return node.header == 'EVENT' && node.eventId.indexOf(".change.") > -1; return node.header == 'EVENT' && node.eventId.indexOf('.change.') > -1;
} }
const getSelectedClass = (node: IKintoneEventNode) => { const getSelectedClass = (node: IKintoneEventNode) => {
@@ -117,7 +117,7 @@ export default defineComponent({
$q.notify({ $q.notify({
type: 'positive', type: 'positive',
caption: "通知", caption: '通知',
message: `イベント ${node.label} 削除` message: `イベント ${node.label} 削除`
}) })
} }

View File

@@ -92,11 +92,11 @@ export default defineComponent({
}, },
emits: [ emits: [
'addNode', 'addNode',
"nodeSelected", 'nodeSelected',
"nodeEdit", 'nodeEdit',
"deleteNode", 'deleteNode',
"deleteAllNextNodes", 'deleteAllNextNodes',
"copyFlow" 'copyFlow'
], ],
setup(props, context) { setup(props, context) {
const store = useFlowEditorStore(); const store = useFlowEditorStore();
@@ -204,7 +204,7 @@ export default defineComponent({
* 変数名取得 * 変数名取得
*/ */
const varName =(node:IActionNode)=>{ const varName =(node:IActionNode)=>{
const prop = node.actionProps.find((prop) => prop.props.name === "verName"); const prop = node.actionProps.find((prop) => prop.props.name === 'verName');
return prop?.props.modelValue.name; return prop?.props.modelValue.name;
}; };
const copyFlow=()=>{ const copyFlow=()=>{

View File

@@ -31,11 +31,11 @@
import { ref, defineComponent, computed, PropType } from 'vue'; import { ref, defineComponent, computed, PropType } from 'vue';
import { IActionNode, ActionNode, ActionFlow, RootAction } from '../../types/ActionTypes'; import { IActionNode, ActionNode, ActionFlow, RootAction } from '../../types/ActionTypes';
export enum Direction { export enum Direction {
Default = "None", Default = 'None',
Left = "LEFT", Left = 'LEFT',
Right = "RIGHT", Right = 'RIGHT',
LeftNotNext = "LEFTNOTNEXT", LeftNotNext = 'LEFTNOTNEXT',
RightNotNext = "RIGHTNOTNEXT", RightNotNext = 'RIGHTNOTNEXT',
} }
export default defineComponent({ export default defineComponent({
name: 'NodeLine', name: 'NodeLine',

View File

@@ -55,7 +55,7 @@ export default defineComponent({
}); });
const connectProps=(props:IProp)=>{ const connectProps=(props:IProp)=>{
const connProps:any={}; const connProps:any={};
if(props && "connectProps" in props && props.connectProps!=undefined){ if(props && 'connectProps' in props && props.connectProps!=undefined){
for(let connProp of props.connectProps){ for(let connProp of props.connectProps){
let targetProp = componentData.value.find((prop)=>prop.props.name===connProp.propName); let targetProp = componentData.value.find((prop)=>prop.props.name===connProp.propName);
if(targetProp){ if(targetProp){

View File

@@ -72,11 +72,11 @@ export default defineComponent({
} }
}, },
setup(props, { emit }) { setup(props, { emit }) {
const color = ref(props.modelValue??""); const color = ref(props.modelValue??'');
const isSelected = computed(()=>props.modelValue && props.modelValue!==""); const isSelected = computed(()=>props.modelValue && props.modelValue!=='');
const customExp = props.rules === undefined ? [] : eval(props.rules); const customExp = props.rules === undefined ? [] : eval(props.rules);
const errmsg = props.requiredMessage?props.requiredMessage:`${props.displayName}が必須です。`; const errmsg = props.requiredMessage?props.requiredMessage:`${props.displayName}が必須です。`;
const requiredExp = props.required?[((val:any)=>!!val || errmsg ),"anyColor"]:[]; const requiredExp = props.required?[((val:any)=>!!val || errmsg ),'anyColor']:[];
const rulesExp=[...requiredExp,...customExp]; const rulesExp=[...requiredExp,...customExp];
watchEffect(()=>{ watchEffect(()=>{
emit('update:modelValue', color.value); emit('update:modelValue', color.value);

View File

@@ -91,7 +91,7 @@ export default defineComponent({
}, },
setup(props, { emit }) { setup(props, { emit }) {
let source = reactive(props.connectProps["source"]); let source = reactive(props.connectProps['source']);
if(!source){ if(!source){
source = props.context.find(element => element.props.name === 'sources'); source = props.context.find(element => element.props.name === 'sources');
} }

View File

@@ -206,32 +206,32 @@ export default defineComponent({
//集計処理方法 //集計処理方法
const logicalOperators = ref([ const logicalOperators = ref([
{ {
"operator": "", 'operator': '',
"label": "なし" 'label': 'なし'
}, },
{ {
"operator": "SUM", 'operator': 'SUM',
"label": "合計" 'label': '合計'
}, },
{ {
"operator": "AVG", 'operator': 'AVG',
"label": "平均" 'label': '平均'
}, },
{ {
"operator": "MAX", 'operator': 'MAX',
"label": "最大値" 'label': '最大値'
}, },
{ {
"operator": "MIN", 'operator': 'MIN',
"label": "最小値" 'label': '最小値'
}, },
{ {
"operator": "COUNT", 'operator': 'COUNT',
"label": "カウント" 'label': 'カウント'
}, },
{ {
"operator": "FIRST", 'operator': 'FIRST',
"label": "最初の値" 'label': '最初の値'
} }
]); ]);
const checkInput=(val:ValueType)=>{ const checkInput=(val:ValueType)=>{
@@ -239,13 +239,13 @@ export default defineComponent({
return false; return false;
} }
if(!val.name){ if(!val.name){
return "集計結果の変数名を入力してください"; return '集計結果の変数名を入力してください';
} }
if(!val.vars || val.vars.length==0){ if(!val.vars || val.vars.length==0){
return "集計処理を設定してください"; return '集計処理を設定してください';
} }
if(val.vars.some((x)=>!x.vName)){ if(val.vars.some((x)=>!x.vName)){
return "集計結果変数名を入力してください"; return '集計結果変数名を入力してください';
} }
return true; return true;
} }

View File

@@ -70,14 +70,14 @@ export default defineComponent({
const eventId =store.currentFlow?.getRoot()?.name; const eventId =store.currentFlow?.getRoot()?.name;
if(eventId===undefined){return;} if(eventId===undefined){return;}
let displayName = inputValue.value; let displayName = inputValue.value;
if(props.connectProps!==undefined && "displayName" in props.connectProps){ if(props.connectProps!==undefined && 'displayName' in props.connectProps){
displayName =props.connectProps["displayName"].props.modelValue; displayName =props.connectProps['displayName'].props.modelValue;
} }
const customButtonId=`${eventId}.customButtonClick`; const customButtonId=`${eventId}.customButtonClick`;
const findedEvent = store.eventTree.findEventById(customButtonId); const findedEvent = store.eventTree.findEventById(customButtonId);
if(findedEvent && "events" in findedEvent){ if(findedEvent && 'events' in findedEvent){
const customEvents = findedEvent as IKintoneEventGroup; const customEvents = findedEvent as IKintoneEventGroup;
const addEventId = customButtonId+"." + inputValue.value; const addEventId = customButtonId+'.' + inputValue.value;
if(store.eventTree.findEventById(addEventId)){ if(store.eventTree.findEventById(addEventId)){
return; return;
} }

View File

@@ -71,7 +71,7 @@ export default defineComponent({
const rulesExp=[...requiredExp,...customExp]; const rulesExp=[...requiredExp,...customExp];
watchEffect(()=>{ watchEffect(()=>{
emit("update:modelValue",numValue.value); emit('update:modelValue',numValue.value);
}); });
return { return {
numValue, numValue,

View File

@@ -59,7 +59,7 @@ export default defineComponent({
const properties=ref(props.nodeProps); const properties=ref(props.nodeProps);
const connectProps=(props:IProp)=>{ const connectProps=(props:IProp)=>{
const connProps:any={context:properties}; const connProps:any={context:properties};
if(props && "connectProps" in props && props.connectProps!=undefined){ if(props && 'connectProps' in props && props.connectProps!=undefined){
for(let connProp of props.connectProps){ for(let connProp of props.connectProps){
let targetProp = properties.value.find((prop)=>prop.props.name===connProp.propName); let targetProp = properties.value.find((prop)=>prop.props.name===connProp.propName);
if(targetProp){ if(targetProp){

View File

@@ -40,8 +40,7 @@ import { IActionNode, IActionProperty } from 'src/types/ActionTypes';
}, },
props: { props: {
actionNode:{ actionNode:{
type:Object as PropType<IActionNode>, type:Object as PropType<IActionNode>
required:true
}, },
drawerRight:{ drawerRight:{
type:Boolean, type:Boolean,
@@ -55,7 +54,7 @@ import { IActionNode, IActionProperty } from 'src/types/ActionTypes';
setup(props,{emit}) { setup(props,{emit}) {
const showPanel =ref(props.drawerRight); const showPanel =ref(props.drawerRight);
const cloneProps = (actionProps:IActionProperty[]):IActionProperty[]|null=>{ const cloneProps = (actionProps:IActionProperty[]|undefined):IActionProperty[]|null=>{
if(!actionProps){ if(!actionProps){
return null; return null;
} }

View File

@@ -9,7 +9,7 @@ export class Auth
params.append('username', user); params.append('username', user);
params.append('password', pwd); params.append('password', pwd);
try{ try{
const result = await api.post(`api/token`,params); const result = await api.post('api/token',params);
console.info(result); console.info(result);
localStorage.setItem('Token', result.data.access_token); localStorage.setItem('Token', result.data.access_token);
return true; return true;

View File

@@ -32,7 +32,7 @@ export class FlowCtrl {
* @returns * @returns
*/ */
async UpdateFlow(jsonData: any): Promise<boolean> { async UpdateFlow(jsonData: any): Promise<boolean> {
const result = await api.put('api/flow/' + jsonData.flowid, jsonData); const result = await api.put('api/flow', jsonData);
console.info(result.data); console.info(result.data);
return true; return true;
} }

View File

@@ -8,7 +8,7 @@
<q-badge align="top" outline>V{{ version }}</q-badge> <q-badge align="top" outline>V{{ version }}</q-badge>
</q-toolbar-title> </q-toolbar-title>
<domain-selector></domain-selector> <domain-selector></domain-selector>
<q-btn flat round dense icon="logout" @click="authStore.logout()" /> <user-info-button />
</q-toolbar> </q-toolbar>
</q-header> </q-header>
@@ -19,10 +19,6 @@
</q-item-label> </q-item-label>
<EssentialLink v-for="link in essentialLinks" :key="link.title" v-bind="link" /> <EssentialLink v-for="link in essentialLinks" :key="link.title" v-bind="link" />
<div v-if="isAdmin()">
<EssentialLink v-for="link in adminLinks" :key="link.title" v-bind="link" />
</div>
<EssentialLink v-for="link in domainLinks" :key="link.title" v-bind="link" />
</q-list> </q-list>
</q-drawer> </q-drawer>
@@ -34,12 +30,16 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, onMounted, reactive } from 'vue'; import { computed, onMounted, reactive, getCurrentInstance } from 'vue';
import EssentialLink, { EssentialLinkProps } from 'components/EssentialLink.vue'; import EssentialLink, { EssentialLinkProps } from 'components/EssentialLink.vue';
import DomainSelector from 'components/DomainSelector.vue'; import DomainSelector from 'components/DomainSelector.vue';
import UserInfoButton from 'components/UserInfoButton.vue';
import { useAuthStore } from 'stores/useAuthStore'; import { useAuthStore } from 'stores/useAuthStore';
import { useRoute } from 'vue-router';
import { MenuMapping } from 'src/boot/permissions';
const authStore = useAuthStore(); const authStore = useAuthStore();
const route = useRoute()
const noDomain = computed(() => !authStore.hasDomain); const noDomain = computed(() => !authStore.hasDomain);
const essentialLinks: EssentialLinkProps[] = reactive([ const essentialLinks: EssentialLinkProps[] = reactive([
@@ -49,7 +49,8 @@ const essentialLinks: EssentialLinkProps[] = reactive([
icon: 'home', icon: 'home',
link: '/', link: '/',
target: '_self', target: '_self',
disable: noDomain disable: noDomain,
permission: MenuMapping.home
}, },
// { // {
// title: 'フローエディター', // title: 'フローエディター',
@@ -59,12 +60,13 @@ const essentialLinks: EssentialLinkProps[] = reactive([
// target: '_self' // target: '_self'
// }, // },
{ {
title: 'アプリ管理', title: 'アプリ',
caption: 'アプリを管理する', caption: 'アプリのカスタマイズ',
icon: 'widgets', icon: 'widgets',
link: '/#/app', link: '/#/app',
target: '_self', target: '_self',
disable: noDomain disable: noDomain,
permission: MenuMapping.app
}, },
// { // {
// title: '条件エディター', // title: '条件エディター',
@@ -77,6 +79,40 @@ const essentialLinks: EssentialLinkProps[] = reactive([
title: '', title: '',
isSeparator: true isSeparator: true
}, },
// ------------ユーザー-------------
{
title: 'ユーザー管理',
caption: 'ユーザーを管理する',
icon: 'manage_accounts',
link: '/#/user',
target: '_self',
permission: MenuMapping.user
},
{
title: 'ロールの割り当て',
caption: 'ロールを管理する',
icon: 'work',
link: '/#/role',
target: '_self',
permission: MenuMapping.role
},
// ------------ドメイン-------------
{
title: '接続先管理',
caption: 'kintoneの接続先設定',
icon: 'domain',
link: '/#/domain',
target: '_self',
permission: MenuMapping.domain
},
{
title: '接続先の割り当て',
caption: '利用可能な接続先の設定',
icon: 'assignment_ind',
link: '/#/userDomain',
target: '_self',
permission: MenuMapping.userDomain
},
// { // {
// title:'Kintone ポータル', // title:'Kintone ポータル',
// caption:'Kintone', // caption:'Kintone',
@@ -97,45 +133,15 @@ const essentialLinks: EssentialLinkProps[] = reactive([
// }, // },
]); ]);
const domainLinks: EssentialLinkProps[] = reactive([
{
title: 'ドメイン管理',
caption: 'kintoneのドメイン設定',
icon: 'domain',
link: '/#/domain',
target: '_self'
},
{
title: 'ドメイン適用',
caption: 'ユーザー使用可能なドメインの設定',
icon: 'assignment_ind',
link: '/#/userDomain',
target: '_self'
},
]);
const adminLinks: EssentialLinkProps[] = reactive([
{
title: 'ユーザー管理',
caption: 'ユーザーを管理する',
icon: 'manage_accounts',
link: '/#/user',
target: '_self'
},
])
const version = process.env.version; const version = process.env.version;
const productName = process.env.productName; const productName = process.env.productName;
onMounted(() => { onMounted(() => {
authStore.setLeftMenu(true); authStore.setLeftMenu(!route.path.startsWith('/FlowChart/'));
}); });
function toggleLeftDrawer() { function toggleLeftDrawer() {
getCurrentInstance();
authStore.toggleLeftMenu(); authStore.toggleLeftMenu();
} }
function isAdmin(){
const permission = authStore.permissions;
return permission === 'admin'
}
</script> </script>

View File

@@ -2,10 +2,10 @@
<div class="q-pa-md"> <div class="q-pa-md">
<div class="q-gutter-sm row items-start"> <div class="q-gutter-sm row items-start">
<q-breadcrumbs> <q-breadcrumbs>
<q-breadcrumbs-el icon="widgets" label="アプリ管理" /> <q-breadcrumbs-el icon="widgets" label="アプリ" />
</q-breadcrumbs> </q-breadcrumbs>
</div> </div>
<q-table title="Treats" :rows="rows" :columns="columns" row-key="id" :filter="filter" :loading="loading" :pagination="pagination"> <q-table :rows="rows" :columns="columns" row-key="id" :filter="filter" :loading="loading" :pagination="pagination">
<template v-slot:top> <template v-slot:top>
<q-btn color="primary" :disable="loading" label="新規" @click="showAddAppDialog" /> <q-btn color="primary" :disable="loading" label="新規" @click="showAddAppDialog" />
@@ -28,49 +28,67 @@
</a> </a>
</q-td> </q-td>
</template> </template>
<template v-slot:body-cell-actions="p"> <template v-slot:body-cell-version="p">
<q-td :props="p"> <q-td :props="p">
<table-action-menu :row="p.row" :actions="actionList" /> <div class="flex justify-between full-width" >
<span v-if="p.row.version == 0"></span>
<span v-else class="ellipsis" :title="p.row.versionName">{{ p.row.versionName }}</span>
<q-badge v-if="isVersionEditing(p.row)" color="orange-7">変更あり</q-badge>
</div>
</q-td>
</template>
<template v-slot:body-cell-updateUser="p">
<q-td auto-width :props="p">
<q-badge v-if="p.row.updateUser.id == Number(authStore.userId)" color="purple">自分</q-badge>
<span v-else>{{ p.row.updateUser.fullName }}</span>
</q-td>
</template>
<template v-slot:body-cell-actions="p">
<q-td auto-width :props="p">
<table-action-menu :row="p.row" minWidth="180px" max-width="200px" :actions="actionList" />
</q-td> </q-td>
</template> </template>
</q-table> </q-table>
<show-dialog v-model:visible="showSelectApp" name="アプリ選択" @close="closeSelectAppDialog" min-width="50vw" min-height="50vh" :ok-btn-auto-close="false" :ok-btn-loading="isAdding"> <show-dialog v-model:visible="showSelectApp" name="アプリ選択" @close="closeSelectAppDialog" min-width="50vw" min-height="50vh" :ok-btn-auto-close="false" :ok-btn-loading="isAdding">
<template v-slot:toolbar> <template v-slot:toolbar>
<q-input dense debounce="300" v-model="filter" placeholder="検索" clearable> <q-input dense debounce="300" v-model="dgFilter" placeholder="検索" clearable>
<template v-slot:before> <template v-slot:before>
<q-icon name="search" /> <q-icon name="search" />
</template> </template>
</q-input> </q-input>
</template> </template>
<app-select-box ref="appDialog" name="アプリ" type="single" :filter="filter" :filterInitRowsFunc="filterInitRows" /> <app-select-box ref="appDialog" name="アプリ" type="single" :filter="dgFilter" :filterInitRowsFunc="filterInitRows" />
</show-dialog> </show-dialog>
<q-dialog v-model="deleteDialog" persistent>
<q-card>
<q-card-section class="row items-center">
<q-icon name="warning" color="warning" size="2em" />
<span class="q-ml-sm">削除してもよろしいですか</span>
</q-card-section>
<q-card-actions align="right">
<q-btn flat label="Cancel" color="primary" v-close-popup />
<q-btn flat label="OK" color="primary" :loading="deleteUserLoading" @click="deleteApp" />
</q-card-actions>
</q-card>
</q-dialog>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted, watch, reactive } from 'vue'; import { ref, onMounted, computed } from 'vue';
import { useQuasar } from 'quasar' import { useAppStore } from 'stores/useAppStore';
import { api } from 'boot/axios';
import { useAuthStore } from 'stores/useAuthStore'; import { useAuthStore } from 'stores/useAuthStore';
import { useFlowEditorStore } from 'stores/flowEditor'; import { useFlowEditorStore } from 'stores/flowEditor';
import { router } from 'src/router'; import { router } from 'src/router';
import { date } from 'quasar' import { IAppDisplay } from 'src/types/AppTypes';
import { IManagedApp } from 'src/types/AppTypes';
import ShowDialog from 'src/components/ShowDialog.vue'; import ShowDialog from 'src/components/ShowDialog.vue';
import AppSelectBox from 'src/components/AppSelectBox.vue'; import AppSelectBox from 'src/components/AppSelectBox.vue';
import TableActionMenu from 'components/TableActionMenu.vue'; import TableActionMenu from 'components/TableActionMenu.vue';
interface IAppDisplay{ const appStore = useAppStore();
id:string;
sortId: number;
name:string;
url:string;
user:string;
version:string;
updatetime:string;
}
const authStore = useAuthStore(); const authStore = useAuthStore();
const numberStringSorting = (a: string, b: string) => parseInt(a, 10) - parseInt(b, 10); const numberStringSorting = (a: string, b: string) => parseInt(a, 10) - parseInt(b, 10);
@@ -78,65 +96,54 @@ const columns = [
{ name: 'id', label: 'アプリID', field: 'id', align: 'left', sortable: true, sort: numberStringSorting }, { name: 'id', label: 'アプリID', field: 'id', align: 'left', sortable: true, sort: numberStringSorting },
{ name: 'name', label: 'アプリ名', field: 'name', align: 'left', sortable: true }, { name: 'name', label: 'アプリ名', field: 'name', align: 'left', sortable: true },
{ name: 'url', label: 'URL', field: 'url', align: 'left', sortable: true }, { name: 'url', label: 'URL', field: 'url', align: 'left', sortable: true },
{ name: 'user', label: '最後更新者', field: 'user', align: 'left', sortable: true}, { name: 'updateUser', label: '最後更新者', field: '', align: 'left', sortable: true},
{ name: 'updatetime', label: '最後更新日', field: 'updatetime', align: 'left', sortable: true}, { name: 'updateTime', label: '最後更新日', field: 'updateTime', align: 'left', sortable: true},
{ name: 'version', label: 'バージョン', field: 'version', align: 'left', sortable: true, sort: numberStringSorting }, { name: 'version', label: 'バージョン', field: '', align: 'left', sortable: true, style: 'max-width: 200px;',sort: numberStringSorting },
{ name: 'actions', label: '', field: 'actions' } { name: 'actions', label: '', field: 'actions' }
]; ];
const pagination = ref({ sortBy: 'id', descending: true, rowsPerPage: 20 }); const pagination = ref({ sortBy: 'id', descending: true, rowsPerPage: 20 });
const loading = ref(false); const loading = ref(false);
const filter = ref(''); const filter = ref('');
const rows = ref<IAppDisplay[]>([]); const dgFilter = ref('');
const rowIds = new Set<string>(); const rows = computed(() => appStore.apps);
const targetRow = ref<IAppDisplay>();
const $q = useQuasar()
const store = useFlowEditorStore(); const store = useFlowEditorStore();
const appDialog = ref(); const appDialog = ref();
const showSelectApp=ref(false); const showSelectApp=ref(false);
const isAdding = ref(false); const isAdding = ref(false);
const deleteDialog = ref(false);
const deleteUserLoading = ref(false);
const actionList = [ const actionList = [
{ label: '設定', icon: 'account_tree', action: toEditFlowPage }, { label: 'フローの編集', icon: 'account_tree', action: toEditFlowPage },
{ label: '履歴', icon: 'history', action: showHistory }, { label: 'バージョンの管理', icon: 'history', action: toVersionHistoryPage },
{ separator: true }, { separator: true },
{ label: '削除', icon: 'delete_outline', class: 'text-red', action: removeRow }, { label: '削除', icon: 'delete_outline', class: 'text-red', action: removeRow },
]; ];
const getApps = async () => { const getApps = async () => {
loading.value = true; loading.value = true;
rowIds.clear(); await appStore.loadApps();
try { loading.value = false;
const { data } = await api.get('api/apps');
rows.value = data.data.map((item: IManagedApp) => {
rowIds.add(item.appid);
return appToAppDisplay(item)
}).sort((a: IAppDisplay, b: IAppDisplay) => a.sortId - b.sortId); // set default order
} catch (error) {
$q.notify({
icon: 'error',
color: 'negative',
message: 'アプリ一覧の読み込みに失敗しました'
});
} finally {
loading.value = false;
}
} }
function isVersionEditing(app: IAppDisplay) {
return !!app.versionChanged;
};
onMounted(async () => { onMounted(async () => {
await getApps(); await getApps();
}); });
watch(() => authStore.currentDomain.id, async () => {
await getApps();
});
const filterInitRows = (row: {id: string}) => { const filterInitRows = (row: {id: string}) => {
return !rowIds.has(row.id); return !appStore.rowIds.has(row.id);
} }
const showAddAppDialog = () => { const showAddAppDialog = () => {
showSelectApp.value = true; showSelectApp.value = true;
dgFilter.value = ''
} }
const closeSelectAppDialog = async (val: 'OK'|'Cancel') => { const closeSelectAppDialog = async (val: 'OK'|'Cancel') => {
@@ -150,31 +157,34 @@ const closeSelectAppDialog = async (val: 'OK'|'Cancel') => {
} }
function removeRow(app:IAppDisplay) { function removeRow(app:IAppDisplay) {
return targetRow.value = app;
deleteDialog.value = true;
} }
function showHistory(app:IAppDisplay) { const deleteApp = async () => {
return if (targetRow.value?.id) {
} deleteUserLoading.value = true;
await appStore.deleteApp(targetRow.value)
const appToAppDisplay = (app: IManagedApp) => { await getApps();
return { deleteUserLoading.value = false;
id: app.appid, deleteDialog.value = false;
sortId: parseInt(app.appid, 10),
name: app.appname,
url: `${app.domainurl}/k/${app.appid}`,
user: `${app.updateuser.first_name} ${app.updateuser.last_name}` ,
updatetime:date.formatDate(app.update_time, 'YYYY/MM/DD HH:mm'),
version: app.version
} }
} }
function toEditFlowPage(app:IAppDisplay) { async function toVersionHistoryPage(app:IAppDisplay) {
await router.push('/app/version/' + app.id).catch(err => {
console.error(err);
});
}
async function toEditFlowPage(app:IAppDisplay) {
store.setApp({ store.setApp({
appId: app.id, appId: app.id,
name: app.name name: app.name
}); });
store.selectFlow(undefined); store.selectFlow(undefined);
router.push('/FlowChart/' + app.id); await router.push('/FlowChart/' + app.id).catch(err => {
console.error(err);
});
}; };
</script> </script>

View File

@@ -0,0 +1,207 @@
<template>
<div class="q-pa-md">
<div class="q-gutter-sm row items-start">
<q-breadcrumbs>
<q-breadcrumbs-el icon="widgets" label="アプリ" to="/app" />
<q-breadcrumbs-el>
<template v-slot>
<a class="full-width" :href="app?.url" target="_blank" title="Kiontoneへ">
{{ app?.name }}
<q-icon
class="q-ma-xs"
name="open_in_new"
color="grey-9"
/>
</a>
</template>
</q-breadcrumbs-el>
</q-breadcrumbs>
</div>
<q-table :rows="rows" title="バージョン履歴" :columns="columns" :loading="versionLoading" :pagination="pagination" :filter="filter" >
<template v-slot:top-right>
<q-input borderless dense filled clearable debounce="300" v-model="filter" placeholder="検索">
<template v-slot:append>
<q-icon name="search"/>
</template>
</q-input>
</template>
<template v-slot:body-cell-id="p">
<q-td :props="p">
<div class="">
<span>{{ p.row.id }}</span>
<span class="q-ml-md" v-if="p.row.id === app.version">
<q-badge color="primary">適用中</q-badge>
<q-badge class="q-ml-xs" v-if="isVersionEditing()" color="orange-7">変更あり</q-badge>
</span>
</div>
</q-td>
</template>
<template v-slot:body-cell-comment="p">
<q-td :props="p">
<q-scroll-area class="multiline-cell">
<div v-html="p.row['comment']"></div>
</q-scroll-area>
</q-td>
</template>
<template v-slot:body-cell-creator="p">
<q-td auto-width :props="p">
<q-badge v-if="p.row.creator.id == Number(authStore.userId)" color="purple">自分</q-badge>
<span v-else>{{ p.row.creator.fullName }}</span>
</q-td>
</template>
<template v-slot:body-cell-actions="p">
<q-td :props="p">
<table-action-menu :row="p.row" minWidth="140px" :actions="actionList" />
</q-td>
</template>
</q-table>
<q-dialog v-model="confirmDialog" persistent>
<q-card>
<q-card-section class="q-pb-none">
<q-list>
<q-item class="q-px-none">
<q-item-section avatar class="items-center">
<q-icon name="warning" color="warning" size="2em" />
</q-item-section>
<q-item-section>
<div>現在のバージョンは未保存です</div>
<div>プルすると上書されますのでよろしいでしょうか</div>
</q-item-section>
</q-item>
</q-list>
</q-card-section>
<q-card-actions align="right">
<q-btn flat label="キャンセル" color="primary" v-close-popup />
<q-btn flat label="上書きする" color="primary" :loading="deleteUserLoading" @click="doChangeVersion()" />
</q-card-actions>
</q-card>
</q-dialog>
</div>
</template>
<script setup lang="ts">
import { useQuasar } from 'quasar';
import { ref, onMounted } from 'vue';
import { useAppStore } from 'stores/useAppStore';
import { useAuthStore } from 'stores/useAuthStore';
import { router } from 'src/router';
import { useRoute } from 'vue-router';
import { IAppDisplay, IAppVersionDisplay } from 'src/types/AppTypes';
import TableActionMenu from 'components/TableActionMenu.vue';
const authStore = useAuthStore();
const appStore = useAppStore();
const route = useRoute()
const $q = useQuasar();
const app = ref<IAppDisplay>({} as IAppDisplay);
const rows = ref<IAppVersionDisplay[]>([]);
const columns = [
{ name: 'id', label: 'バージョン番号', field: 'version', align: 'left', sortable: true },
{ name: 'name', label: 'バージョン名', field: 'name', align: 'left', sortable: true },
{ name: 'comment', label: 'コメント', field: 'comment', align: 'left', sortable: true },
{ name: 'creator', label: '作成者', field: '', align: 'left', sortable: true },
{ name: 'createTime', label: '作成日時', field: 'createTime', align: 'left', sortable: true },
// { name: 'updater', label: '更新者', field: (row: IVersionDisplay) => row.updater.fullName, align: 'left', sortable: true },
// { name: 'updateTime', label: '更新日時', field: 'updateTime', align: 'left', sortable: true },
{ name: 'actions', label: '', field: 'actions' }
];
const pagination = ref({ sortBy: 'id', descending: true, rowsPerPage: 20 });
const filter = ref('');
const versionLoading = ref(false);
const target = ref<IAppVersionDisplay>();
const confirmDialog = ref(false);
const deleteUserLoading = ref(false);
const actionList = ref([
{ label: '回復する', icon: 'flag', action: changeVersion },
// { label: 'プレビュー', icon: 'visibility', action: toVersionHistoryPage },
// { separator: true },
// { label: '削除', icon: 'delete_outline', class: 'text-red', action: removeRow },
]);
const getApps = async () => {
await appStore.loadApps();
}
const getAppById = () => {
let res = appStore.getAppById(route.params.id as string);
if (res != null) {
app.value = res;
return true;
}
return false;
}
const getVersions = async () => {
versionLoading.value = true;
rows.value = await appStore.getVersionsByAppId(app.value);
versionLoading.value = false;
}
function isVersionEditing() {
return !!app.value.versionChanged;
};
onMounted(async () => {
versionLoading.value = true;
let isSuccess = getAppById();
if (!isSuccess) {
await getApps();
isSuccess = getAppById();
if (!isSuccess) {
$q.notify({
icon: 'error',
color: 'negative',
message: 'バージョン一覧の読み込みに失敗しました'
})
await router.push('/app');
}
}
await getVersions();
});
async function changeVersion(version: IAppVersionDisplay) {
target.value = version;
if (!isVersionEditing()) {
await doChangeVersion(version);
} else {
confirmDialog.value = true;
}
}
async function doChangeVersion(version?: IAppVersionDisplay) {
if (!version) {
version = target.value as IAppVersionDisplay;
}
confirmDialog.value = false;
versionLoading.value = true;
await appStore.changeVersion(app.value, version);
await getApps();
getAppById();
versionLoading.value = false;
}
</script>
<style lang="scss">
.multiline-cell {
height: 45px;
min-width: 300px;
max-height: 45px;
white-space: break-spaces;
.q-scrollarea__content {
display: flex;
align-items: center;
}
}
</style>

View File

@@ -14,6 +14,15 @@
<q-space></q-space> <q-space></q-space>
<q-btn-dropdown color="primary" label="保存" icon="save" :loading="saveLoading" > <q-btn-dropdown color="primary" label="保存" icon="save" :loading="saveLoading" >
<q-list> <q-list>
<q-item clickable v-close-popup @click="onSaveVersion">
<q-item-section avatar >
<q-icon name="bookmark_border"></q-icon>
</q-item-section>
<q-item-section>
<q-item-label>保存して新バージョン</q-item-label>
</q-item-section>
</q-item>
<q-item clickable v-close-popup @click="onSaveFlow"> <q-item clickable v-close-popup @click="onSaveFlow">
<q-item-section avatar > <q-item-section avatar >
<q-icon name="save" color="primary"></q-icon> <q-icon name="save" color="primary"></q-icon>
@@ -42,10 +51,10 @@
@click="drawerLeft=!drawerLeft" class="expand" /> @click="drawerLeft=!drawerLeft" class="expand" />
<q-breadcrumbs v-if="store.appInfo" class="fixed q-pl-md" <q-breadcrumbs v-if="store.appInfo" class="fixed q-pl-md"
:style="{'left': fixedLeftPosition}"> :style="{'left': fixedLeftPosition}">
<q-breadcrumbs-el icon="widgets" label="アプリ管理" to="/app" /> <q-breadcrumbs-el icon="widgets" label="アプリ" to="/app" />
<q-breadcrumbs-el> <q-breadcrumbs-el>
<template v-slot> <template v-slot>
<a class="full-width" :href="!store.appInfo?'':`${authStore.currentDomain.kintoneUrl}/k/${store.appInfo?.appId}`" target="_blank" title="Kiontoneへ"> <a class="full-width" :href="store.appInfo ? `${authStore.currentDomain.kintoneUrl}/k/${store.appInfo?.appId}` : ''" target="_blank" title="Kiontoneへ">
{{ store.appInfo?.name }} {{ store.appInfo?.name }}
<q-icon <q-icon
class="q-ma-xs" class="q-ma-xs"
@@ -75,6 +84,10 @@
</template> </template>
<action-select ref="appDg" name="model" :filter="filter" type="single" @clearFilter="onClearFilter" ></action-select> <action-select ref="appDg" name="model" :filter="filter" type="single" @clearFilter="onClearFilter" ></action-select>
</ShowDialog> </ShowDialog>
<!-- save version dialog -->
<ShowDialog v-model:visible="saveVersionAction" name="保存して新バージョン" @close="closeSaveVersionDg" :ok-btn-auto-close="false" min-width="500px">
<version-input ref="versionInputRef" v-model="versionSubmit" />
</ShowDialog>
<q-inner-loading <q-inner-loading
:showing="initLoading" :showing="initLoading"
color="primary" color="primary"
@@ -86,11 +99,12 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, reactive, computed, onMounted } from 'vue'; import { ref, reactive, computed, onMounted } from 'vue';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import { IActionNode, ActionNode, IActionFlow, ActionFlow, RootAction, IActionProperty } from 'src/types/ActionTypes'; import { IActionNode, ActionNode, IActionFlow, ActionFlow, RootAction, IActionProperty, AppInfo } from 'src/types/ActionTypes';
import { IManagedApp } from 'src/types/AppTypes'; import { IAppDisplay, IManagedApp, IVersionSubmit } from 'src/types/AppTypes';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { useFlowEditorStore } from 'stores/flowEditor'; import { useFlowEditorStore } from 'stores/flowEditor';
import { useAuthStore } from 'stores/useAuthStore'; import { useAuthStore } from 'stores/useAuthStore';
import { useAppStore } from 'stores/useAppStore';
import { api } from 'boot/axios'; import { api } from 'boot/axios';
import NodeItem from 'src/components/main/NodeItem.vue'; import NodeItem from 'src/components/main/NodeItem.vue';
@@ -98,6 +112,7 @@ import ShowDialog from 'components/ShowDialog.vue';
import ActionSelect from 'components/ActionSelect.vue'; import ActionSelect from 'components/ActionSelect.vue';
import PropertyPanel from 'components/right/PropertyPanel.vue'; import PropertyPanel from 'components/right/PropertyPanel.vue';
import EventTree from 'components/left/EventTree.vue'; import EventTree from 'components/left/EventTree.vue';
import VersionInput from 'components/dialog/VersionInput.vue';
import { FlowCtrl } from '../control/flowctrl'; import { FlowCtrl } from '../control/flowctrl';
import { useQuasar } from 'quasar'; import { useQuasar } from 'quasar';
@@ -105,21 +120,25 @@ const deployLoading = ref(false);
const saveLoading = ref(false); const saveLoading = ref(false);
const initLoading = ref(true); const initLoading = ref(true);
const drawerLeft = ref(false); const drawerLeft = ref(false);
const versionSubmit = ref<IVersionSubmit>({} as IVersionSubmit);
const $q = useQuasar(); const $q = useQuasar();
const store = useFlowEditorStore(); const store = useFlowEditorStore();
const authStore = useAuthStore(); const authStore = useAuthStore();
const appStore = useAppStore();
const route = useRoute() const route = useRoute()
const appDg = ref(); const appDg = ref();
const prevNodeIfo = ref({ const prevNodeIfo = ref({
prevNode: {} as IActionNode, prevNode: {} as IActionNode,
inputPoint: "" inputPoint: ''
}); });
// const refFlow = ref<ActionFlow|null>(null); // const refFlow = ref<ActionFlow|null>(null);
const showAddAction = ref(false); const showAddAction = ref(false);
const saveVersionAction = ref(false);
const versionInputRef = ref();
const drawerRight = ref(false); const drawerRight = ref(false);
const filter=ref(""); const filter=ref('');
const model = ref(""); const model = ref('');
const rootNode = computed(()=>{ const rootNode = computed(()=>{
return store.currentFlow?.getRoot(); return store.currentFlow?.getRoot();
@@ -129,11 +148,11 @@ const minPanelWidth=computed(()=>{
if(store.currentFlow && root){ if(store.currentFlow && root){
return store.currentFlow?.getColumns(root) * 300 + 'px'; return store.currentFlow?.getColumns(root) * 300 + 'px';
}else{ }else{
return "300px"; return '300px';
} }
}); });
const fixedLeftPosition = computed(()=>{ const fixedLeftPosition = computed(()=>{
return drawerLeft.value?"300px":"0px"; return drawerLeft.value?'300px':'0px';
}); });
const addNode = (node: IActionNode, inputPoint: string) => { const addNode = (node: IActionNode, inputPoint: string) => {
@@ -176,12 +195,12 @@ const onDeleteAllNextNodes = (node: IActionNode) => {
store.currentFlow?.removeAllNext(node.id); store.currentFlow?.removeAllNext(node.id);
} }
const closeDg = (val: any) => { const closeDg = (val: any) => {
console.log("Dialog closed->", val); console.log('Dialog closed->', val);
if (val == 'OK') { if (val == 'OK' && appDg?.value?.selected?.length > 0) {
const data = appDg.value.selected[0]; const data = appDg.value.selected[0];
const actionProps = JSON.parse(data.property); const actionProps = JSON.parse(data.property);
const outputPoint = JSON.parse(data.outputPoints); const outputPoint = JSON.parse(data.outputPoints);
const action = new ActionNode(data.name, data.desc, "", outputPoint, actionProps); const action = new ActionNode(data.name, data.desc, '', outputPoint, actionProps);
store.currentFlow?.addNode(action, prevNodeIfo.value.prevNode, prevNodeIfo.value.inputPoint); store.currentFlow?.addNode(action, prevNodeIfo.value.prevNode, prevNodeIfo.value.inputPoint);
} }
} }
@@ -208,8 +227,8 @@ const onDeploy = async () => {
if (store.appInfo === undefined || store.flows?.length === 0) { if (store.appInfo === undefined || store.flows?.length === 0) {
$q.notify({ $q.notify({
type: 'negative', type: 'negative',
caption: "エラー", caption: 'エラー',
message: `設定されたフローがありません。` message: '設定されたフローがありません。'
}); });
return; return;
} }
@@ -219,16 +238,16 @@ const onDeploy = async () => {
deployLoading.value = false; deployLoading.value = false;
$q.notify({ $q.notify({
type: 'positive', type: 'positive',
caption: "通知", caption: '通知',
message: `デプロイを成功しました。` message: 'デプロイを成功しました。'
}); });
} catch (error) { } catch (error) {
console.error(error); console.error(error);
deployLoading.value = false; deployLoading.value = false;
$q.notify({ $q.notify({
type: 'negative', type: 'negative',
caption: "エラー", caption: 'エラー',
message: `デプロイが失敗しました。` message: 'デプロイが失敗しました。'
}) })
} }
return; return;
@@ -239,19 +258,40 @@ const onSaveActionProps=(props:IActionProperty[])=>{
store.activeNode.actionProps=props; store.activeNode.actionProps=props;
$q.notify({ $q.notify({
type: 'positive', type: 'positive',
caption: "通知", caption: '通知',
message: `${store.activeNode?.subTitle}の属性を設定しました。(保存はされていません)` message: `${store.activeNode?.subTitle}の属性を設定しました。(保存はされていません)`
}); });
} }
}; };
const onSaveVersion = async () => {
if (!store.appInfo) return;
versionSubmit.value = { appId: store.appInfo.appId }
saveVersionAction.value = true;
}
const closeSaveVersionDg = async (val: 'OK'|'CANCEL') => {
saveVersionAction.value = true;
if (val == 'OK') {
if (versionInputRef?.value?.isValid()) {
saveVersionAction.value = false;
await onSaveAllFlow();
await appStore.createVersion(versionSubmit.value);
} else {
saveVersionAction.value = true;
}
} else {
saveVersionAction.value = false;
}
}
const onSaveFlow = async () => { const onSaveFlow = async () => {
const targetFlow = store.selectedFlow; const targetFlow = store.selectedFlow;
if (targetFlow === undefined) { if (targetFlow === undefined) {
$q.notify({ $q.notify({
type: 'negative', type: 'negative',
caption: 'エラー', caption: 'エラー',
message: `選択中のフローがありません。` message: '選択中のフローがありません。'
}); });
return; return;
} }
@@ -261,7 +301,7 @@ const onSaveFlow = async () => {
saveLoading.value = false; saveLoading.value = false;
$q.notify({ $q.notify({
type: 'positive', type: 'positive',
caption: "通知", caption: '通知',
message: `${targetFlow.getRoot()?.subTitle}のフロー設定を保存しました。` message: `${targetFlow.getRoot()?.subTitle}のフロー設定を保存しました。`
}); });
} catch (error) { } catch (error) {
@@ -269,7 +309,7 @@ const onSaveFlow = async () => {
saveLoading.value = false; saveLoading.value = false;
$q.notify({ $q.notify({
type: 'negative', type: 'negative',
caption: "エラー", caption: 'エラー',
message: `${targetFlow.getRoot()?.subTitle}のフローの設定の保存が失敗しました。` message: `${targetFlow.getRoot()?.subTitle}のフローの設定の保存が失敗しました。`
}) })
} }
@@ -284,7 +324,7 @@ const onSaveAllFlow= async ()=>{
$q.notify({ $q.notify({
type: 'negative', type: 'negative',
caption: 'エラー', caption: 'エラー',
message: `設定されたフローがありません。` message: '設定されたフローがありません。'
}); });
return; return;
} }
@@ -298,8 +338,8 @@ const onSaveAllFlow= async ()=>{
} }
$q.notify({ $q.notify({
type: 'positive', type: 'positive',
caption: "通知", caption: '通知',
message: `すべてのフロー設定を保存しました。` message: 'すべてのフロー設定を保存しました。'
}); });
saveLoading.value = false; saveLoading.value = false;
}catch (error) { }catch (error) {
@@ -307,8 +347,8 @@ const onSaveAllFlow= async ()=>{
saveLoading.value = false; saveLoading.value = false;
$q.notify({ $q.notify({
type: 'negative', type: 'negative',
caption: "エラー", caption: 'エラー',
message: `フローの設定の保存が失敗しました。` message: 'フローの設定の保存が失敗しました。'
}); });
} }
} }
@@ -316,11 +356,9 @@ const onSaveAllFlow= async ()=>{
const fetchData = async () => { const fetchData = async () => {
initLoading.value = true; initLoading.value = true;
if (store.appInfo === undefined && route?.params?.id !== undefined) { if (store.appInfo === undefined && route?.params?.id !== undefined) {
const { appid, appname } = await fetchAppById(route.params.id as string); // only for page refreshed
store.setApp({ const app = await fetchAppById(route.params.id as string);
appId: appid, store.setApp(app);
name: appname
});
}; };
await store.loadFlow(); await store.loadFlow();
initLoading.value = false initLoading.value = false
@@ -328,31 +366,46 @@ const fetchData = async () => {
} }
const fetchAppById = async(id: string) => { const fetchAppById = async(id: string) => {
try { let result = await api.get('api/apps');
const result = await api.get('api/apps'); const app = result.data?.data?.find((item: IManagedApp) => item.appid === id ) as IManagedApp;
return result.data.find((item: IManagedApp) => item.appid === id ) as IManagedApp; if (app) {
} catch (e) { return convertManagedAppToAppInfo(app);
console.error(e);
const result = await api.get(`api/v1/app?app=${id}`);
const data = result?.data;
if (data?.message) {
$q.notify({
type: 'negative',
caption: "エラー",
message: data.message
});
}
return { appid: data.appId, appname: data.name };
} }
result = await api.get(`api/v1/app?app=${id}`);
const kApp = result?.data as IAppDisplay | KErrorMsg;
if (isErrorMsg(kApp)) {
$q.notify({
type: 'negative',
caption: 'エラー',
message: kApp.message,
});
}
return kApp;
} }
type KErrorMsg = {
message: string;
}
const isErrorMsg = (e: IAppDisplay | KErrorMsg): e is KErrorMsg => {
return 'message' in e;
};
const convertManagedAppToAppInfo = (app: IManagedApp): AppInfo => {
return {
appId: app.appid,
name: app.appname
}
};
const onClearFilter=()=>{ const onClearFilter=()=>{
filter.value=''; filter.value='';
} }
onMounted(() => { onMounted(async () => {
authStore.setLeftMenu(false); authStore.setLeftMenu(false);
fetchData(); await fetchData();
}); });
</script> </script>

View File

@@ -27,23 +27,23 @@ import ActionSelect from 'components/ActionSelect.vue';
import PropertyPanel from 'components/right/PropertyPanel.vue'; import PropertyPanel from 'components/right/PropertyPanel.vue';
const rootNode:RootAction =new RootAction("app.record.create.submit","レコード追加画面","保存するとき"); const rootNode:RootAction =new RootAction('app.record.create.submit','レコード追加画面','保存するとき');
const actionFlow: ActionFlow = new ActionFlow(rootNode); const actionFlow: ActionFlow = new ActionFlow(rootNode);
const saibanProps:IActionProperty[]=[{ const saibanProps:IActionProperty[]=[{
component:"InputText", component:'InputText',
props:{ props:{
displayName:"フォーマット", displayName:'フォーマット',
modelValue:"", modelValue:'',
name:"format", name:'format',
placeholder:"フォーマットを入力してください", placeholder:'フォーマットを入力してください',
} }
},{ },{
component:"FieldInput", component:'FieldInput',
props:{ props:{
displayName:"採番項目", displayName:'採番項目',
modelValue:"", modelValue:'',
name:"field", name:'field',
placeholder:"採番項目を選択してください", placeholder:'採番項目を選択してください',
} }
}]; }];
@@ -91,7 +91,7 @@ const onDeleteAllNextNodes=(node:IActionNode)=>{
refFlow.value.removeAllNext(node.id); refFlow.value.removeAllNext(node.id);
} }
const closeDg=(val :any)=>{ const closeDg=(val :any)=>{
console.log("Dialog closed->",val); console.log('Dialog closed->',val);
} }
</script> </script>

View File

@@ -1,12 +1,11 @@
<template> <template>
<q-page> <q-page>
<div class="q-pa-md"> <div class="q-pa-md">
<div class="q-gutter-sm row items-start"> <div class="q-gutter-sm row items-start">
<q-breadcrumbs> <q-breadcrumbs>
<q-breadcrumbs-el icon="home" label="ホーム" /> <q-breadcrumbs-el icon="home" label="ホーム" />
</q-breadcrumbs> </q-breadcrumbs>
</div> </div>
<div class="q-gutter-sm row items-start"> <div class="q-gutter-sm row items-start">
<doc-uploader @uploaded="onAppUploaded"></doc-uploader> <doc-uploader @uploaded="onAppUploaded"></doc-uploader>
</div> </div>
@@ -16,28 +15,26 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import {ref} from 'vue' import { ref } from 'vue';
import DocUploader from 'components/DocUpload.vue'; import DocUploader from 'components/DocUpload.vue';
import AppInfo from 'components/AppInfo.vue'; import AppInfo from 'components/AppInfo.vue';
import { AppSeed } from 'src/components/models'; import { AppSeed } from 'src/components/models';
interface AppInfo { interface AppInfo {
app:string, app: string;
revision:string revision: string;
} }
const appseed = withDefaults( defineProps<AppSeed>(),{ const appseed = withDefaults(defineProps<AppSeed>(), {
app:'' app: '',
}); });
// const appseed = defineProps<AppSeed>(); // const appseed = defineProps<AppSeed>();
const props = ref(appseed); const props = ref(appseed);
function onAppUploaded(responseText :string){ function onAppUploaded(responseText: string) {
let json:AppInfo = JSON.parse(responseText); let json: AppInfo = JSON.parse(responseText);
props.value=json; props.value = json;
} }
</script> </script>

View File

@@ -48,20 +48,20 @@
</q-layout>> </q-layout>>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { useQuasar } from 'quasar' import { useQuasar, QForm } from 'quasar'
// import { useRouter } from 'vue-router'; // import { useRouter } from 'vue-router';
import { ref } from 'vue'; import { ref } from 'vue';
// import { Auth } from '../control/auth' // import { Auth } from '../control/auth'
import { useAuthStore } from 'stores/useAuthStore'; import { useAuthStore } from 'stores/useAuthStore';
const authStore = useAuthStore(); const authStore = useAuthStore();
const $q = useQuasar() const $q = useQuasar()
const loginForm = ref(null); const loginForm = ref<QForm>();
const loading = ref(false); const loading = ref(false);
let title = ref('ログイン'); let title = ref('ログイン');
let email = ref(''); let email = ref('');
let password = ref(''); let password = ref('');
let visibility = ref(false); let visibility = ref(false);
let passwordFieldType = ref('password'); let passwordFieldType = ref<'text'|'password'>('password');
let visibilityIcon = ref('visibility'); let visibilityIcon = ref('visibility');
const required = (val:string) => { const required = (val:string) => {
return (val && val.length > 0 || '必須項目') return (val && val.length > 0 || '必須項目')
@@ -78,35 +78,41 @@ import { useAuthStore } from 'stores/useAuthStore';
passwordFieldType.value = visibility.value ? 'text' : 'password' passwordFieldType.value = visibility.value ? 'text' : 'password'
visibilityIcon.value = visibility.value ? 'visibility_off' : 'visibility' visibilityIcon.value = visibility.value ? 'visibility_off' : 'visibility'
} }
const submit = async () =>{ const submit = () => {
loading.value=true; if (!loginForm.value) {
try { return;
const result = await authStore.login(email.value,password.value);
loading.value=false;
if(result){
$q.notify({
icon: 'done',
color: 'positive',
message: 'ログイン成功'
});
}
else{
$q.notify({
icon: 'error',
color: 'negative',
message: 'ログイン失敗'
});
}
}catch (error) {
console.error(error);
loading.value=false;
$q.notify({
icon: 'error',
color: 'negative',
message: 'ログイン失敗'
});
} }
loginForm.value.validate().then(async (success) => {
if (success) {
loading.value=true;
try {
const result = await authStore.login(email.value,password.value);
loading.value=false;
if(result){
$q.notify({
icon: 'done',
color: 'positive',
message: 'ログイン成功'
});
}
else{
$q.notify({
icon: 'error',
color: 'negative',
message: 'ログイン失敗'
});
}
} catch (error) {
console.error(error);
loading.value=false;
$q.notify({
icon: 'error',
color: 'negative',
message: 'ログイン失敗'
});
}
}
})
} }
</script> </script>

View File

@@ -0,0 +1,250 @@
<template>
<div class="q-pa-md">
<div class="q-gutter-sm row items-start">
<q-breadcrumbs>
<q-breadcrumbs-el icon="work" label="ロールの割り当て" />
</q-breadcrumbs>
</div>
<div class="row" style="min-height: 80vh;">
<!-- left role panel -->
<div class="col-auto">
<div class="q-pa-md" style="width: 250px">
<q-list bordered separator class="rounded-borders">
<q-item active v-if="allLoading" active-class="menu-active">
<q-item-section class="text-weight-bold"> 読み込み中... </q-item-section>
</q-item>
<q-item v-else v-for="item in roles" :key="item.id" clickable v-ripple :active="selected?.id === item.id" @click="roleClicked(item)" active-class="menu-active">
<q-item-section class="text-weight-bold"> {{ item.name }} </q-item-section>
</q-item>
</q-list>
</div>
</div>
<!-- right table panel -->
<div class="col">
<q-table title="ユーザーリスト" :rows="rows" :columns="columns" row-key="id" :filter="filter" :loading="allLoading || loading"
:pagination="pagination" >
<template v-slot:top>
<q-btn color="primary" :disable="allLoading || loading || selected?.id == EMPTY_ROLE.id" label="追加" @click="showAddRoleDialog" />
<q-space />
<q-input borderless dense filled debounce="300" v-model="filter" placeholder="検索">
<template v-slot:append>
<q-icon name="search" />
</template>
</q-input>
</template>
<template v-slot:body-cell-status="props">
<q-td :props="props">
<div class="row">
<div v-if="props.row.isActive">
<q-chip square color="positive" text-color="white" icon="done" label="使用可能" size="sm" />
</div>
<div v-else>
<q-chip square color="negative" text-color="white" icon="block" label="使用不可" size="sm" />
</div>
<q-chip v-if="props.row.isSuperuser" square color="accent" text-color="white" icon="admin_panel_settings"
label="システム管理者" size="sm" />
</div>
</q-td>
</template>
<template v-slot:header-cell-status="p">
<q-th :props="p">
<div class="row items-center">
<label class="q-mr-md">{{ p.col.label }}</label>
<q-select v-model="statusFilter" :options="statusFilterOptions" borderless
dense options-dense style="font-size: 12px; padding-top: 1px;" />
</div>
</q-th>
</template>
<template v-if="selected && selected.id > 0" v-slot:body-cell-actions="p">
<q-td auto-width :props="p">
<table-action-menu :row="p.row" minWidth="180px" max-width="200px" :actions="actionList" />
</q-td>
</template>
</q-table>
</div>
</div>
<show-dialog v-model:visible="showSelectUser" name="ユーザー選択" @close="closeSelectUserDialog" min-width="50vw" min-height="50vh" :ok-btn-auto-close="false" :ok-btn-loading="isAdding">
<template v-slot:toolbar>
<q-input dense debounce="300" v-model="dgFilter" placeholder="検索" clearable>
<template v-slot:before>
<q-icon name="search" />
</template>
</q-input>
</template>
<user-select-box ref="userDialog" name="ユーザー" type="single" :filter="dgFilter" :filterInitRowsFunc="filterInitRows" />
</show-dialog>
<q-dialog v-model="deleteDialog" persistent>
<q-card>
<q-card-section class="row items-center">
<q-icon name="warning" color="warning" size="2em" />
<div class="q-ml-sm text-weight-bold">ロールメンバーを削除</div>
</q-card-section>
<q-card-section class="q-py-none">
<!-- <span class="q-ml-sm">この役割を与えられたユーザーはメンバー役に再配置されます</span> -->
<div class="q-mx-sm">ユーザー{{targetRow?.email}}{{selected?.name}}の役割から</div>
<div class="q-mx-sm">本当に外しますか</div>
</q-card-section>
<q-card-actions align="right">
<q-btn flat label="Cancel" color="primary" v-close-popup />
<q-btn flat label="OK" color="primary" :loading="deleteUserRoleLoading" @click="deleteUserRole" />
</q-card-actions>
</q-card>
</q-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, computed, watch } from 'vue';
import { IRolesDisplay, IUserRolesDisplay } from 'src/types/UserTypes';
import { useUserStore } from 'stores/useUserStore';
import ShowDialog from 'src/components/ShowDialog.vue';
import UserSelectBox from 'src/components/dialog/UserSelectBox.vue';
import TableActionMenu from 'components/TableActionMenu.vue';
const userStore = useUserStore();
const columns = [
{ name: 'id', label: 'ID', field: 'id', align: 'left', sortable: true },
{ name: 'lastName', label: '氏名', field: 'lastName', align: 'left', sortable: true },
{ name: 'firstName', label: '苗字', field: 'firstName', align: 'left', sortable: true },
{ name: 'email', label: '電子メール', field: 'email', align: 'left', sortable: true },
{ name: 'status', label: '状況', field: 'status', align: 'left' },
{ name: 'actions', label: '', field: 'actions', align: 'right' }
];
const statusFilterOptions = [
{ label: '全データ', filter: () => true },
{ label: 'システム管理者のみ', filter: (row: IUserRolesDisplay) => row.isSuperuser },
{ label: '使用可能', filter: (row: IUserRolesDisplay) => row.isActive },
{ label: '使用不可', filter: (row: IUserRolesDisplay) => !row.isActive },
]
const pagination = ref({ sortBy: 'id', descending: true, rowsPerPage: 20 });
const roles = computed(() => {
if (userStore.roles.length > 0) {
return userStore.roles.concat(EMPTY_ROLE);
}
return userStore.roles;
});
const selected = ref<IRolesDisplay>();
const allLoading = ref(true);
const loading = ref(false);
const filter = ref('');
const statusFilter = ref(statusFilterOptions[0]);
const dgFilter = ref('');
const allUsers = computed(() => userStore.users.filter(statusFilter.value.filter));
const targetRow = ref<IUserRolesDisplay>();
const userDialog = ref();
const showSelectUser=ref(false);
const isAdding = ref(false);
const deleteDialog = ref(false);
const deleteUserRoleLoading = ref(false);
const EMPTY_ROLE: IRolesDisplay = {
id: -2,
name: 'ロールなし',
key: 'dummy',
level: -1
}
const actionList = [
// { label: '移動', icon: 'account_tree', action: toEditFlowPage },
// { separator: true },
{ label: '削除', icon: 'delete_outline', class: 'text-red', action: removeRow },
];
const rows = computed(() => allUsers.value.filter((item) => {
if (!selected.value) {
return false;
}
if (selected.value.id == -2) {
return !item.roles || item.roles.length == 0;
}
return item.roleIds?.includes(selected.value.id);
}));
const rowIds = computed(() => new Set(rows.value?.map((item) => item.id)));
const getUsers = async () => {
loading.value = true;
await userStore.loadUsers();
loading.value = false;
}
onMounted(async () => {
allLoading.value = true;
await Promise.all([
userStore.loadRoles(),
getUsers()
]);
allLoading.value = false;
});
watch(roles, (newRoles) => {
if (newRoles.length > 0) {
selected.value = newRoles[0];
}
});
const roleClicked = async (role: IRolesDisplay) => {
selected.value = role;
}
const filterInitRows = (user: IUserRolesDisplay) => {
return !rowIds.value.has(user.id);
}
const showAddRoleDialog = () => {
showSelectUser.value = true;
isAdding.value = false;
dgFilter.value = ''
}
const closeSelectUserDialog = async (val: 'OK'|'Cancel') => {
showSelectUser.value = true;
if (val == 'OK' && userDialog.value.selected[0] && selected.value && selected.value.id >= 0) {
isAdding.value = true;
await userStore.addRole(userDialog.value.selected[0], selected.value);
await getUsers();
}
showSelectUser.value = false;
isAdding.value = false;
}
function removeRow(user: IUserRolesDisplay) {
targetRow.value = user;
deleteDialog.value = true;
}
const deleteUserRole = async () => {
if (targetRow.value?.id && selected.value && selected.value.id >= 0) {
deleteUserRoleLoading.value = true;
await userStore.removeRole(targetRow.value, selected.value);
await getUsers();
deleteUserRoleLoading.value = false;
deleteDialog.value = false;
}
}
</script>
<style lang="scss" scoped>
.menu-active {
color: white;
background: var(--q-primary)
}
</style>

View File

@@ -66,8 +66,8 @@ interface Props {
actions: string[]; actions: string[];
} }
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
title: "ルールエディター", title: 'ルールエディター',
actions: () => ["フィールド制御", "一覧画面", "その他"] actions: () => ['フィールド制御', '一覧画面', 'その他']
}); });
function onItemClick(evt: Event) { function onItemClick(evt: Event) {
return; return;

View File

@@ -2,13 +2,13 @@
<div class="q-pa-md"> <div class="q-pa-md">
<div class="q-gutter-sm row items-start"> <div class="q-gutter-sm row items-start">
<q-breadcrumbs> <q-breadcrumbs>
<q-breadcrumbs-el icon="domain" label="ドメイン管理" /> <q-breadcrumbs-el icon="domain" label="接続先管理" />
</q-breadcrumbs> </q-breadcrumbs>
</div> </div>
<q-table :rows="rows" :columns="columns" row-key="id" :filter="filter" :loading="loading" :pagination="pagination"> <q-table :rows="rows" :columns="columns" row-key="id" :filter="filter" :loading="loading" :pagination="pagination">
<template v-slot:top> <template v-slot:top>
<q-btn color="primary" :disable="loading" label="新規" @click="addRow" /> <q-btn v-permissions="Actions.domain.add" color="primary" :disable="loading" label="新規" @click="addRow" />
<q-space /> <q-space />
<q-input borderless dense filled debounce="300" v-model="filter" placeholder="検索"> <q-input borderless dense filled debounce="300" v-model="filter" placeholder="検索">
<template v-slot:append> <template v-slot:append>
@@ -31,6 +31,13 @@
</q-td> </q-td>
</template> </template>
<template v-slot:body-cell-owner="p">
<q-td auto-width :props="p">
<q-badge v-if="isOwner(p.row)" color="purple">自分</q-badge>
<span v-else>{{ p.row.owner.fullName }}</span>
</q-td>
</template>
<template v-slot:body-cell-actions="p"> <template v-slot:body-cell-actions="p">
<q-td :props="p"> <q-td :props="p">
<table-action-menu :row="p.row" :actions="actionList" /> <table-action-menu :row="p.row" :actions="actionList" />
@@ -112,28 +119,34 @@
<q-dialog v-model="confirm" persistent> <q-dialog v-model="confirm" persistent>
<q-card> <q-card>
<!-- -1 loading -->
<q-card-section v-if="deleteLoadingState == -1" class="row items-center"> <q-card-section v-if="deleteLoadingState == -1" class="row items-center">
<q-spinner color="primary" size="2em"/> <q-spinner color="primary" size="2em"/>
<span class="q-ml-sm">ドメイン利用権限を確認中</span> <span class="q-ml-sm">ドメイン利用権限を確認中</span>
</q-card-section> </q-card-section>
<q-card-section v-else-if="deleteLoadingState == 0" class="row items-center"> <!-- > 0 can't delete -->
<q-card-section v-else-if="deleteLoadingState > 0" class="row items-center">
<q-icon name="error" color="negative" size="2em" />
<span class="q-ml-sm">ドメインは使用中です。削除してもよろしいですか?</span>
</q-card-section>
<!-- 0/-2 can delete -->
<q-card-section v-else class="row items-center">
<q-icon name="warning" color="warning" size="2em" /> <q-icon name="warning" color="warning" size="2em" />
<span class="q-ml-sm">削除してもよろしいですか?</span> <span class="q-ml-sm">削除してもよろしいですか?</span>
</q-card-section> </q-card-section>
<q-card-section v-else class="row items-center">
<q-icon name="error" color="negative" size="2em" />
<span class="q-ml-sm">ドメイン利用権限が存在しキャンセルする必要がある</span>
</q-card-section>
<q-card-actions align="right"> <q-card-actions align="right">
<q-btn flat label="Cancel" color="primary" v-close-popup /> <q-btn flat label="キャンセル" color="primary" v-close-popup />
<q-btn v-if="deleteLoadingState > 0" label="処理に行く" color="primary" v-close-popup @click="openShareDg(editId)" /> <!-- > 0 can't delete -->
<q-btn flat v-else label="OK" :disabled="deleteLoadingState" color="primary" v-close-popup @click="deleteDomain()" /> <q-btn v-if="deleteLoadingState > 0" label="実行" color="primary" v-close-popup @click="openShareDg(SHARE_USE, editId)" />
<!-- 0/-2 can delete -->
<q-btn flat v-else label="OK" :disabled="deleteLoadingState == -1" :loading="deleteLoadingState == -2" color="primary" @click="deleteDomain()" />
</q-card-actions> </q-card-actions>
</q-card> </q-card>
</q-dialog> </q-dialog>
<share-domain-dialog v-model="shareDg" :domain="shareDomain" @close="closeShareDg()" /> <share-usage-dialog v-model="shareDg" :domain="shareDomain" @close="shareDg = false" />
<share-manage-dialog v-model="shareManageDg" :domain="shareDomain" @close="shareManageDg = false" />
</div> </div>
</template> </template>
@@ -141,14 +154,14 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted, computed } from 'vue'; import { ref, onMounted, computed } from 'vue';
import { api } from 'boot/axios'; import { api } from 'boot/axios';
import { Actions } from 'boot/permissions';
import { useAuthStore } from 'stores/useAuthStore'; import { useAuthStore } from 'stores/useAuthStore';
import { useDomainStore } from 'stores/useDomainStore'; import ShareUsageDialog from 'components/ShareDomain/ShareUsageDialog.vue';
import ShareDomainDialog from 'components/ShareDomain/ShareDomainDialog.vue'; import ShareManageDialog from 'components/ShareDomain/ShareManageDialog.vue';
import TableActionMenu from 'components/TableActionMenu.vue'; import TableActionMenu from 'components/TableActionMenu.vue';
import { IDomain, IDomainOwnerDisplay, IDomainSubmit } from '../types/DomainTypes'; import { IDomain, IDomainDisplay, IDomainOwnerDisplay, IDomainSubmit } from '../types/DomainTypes';
const authStore = useAuthStore(); const authStore = useAuthStore();
const domainStore = useDomainStore();
const inactiveRowClass = (row: IDomainOwnerDisplay) => row.domainActive ? '' : 'inactive-row'; const inactiveRowClass = (row: IDomainOwnerDisplay) => row.domainActive ? '' : 'inactive-row';
const columns = [ const columns = [
@@ -163,17 +176,17 @@ const columns = [
// classes: inactiveRowClass // classes: inactiveRowClass
// }, // },
{ name: 'name', label: '環境名', field: 'name', align: 'left', sortable: true, classes: inactiveRowClass }, { name: 'name', label: '環境名', field: 'name', align: 'left', sortable: true, classes: inactiveRowClass },
{ name: 'active', label: 'x', align: 'left', field: 'domainActive', classes: inactiveRowClass }, { name: 'active', label: '', align: 'left', field: 'domainActive', classes: inactiveRowClass },
{ name: 'url', label: 'URL', field: 'url', align: 'left', sortable: true, classes: inactiveRowClass }, { name: 'url', label: 'URL', field: 'url', align: 'left', sortable: true, classes: inactiveRowClass },
{ name: 'user', label: 'ログイン名', field: 'user', align: 'left', classes: inactiveRowClass }, { name: 'user', label: 'ログイン名', field: 'user', align: 'left', classes: inactiveRowClass },
{ name: 'owner', label: '所有者', field: row => row.owner.fullName, align: 'left', classes: inactiveRowClass }, { name: 'owner', label: '所有者', field: '', align: 'left', classes: inactiveRowClass },
{ name: 'actions', label: '', field: 'actions', classes: inactiveRowClass } { name: 'actions', label: '', field: 'actions', classes: inactiveRowClass }
]; ];
const pagination = ref({ sortBy: 'id', descending: true, rowsPerPage: 20 }); const pagination = ref({ sortBy: 'id', descending: true, rowsPerPage: 20 });
const loading = ref(false); const loading = ref(false);
const addEditLoading = ref(false); const addEditLoading = ref(false);
const deleteLoadingState = ref<number>(-1); // -1: loading, 0: allow, > 0: user count const deleteLoadingState = ref<number>(-1); // -2: deleteLoading, -1: loading, 0: allow, > 0: user count
const filter = ref(''); const filter = ref('');
const rows = ref<IDomainOwnerDisplay[]>([]); const rows = ref<IDomainOwnerDisplay[]>([]);
@@ -193,10 +206,11 @@ const domainActive = ref(true);
const isCreate = ref(true); const isCreate = ref(true);
let editId = ref(0); let editId = ref(0);
const shareDg = ref(false); const shareDg = ref(false);
const shareManageDg = ref(false);
const shareDomain = ref<IDomainOwnerDisplay>({} as IDomainOwnerDisplay); const shareDomain = ref<IDomainOwnerDisplay>({} as IDomainOwnerDisplay);
const activeOptions = [ const activeOptions = [
{ value: 0, label: '全状態' }, { value: 0, label: 'すべて' },
{ value: 1, label: '使用' }, { value: 1, label: '使用' },
{ value: 2, label: '未使用'} { value: 2, label: '未使用'}
] ]
@@ -216,16 +230,27 @@ const activeFilterUpdate = (option: {value: number}) => {
} }
} }
const SHARE_USE = 'use';
const SHARE_MANAGE = 'manage';
const actionList = [ const actionList = [
{ label: '編集', icon: 'edit_note', action: editRow }, { label: '編集', icon: 'edit_note', permission: Actions.domain.edit, action: editRow },
{ label: '利用権限設定', icon: 'person_add_alt', action: openShareDg }, { label: '利用権限設定', icon: 'person_add_alt', permission: Actions.domain.grantUse,
action: (row: IDomainOwnerDisplay) => {openShareDg(SHARE_USE, row)} },
{ label: '管理権限設定', icon: 'add_moderator', permission: Actions.domain.grantManage,
disable: (row: IDomainOwnerDisplay) => !isOwner(row),
tooltip: (row: IDomainOwnerDisplay) => isOwner(row) ? '' : 'ドメイン所有者でないため、操作できません',
action: (row: IDomainOwnerDisplay) => {openShareDg(SHARE_MANAGE, row)}
},
{ separator: true }, { separator: true },
{ label: '削除', icon: 'delete_outline', class: 'text-red', action: removeRow }, { label: '削除', icon: 'delete_outline', permission: Actions.domain.delete, class: 'text-red', action: removeRow },
]; ];
const isOwner = (row: IDomainOwnerDisplay) => row.owner.id === Number(authStore.userId);
const getDomain = async (filter?: (row: IDomainOwnerDisplay) => boolean) => { const getDomain = async (filter?: (row: IDomainOwnerDisplay) => boolean) => {
loading.value = true; loading.value = true;
const { data } = await api.get<{data:IDomain[]}>(`api/domains`); const { data } = await api.get<{data:IDomain[]}>('api/domains');
rows.value = data.data.map((item) => { rows.value = data.data.map((item) => {
return { return {
id: item.id, id: item.id,
@@ -271,18 +296,20 @@ async function removeRow(row: IDomainOwnerDisplay) {
} }
const deleteDomain = () => { const deleteDomain = () => {
deleteLoadingState.value = -2;
api.delete(`api/domain/${editId.value}`).then(({ data }) => { api.delete(`api/domain/${editId.value}`).then(({ data }) => {
if (!data.data) { if (!data.data) {
// TODO dialog // TODO dialog
} }
confirm.value = false;
deleteLoadingState.value = -1;
getDomain(); getDomain();
// authStore.setCurrentDomain(); // authStore.setCurrentDomain();
}) })
editId.value = 0; // set in removeRow() editId.value = 0; // set in removeRow()
deleteLoadingState.value = -1;
}; };
function editRow(row) { function editRow(row: any) {
isCreate.value = false isCreate.value = false
editId.value = row.id; editId.value = row.id;
// tenantid.value = row.tenantid; // tenantid.value = row.tenantid;
@@ -323,31 +350,30 @@ const onSubmit = () => {
'ownerid': authStore.userId || '' 'ownerid': authStore.userId || ''
} }
// for search: api.put(`api/domain`)、api.post(`api/domain`) // for search: api.put(`api/domain`)、api.post(`api/domain`)
api[method].apply(api, [`api/domain`, param]).then(async (resp: any) => { api[method].apply(api, ['api/domain', param]).then(async (resp: any) => {
const res = resp.data; const res = resp.data;
if (res.data.id === currentDomainId.value && !res.data.is_active) { if (res.data.id === currentDomainId.value && !res.data.is_active) {
await authStore.setCurrentDomain(); await authStore.setCurrentDomain();
} }
getDomain(); getDomain();
domainStore.loadUserDomains();
closeDg(); closeDg();
onReset(); onReset();
addEditLoading.value = false; addEditLoading.value = false;
}) })
} }
function openShareDg(row: IDomainOwnerDisplay|number) { function openShareDg(type: typeof SHARE_MANAGE|typeof SHARE_USE, row: IDomainOwnerDisplay|number) {
if (typeof row === 'number') { if (typeof row === 'number') {
row = rows.value.find(item => item.id === row) as IDomainOwnerDisplay; row = rows.value.find(item => item.id === row) as IDomainOwnerDisplay;
} }
shareDomain.value = row ; shareDomain.value = row;
shareDg.value = true; if (type === SHARE_USE) {
shareDg.value = true;
} else if (type === SHARE_MANAGE) {
shareManageDg.value = true;
}
}; };
function closeShareDg() {
shareDg.value = false;
}
const onReset = () => { const onReset = () => {
name.value = ''; name.value = '';
url.value = ''; url.value = '';

View File

@@ -3,7 +3,7 @@
<div class="q-pa-md"> <div class="q-pa-md">
<div class="q-gutter-sm row items-start"> <div class="q-gutter-sm row items-start">
<q-breadcrumbs> <q-breadcrumbs>
<q-breadcrumbs-el icon="assignment_ind" label="ドメイン適用" /> <q-breadcrumbs-el icon="assignment_ind" label="接続先の割り当て" />
</q-breadcrumbs> </q-breadcrumbs>
</div> </div>
@@ -14,7 +14,7 @@
<q-btn class="q-mx-none" color="primary" label="追加" @click="clickAddDomain()" /> <q-btn class="q-mx-none" color="primary" label="追加" @click="clickAddDomain()" />
<q-space /> <q-space />
<div class="row q-gutter-md"> <div class="row q-gutter-md">
<q-input borderless dense filled debounce="300" v-model="userDomainTableFilter" placeholder="Search"> <q-input borderless dense filled debounce="300" v-model="userDomainTableFilter" placeholder="検索">
<template v-slot:append> <template v-slot:append>
<q-icon name="search" /> <q-icon name="search" />
</template> </template>
@@ -36,7 +36,9 @@
既定 既定
</q-chip> </q-chip>
<q-btn flat v-else :loading="activeDomainLoadingId === props.row.id" :disable="deleteDomainLoadingId === props.row.id" @click="activeDomain(props.row)">既定にする</q-btn> <q-btn flat v-else :loading="activeDomainLoadingId === props.row.id" :disable="deleteDomainLoadingId === props.row.id" @click="activeDomain(props.row)">既定にする</q-btn>
<q-btn flat :disable="isNotOwner(props.row.owner.id) || activeDomainLoadingId === props.row.id" :text-color="isNotOwner(props.row.owner.id)?'grey':''" :loading="deleteDomainLoadingId === props.row.id" @click="clickDeleteConfirm(props.row)">削除</q-btn> <q-btn flat :disable="activeDomainLoadingId === props.row.id" :loading="deleteDomainLoadingId === props.row.id" @click="clickDeleteConfirm(props.row)">
削除
</q-btn>
</q-card-actions> </q-card-actions>
</template> </template>
</domain-card> </domain-card>
@@ -112,7 +114,10 @@ const addUserDomainFinished = async (val: string) => {
const selected = addDomainRef.value.selected; const selected = addDomainRef.value.selected;
if (val == 'OK' && selected.length > 0) { if (val == 'OK' && selected.length > 0) {
addUserDomainLoading.value = true; addUserDomainLoading.value = true;
const { data } = await api.post(`api/domain/${authStore.userId}?domainid=${selected[0].id}`) const { data } = await api.post('api/userdomain', {
userid: authStore.userId,
domainid: selected[0].id,
});
if (rows.value.length === 0 && data.data) { if (rows.value.length === 0 && data.data) {
const domain = data.data; const domain = data.data;
await authStore.setCurrentDomain({ await authStore.setCurrentDomain({
@@ -162,15 +167,11 @@ const isActive = computed(() => (id: number) => {
return id == activeDomainId.value; return id == activeDomainId.value;
}); });
const isNotOwner = computed(() => (ownerId: string) => {
return ownerId !== authStore.userId;
});
const getDomain = async (userId? : string) => { const getDomain = async (userId? : string) => {
rowIds.clear(); rowIds.clear();
const resp = await api.get(`api/defaultdomain`); const resp = await api.get('api/defaultdomain');
activeDomainId.value = resp?.data?.data?.id; activeDomainId.value = resp?.data?.data?.id;
const domainResult = userId ? await api.get(`api/domain?userId=${userId}`) : await api.get(`api/domain`); const domainResult = userId ? await api.get(`api/domain?userId=${userId}`) : await api.get('api/domain');
const domains = domainResult.data as any[]; const domains = domainResult.data as any[];
rows.value = domains.sort((a, b) => a.id - b.id).reduce((acc, item) => { rows.value = domains.sort((a, b) => a.id - b.id).reduce((acc, item) => {
rowIds.add(item.id); rowIds.add(item.id);

View File

@@ -9,7 +9,7 @@
:pagination="pagination" > :pagination="pagination" >
<template v-slot:top> <template v-slot:top>
<q-btn color="primary" :disable="loading" label="新規" @click="addRow" /> <q-btn v-permissions="Actions.user.add" color="primary" :disable="loading" label="新規" @click="addRow" />
<q-space /> <q-space />
<q-input borderless dense filled debounce="300" v-model="filter" placeholder="検索"> <q-input borderless dense filled debounce="300" v-model="filter" placeholder="検索">
<template v-slot:append> <template v-slot:append>
@@ -27,30 +27,34 @@
<div v-else> <div v-else>
<q-chip square color="negative" text-color="white" icon="block" label="使用不可" size="sm" /> <q-chip square color="negative" text-color="white" icon="block" label="使用不可" size="sm" />
</div> </div>
<q-chip v-if="props.row.isSuperuser" square color="accent" text-color="white" icon="admin_panel_settings"
label="システム管理者" size="sm" />
</div> </div>
</q-td> </q-td>
</template> </template>
<template v-slot:body-cell-roles="props">
<q-td :props="props">
<div class="row">
<q-chip v-if="(props.row as IUserRolesDisplay).isSuperuser" square color="accent" text-color="white" icon="admin_panel_settings"
label="システム管理者" size="sm" />
<q-chip v-else v-for="(item) in (props.row as IUserRolesDisplay).roles" square color="primary" text-color="white" :key="item.id" :label="item.name" size="sm" />
</div>
</q-td>
</template>
<template v-slot:header-cell-status="p"> <template v-slot:header-cell-status="p">
<q-th :props="p"> <q-th :props="p">
<div class="row items-center"> <div class="row items-center">
<label class="q-mr-md">{{ p.col.label }}</label> <label class="q-mr-md">{{ p.col.label }}</label>
<q-select v-model="statusFilter" :options="options" @update:model-value="updateStatusFilter" borderless <q-select v-model="statusFilter" :options="statusFilterOptions" borderless
dense options-dense style="font-size: 12px; padding-top: 1px;" /> dense options-dense style="font-size: 12px; padding-top: 1px;" />
</div> </div>
</q-th> </q-th>
</template> </template>
<template v-slot:body-cell-actions="p"> <template v-slot:body-cell-actions="p">
<q-td :props="p"> <q-td auto-width :props="p">
<q-btn-group flat> <table-action-menu :row="p.row" minWidth="180px" max-width="200px" :actions="actionList" />
<q-btn flat color="primary" padding="xs" size="1em" icon="edit_note" @click="editRow(p.row)" />
<q-btn flat color="negative" padding="xs" size="1em" icon="delete_outline" @click="removeRow(p.row)" />
</q-btn-group>
</q-td> </q-td>
</template> </template>
</q-table> </q-table>
@@ -153,8 +157,13 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted } from 'vue'; import { ref, onMounted, computed } from 'vue';
import { api } from 'boot/axios'; import TableActionMenu from 'components/TableActionMenu.vue';
import { useUserStore } from 'stores/useUserStore';
import { IUserDisplay, IUserRolesDisplay } from 'src/types/UserTypes';
import { Actions } from 'boot/permissions';
const userStore = useUserStore();
const columns = [ const columns = [
{ name: 'id', label: 'ID', field: 'id', align: 'left', sortable: true }, { name: 'id', label: 'ID', field: 'id', align: 'left', sortable: true },
@@ -162,16 +171,25 @@ const columns = [
{ name: 'firstName', label: '苗字', field: 'firstName', align: 'left', sortable: true }, { name: 'firstName', label: '苗字', field: 'firstName', align: 'left', sortable: true },
{ name: 'email', label: '電子メール', field: 'email', align: 'left', sortable: true }, { name: 'email', label: '電子メール', field: 'email', align: 'left', sortable: true },
{ name: 'status', label: '状況', field: 'status', align: 'left' }, { name: 'status', label: '状況', field: 'status', align: 'left' },
{ name: 'actions', label: '操作', field: 'actions' } { name: 'roles', label: 'ロール', field: '', align: 'left' },
{ name: 'actions', label: '', field: 'actions' }
]; ];
const statusFilterOptions = [
{ label: '全データ', filter: () => true },
{ label: 'システム管理者のみ', filter: (row: IUserRolesDisplay) => row.isSuperuser },
{ label: '使用可能', filter: (row: IUserRolesDisplay) => row.isActive },
{ label: '使用不可', filter: (row: IUserRolesDisplay) => !row.isActive },
]
const pagination = ref({ sortBy: 'id', descending: true, rowsPerPage: 20 }); const pagination = ref({ sortBy: 'id', descending: true, rowsPerPage: 20 });
const loading = ref(false); const loading = ref(false);
const addEditLoading = ref(false); const addEditLoading = ref(false);
const filter = ref(''); const filter = ref('');
const statusFilter = ref('全データ'); const statusFilter = ref(statusFilterOptions[0]);
const rows = ref([]); const rows = computed(() => userStore.users.filter(statusFilter.value.filter));
const show = ref(false); const show = ref(false);
const confirm = ref(false); const confirm = ref(false);
const resetPsw = ref(false); const resetPsw = ref(false);
@@ -184,69 +202,47 @@ const isActive = ref(true);
const isPwd = ref(true); const isPwd = ref(true);
const pwd = ref(''); const pwd = ref('');
const isCreate = ref(true); const editId = ref(0);
let editId = ref(0); const isCreate = computed(() => editId.value <= 0);
const getUsers = async (filter = () => true) => { const actionList = [
{ label: '編集', icon: 'edit_note', permission: Actions.user.edit, action: editRow },
{ separator: true },
{ label: '削除', icon: 'delete_outline', permission: Actions.user.delete, class: 'text-red', action: showDeleteUserConfirm },
];
const getUsers = async () => {
loading.value = true; loading.value = true;
const result = await api.get(`api/v1/users`); await userStore.loadUsers();
rows.value = result.data.data.map((item) => {
return { id: item.id, firstName: item.first_name, lastName: item.last_name, email: item.email, isSuperuser: item.is_superuser, isActive: item.is_active }
}).filter(filter);
loading.value = false; loading.value = false;
} }
const updateStatusFilter = (status) => {
switch (status) {
case 'システム管理者のみ':
getUsers((row) => row.isSuperuser)
break;
case '使用可能':
getUsers((row) => row.isActive)
break;
case '使用不可':
getUsers((row) => !row.isActive)
break;
default:
getUsers()
break;
}
}
onMounted(async () => { onMounted(async () => {
await getUsers(); await getUsers();
}) })
const options = ['全データ', 'システム管理者のみ', '使用可能', '使用不可']
// emulate fetching data from server
const addRow = () => { const addRow = () => {
// editId.value
onReset(); onReset();
show.value = true; show.value = true;
} }
const removeRow = (row) => { function showDeleteUserConfirm(row: IUserDisplay) {
confirm.value = true; confirm.value = true;
editId.value = row.id; editId.value = row.id;
} }
const deleteUser = () => { const deleteUser = async () => {
api.delete(`api/v1/users/${editId.value}`).then(() => { await userStore.deleteUser(editId.value);
getUsers(); getUsers();
}) onReset();
editId.value = 0;
}; };
const editRow = (row) => { function editRow(row: IUserDisplay) {
isCreate.value = false
editId.value = row.id; editId.value = row.id;
firstName.value = row.firstName; firstName.value = row.firstName;
lastName.value = row.lastName; lastName.value = row.lastName;
email.value = row.email; email.value = row.email;
pwd.value = row.password;
isSuperuser.value = row.isSuperuser; isSuperuser.value = row.isSuperuser;
isActive.value = row.isActive; isActive.value = row.isActive;
@@ -260,38 +256,25 @@ const closeDg = () => {
onReset(); onReset();
} }
const onSubmit = () => { const onSubmit = async () => {
addEditLoading.value = true; addEditLoading.value = true;
if (editId.value !== 0) { const param = {
api.put(`api/v1/users/${editId.value}`, { id: editId.value,
'first_name': firstName.value, first_name: firstName.value,
'last_name': lastName.value, last_name: lastName.value,
'is_superuser': isSuperuser.value, is_superuser: isSuperuser.value,
'is_active': isActive.value, is_active: isActive.value,
'email': email.value, email: email.value,
...(isCreate.value || resetPsw.value ? { password: pwd.value } : {}) password: (isCreate.value || resetPsw.value) ? pwd.value : undefined
}).then(() => {
getUsers();
closeDg();
onReset();
})
} }
else { if (isCreate.value) {
api.post(`api/v1/users`, { await userStore.addUser(param);
'id': 0, } else {
'first_name': firstName.value, await userStore.editUser(param);
'last_name': lastName.value,
'is_superuser': isSuperuser.value,
'is_active': isActive.value,
'email': email.value,
'password': pwd.value
}).then(() => {
getUsers();
closeDg();
onReset();
})
} }
getUsers();
closeDg();
onReset();
} }
const onReset = () => { const onReset = () => {
@@ -303,7 +286,6 @@ const onReset = () => {
isSuperuser.value = false; isSuperuser.value = false;
isPwd.value = true; isPwd.value = true;
editId.value = 0; editId.value = 0;
isCreate.value = true;
resetPsw.value = false; resetPsw.value = false;
addEditLoading.value = false; addEditLoading.value = false;
} }

View File

@@ -54,8 +54,8 @@ const mouseenter = (event: Event) => {
let oDivs = oDiv1?.getElementsByClassName('add'); let oDivs = oDiv1?.getElementsByClassName('add');
if (oDivs.length === 0) { if (oDivs.length === 0) {
let oDiv2 = document.createElement('div'); let oDiv2 = document.createElement('div');
oDiv2.className = "add"; oDiv2.className = 'add';
oDiv2.setAttribute("style", "display:table-row;height:inherit;position: absolute;left:calc(50% - 19px);"); oDiv2.setAttribute('style', 'display:table-row;height:inherit;position: absolute;left:calc(50% - 19px);');
oDiv2.innerHTML = oAdd; oDiv2.innerHTML = oAdd;
oDiv1?.append(oDiv2); oDiv1?.append(oDiv2);
} }

View File

@@ -34,13 +34,14 @@ const routerInstance = createRouter({
export default route(function (/* { store, ssrContext } */) { export default route(function (/* { store, ssrContext } */) {
routerInstance.beforeEach(async (to) => { routerInstance.beforeEach(async (to, from) => {
// clear alert on route change // clear alert on route change
//const alertStore = useAlertStore(); //const alertStore = useAlertStore();
//alertStore.clear(); //alertStore.clear();
// redirect to login page if not logged in and trying to access a restricted page // redirect to login page if not logged in and trying to access a restricted page
const publicPages = ['/login']; const loginPage = '/login';
const publicPages = [loginPage];
const authRequired = !publicPages.includes(to.path); const authRequired = !publicPages.includes(to.path);
const authStore = useAuthStore(); const authStore = useAuthStore();
@@ -49,12 +50,16 @@ export default route(function (/* { store, ssrContext } */) {
return '/login'; return '/login';
} }
if (authStore.token && to.path === loginPage) {
return from.path == '/' ? '/' : false;
}
// redirect to domain setting page if no domain exist // redirect to domain setting page if no domain exist
const domainPages = [...publicPages, '/domain', '/userDomain', '/user']; const domainPages = [...publicPages, '/domain', '/userDomain', '/user', '/role'];
if (!authStore.hasDomain && !domainPages.includes(to.path)) { if (!authStore.hasDomain && !domainPages.includes(to.path)) {
Dialog.create({ Dialog.create({
title: '注意', title: '注意',
message: '既定/利用可能なドメインはありません。<br>ドメイン管理ページに遷移して処理します。', message: '既定/利用可能なドメインはありません。<br>接続先管理ページに遷移して処理します。',
html: true, html: true,
persistent: true, persistent: true,
}) })

View File

@@ -16,18 +16,18 @@ const routes: RouteRecordRaw[] = [
path: '/', path: '/',
component: () => import('layouts/MainLayout.vue'), component: () => import('layouts/MainLayout.vue'),
children: [ children: [
{ path: '', component: () => import('pages/IndexPage.vue') }, { path: '', component: () => import('pages/IndexPage.vue'), props: { app: '' } },
{ path: 'ruleEditor', component: () => import('pages/RuleEditor.vue') }, { path: 'ruleEditor', component: () => import('pages/RuleEditor.vue') },
{ path: 'test', component: () => import('pages/testQursar.vue') },
{ path: 'flow', component: () => import('pages/testFlow.vue') }, { path: 'flow', component: () => import('pages/testFlow.vue') },
{ path: 'FlowChartTest', component: () => import('pages/FlowChartTest.vue') }, { path: 'FlowChartTest', component: () => import('pages/FlowChartTest.vue') },
{ path: 'flowEditor', component: () => import('pages/FlowEditorPage.vue') }, { path: 'flowEditor', component: () => import('pages/FlowEditorPage.vue') },
// { path: 'FlowChart', component: () => import('pages/FlowChart.vue') }, // { path: 'FlowChart', component: () => import('pages/FlowChart.vue') },
{ path: 'right', component: () => import('pages/testRight.vue') },
{ path: 'domain', component: () => import('pages/TenantDomain.vue') }, { path: 'domain', component: () => import('pages/TenantDomain.vue') },
{ path: 'userdomain', component: () => import('pages/UserDomain.vue')}, { path: 'userdomain', component: () => import('pages/UserDomain.vue')},
{ path: 'user', component: () => import('pages/UserManagement.vue')}, { path: 'user', component: () => import('pages/UserManagement.vue')},
{ path: 'app', component: () => import('pages/AppManagement.vue')}, { path: 'app', component: () => import('pages/AppManagement.vue')},
{ path: 'app/version/:id', component: () => import('pages/AppVersionManagement.vue')},
{ path: 'role', component: () => import('pages/RoleManagement.vue')},
{ path: 'condition', component: () => import('pages/conditionPage.vue') } { path: 'condition', component: () => import('pages/conditionPage.vue') }
], ],
}, },

View File

@@ -8,3 +8,11 @@ declare module '*.vue' {
const component: DefineComponent<{}, {}, any>; const component: DefineComponent<{}, {}, any>;
export default component; export default component;
} }
import { ComponentCustomProperties } from 'vue';
declare module '@vue/runtime-core' {
interface ComponentCustomProperties {
$hasPermission: (permission: any) => boolean;
}
}

View File

@@ -0,0 +1,107 @@
import { defineStore } from 'pinia';
import { api } from 'boot/axios';
import { IAppDisplay, IAppVersion, IAppVersionDisplay, IManagedApp, IVersionSubmit } from 'src/types/AppTypes';
import { date, Notify } from 'quasar'
import { userToUserDisplay } from './useUserStore';
export const useAppStore = defineStore('app', {
state: () => ({
apps: [] as IAppDisplay[],
rowIds: new Set<string>(),
}),
actions: {
async loadApps() {
this.reset();
try {
const { data } = await api.get('api/apps');
this.apps = data.data.map((item: IManagedApp) => {
this.rowIds.add(item.appid);
return appToAppDisplay(item)
}).sort((a: IAppDisplay, b: IAppDisplay) => a.sortId - b.sortId); // set default order
} catch (error) {
Notify.create({
icon: 'error',
color: 'negative',
message: 'アプリ一覧の読み込みに失敗しました'
})
}
},
getAppById(id: string) {
if (!this.rowIds.has(id)) {
return null;
}
return this.apps.find((item: IAppDisplay) => item.id === id);
},
async deleteApp(app: IAppDisplay) {
try {
await api.delete(`api/apps/${app.id}`);
} catch (error) {
console.error(error);
Notify.create({
icon: 'error',
color: 'negative',
message: 'アプリの削除に失敗しました'
});
return false;
}
return true;
},
async getVersionsByAppId(app: IAppDisplay) {
const { data } = await api.get(`api/appversions/${app.id}`);
return data.data.map((item: IAppVersion) => versionToVersionDisplay(item));
},
async changeVersion(app: IAppDisplay, version: IAppVersionDisplay) {
await api.put(`api/appversions/${app.id}/${version.id}`);
},
async createVersion(versionSubmit: IVersionSubmit) {
await api.post('api/apps', {
'appid': versionSubmit.appId,
'versionname': versionSubmit.name,
'comment': versionSubmit.comment
})
},
reset() {
this.apps = [];
this.rowIds.clear();
},
},
});
function versionToVersionDisplay(item: IAppVersion) {
return {
id: item.version,
version: item.version,
appid: item.appid,
name: item.versionname,
comment: item.comment,
updater: userToUserDisplay(item.updateuser),
updateTime: formatDate(item.update_time),
creator: userToUserDisplay(item.createuser),
createTime: formatDate(item.create_time),
} as IAppVersionDisplay;
}
function appToAppDisplay(app: IManagedApp) {
return {
id: app.appid,
sortId: parseInt(app.appid, 10),
name: app.appname,
url: `${app.domainurl}/k/${app.appid}`,
version: app.version,
versionName: app.versionname,
updateTime: formatDate(app.update_time),
updateUser: userToUserDisplay(app.updateuser),
versionChanged: app.is_saved
} as IAppDisplay
}
function formatDate(data: string) {
return date.formatDate(new Date(data + 'Z'), 'YYYY/MM/DD HH:mm');
}

View File

@@ -1,23 +1,31 @@
import { defineStore } from 'pinia'; import { defineStore } from 'pinia';
import { api } from 'boot/axios'; import { api } from 'boot/axios';
import { router } from 'src/router'; import { router } from 'src/router';
import { IDomainInfo } from '../types/DomainTypes'; import { IDomain, IDomainInfo } from '../types/DomainTypes';
import { jwtDecode } from 'jwt-decode'; import { jwtDecode } from 'jwt-decode';
interface UserInfo { import { useAppStore } from './useAppStore';
firstName: string; import { userToUserRolesDisplay, useUserStore } from './useUserStore';
lastName: string; import { IRolesDisplay, IUser, IUserRolesDisplay } from 'src/types/UserTypes';
email: string; import { IResponse } from 'src/types/BaseTypes';
interface IPermission {
id: number,
menu: string,
function: string,
privilege: string,
link: string
} }
type IPermissions = { [key: string]: string };
export interface IUserState { export interface IUserState {
token?: string; token?: string;
returnUrl: string; returnUrl: string;
currentDomain: IDomainInfo; currentDomain: IDomainInfo;
LeftDrawer: boolean; LeftDrawer: boolean;
userId?: string; userInfo: IUserRolesDisplay;
userInfo: UserInfo; tenant: string;
roles:string, permissions: IPermissions;
permissions: string;
} }
export const useAuthStore = defineStore('auth', { export const useAuthStore = defineStore('auth', {
@@ -26,17 +34,22 @@ export const useAuthStore = defineStore('auth', {
returnUrl: '', returnUrl: '',
LeftDrawer: false, LeftDrawer: false,
currentDomain: {} as IDomainInfo, currentDomain: {} as IDomainInfo,
userId: '', userInfo: {} as IUserRolesDisplay,
userInfo: {} as UserInfo, tenant: '',
roles:'', permissions: {} as IPermissions
permissions: '',
}), }),
getters: { getters: {
toggleLeftDrawer(): boolean { userId(): number {
return this.LeftDrawer; return this.userInfo.id;
}, },
hasDomain(): boolean { hasDomain(): boolean {
return this.currentDomain.id !== undefined; return this.currentDomain.id !== undefined;
},
roles(): IRolesDisplay[] {
return this.userInfo.roles;
},
isSuperAdmin(): boolean {
return this.userInfo.isSuperuser;
} }
}, },
actions: { actions: {
@@ -51,43 +64,62 @@ export const useAuthStore = defineStore('auth', {
params.append('username', username); params.append('username', username);
params.append('password', password); params.append('password', password);
try { try {
const result = await api.post(`api/token`, params); const result = await api.post('api/token', params);
console.info(result); // console.info(result);
this.token = result.data.access_token; this.token = result.data.access_token;
const tokenJson = jwtDecode(result.data.access_token); const tokenJson = jwtDecode<{sub: number, tenant: string, roles: string}>(result.data.access_token);
this.userId = tokenJson.sub; this.tenant = tokenJson.tenant;
this.permissions = (tokenJson as any).permissions==='ALL' ? 'admin': 'user'; this.userInfo.id = tokenJson.sub;
this.userInfo.isSuperuser = tokenJson.roles === 'super';
api.defaults.headers['Authorization'] = 'Bearer ' + this.token; api.defaults.headers['Authorization'] = 'Bearer ' + this.token;
this.currentDomain = await this.getCurrentDomain(); await Promise.all([
this.userInfo = await this.getUserInfo(); this.loadCurrentDomain(),
router.push(this.returnUrl || '/'); this.loadUserInfo(),
this.loadPermission()
]);
await router.push(this.returnUrl || '/');
this.returnUrl = '';
return true; return true;
} catch (e) { } catch (e) {
console.error(e); console.error(e);
return false; return false;
} }
}, },
async getCurrentDomain(): Promise<IDomainInfo> { async loadCurrentDomain() {
const resp = await api.get(`api/defaultdomain`); const resp = await api.get<IResponse<IDomain>>('api/defaultdomain');
const activedomain = resp?.data?.data; const activedomain = resp?.data?.data;
return { if (!activedomain) {
id: activedomain?.id, this.currentDomain = {} as IDomainInfo;
domainName: activedomain?.name, } else {
kintoneUrl: activedomain?.url, this.currentDomain = {
}; id: activedomain.id,
}, domainName: activedomain.name,
async getUserInfo():Promise<UserInfo>{ kintoneUrl: activedomain.url,
const resp = (await api.get(`api/v1/users/me`)).data.data; };
return {
firstName: resp.first_name,
lastName: resp.last_name,
email: resp.email,
} }
}, },
logout() { async loadUserInfo() {
const resp = (await api.get<IResponse<IUser>>('api/v1/users/me'))?.data?.data;
this.userInfo = userToUserRolesDisplay(resp)
},
async loadPermission() {
this.permissions = {} as IPermissions;
if (this.isSuperAdmin) return;
const resp = (await api.get<IResponse<IPermission[]>>('api/v1/userpermssions')).data.data;
resp.forEach((permission) => {
this.permissions[permission.link] = permission.menu;
this.permissions[permission.privilege] = permission.function;
});
},
async logout() {
this.token = ''; this.token = '';
this.currentDomain = {} as IDomainInfo; // 清空当前域 this.currentDomain = {} as IDomainInfo; // 清空当前域
router.push('/login'); useAppStore().reset();
useUserStore().reset();
await router.push('/login');
this.tenant = '';
this.userInfo = {} as IUserRolesDisplay;
this.permissions = {} as IPermissions;
}, },
async setCurrentDomain(domain?: IDomainInfo) { async setCurrentDomain(domain?: IDomainInfo) {
if (!domain) { if (!domain) {

View File

@@ -1,23 +0,0 @@
import { defineStore } from 'pinia';
import { api } from 'boot/axios';
import { IDomainInfo, IDomain } from '../types/DomainTypes';
export const useDomainStore = defineStore('domain', {
state: () => ({
userDomains: [] as IDomainInfo[],
}),
actions: {
async loadUserDomains(): Promise<IDomainInfo[]> {
const resp = await api.get(`api/domain`);
const domains = resp.data as IDomain[];
this.userDomains = domains
.filter(data => data.is_active)
.map((data) => ({
id: data.id,
domainName: data.name,
kintoneUrl: data.url,
}));
return this.userDomains;
},
},
});

View File

@@ -0,0 +1,147 @@
import { defineStore } from 'pinia';
import { api } from 'boot/axios';
import { IRoles, IRolesDisplay, IUser, IUserDisplay, IUserRolesDisplay, IUserSubmit } from 'src/types/UserTypes';
import { Notify } from 'quasar'
import { IResponse } from 'src/types/BaseTypes';
export const useUserStore = defineStore('user', {
state: () => ({
users: [] as IUserRolesDisplay[],
userIds: new Set<number>(),
roles: [] as IRolesDisplay[],
}),
actions: {
// -------------------------- users --------------------------
async loadUsers() {
this.reset('users');
try {
const { data } = await api.get<IResponse<IUser[]>>('api/v1/users');
this.users = data.data.map((item) => {
this.userIds.add(item.id);
return userToUserRolesDisplay(item)
}).sort((a, b) => a.id - b.id); // set default order
} catch (error) {
Notify.create({
icon: 'error',
color: 'negative',
message: 'ユーザー一覧の読み込みに失敗しました'
})
}
},
getUserById(id: number) {
if (!this.userIds.has(id)) {
return null;
}
return this.users.find((item: IUserDisplay) => item.id === id);
},
async addUser(user: IUserSubmit) {
return await api.post('api/v1/users', user);
},
async editUser(user: IUserSubmit) {
const id = user.id;
delete user['id']
return await api.put(`api/v1/users/${id}`, user);
},
async deleteUser(userId: number) {
try {
await api.delete(`api/v1/users/${userId}`)
} catch (error) {
console.error(error);
Notify.create({
icon: 'error',
color: 'negative',
message: 'ユーザーの削除に失敗しました'
});
return false;
}
return true;
},
// -------------------------- roles --------------------------
async loadRoles() {
this.reset('roles');
try {
const { data } = await api.get<IResponse<IRoles[]>>('api/v1/roles');
this.roles = data.data.map((item) => {
return roleToRoleDisplay(item)
}).sort((a, b) => a.id - b.id); // set default order
} catch (error) {
Notify.create({
icon: 'error',
color: 'negative',
message: 'ロール一覧の読み込みに失敗しました'
})
}
},
async addRole(user: IUserRolesDisplay, role: IRolesDisplay) {
return await this.updateUserRole(user, user.roleIds ? user.roleIds.concat(role.id) : [role.id]);
},
async removeRole(user: IUserRolesDisplay, role: IRolesDisplay) {
return await this.updateUserRole(user, user.roleIds ? user.roleIds.filter(e => e !== role.id) : []);
},
async updateUserRole(user: IUserRolesDisplay, roleids: number[]) {
return await api.post('api/v1/userrole', {
userid: user.id,
roleids
});
},
reset(target?: 'users'|'roles') {
if (!target) {
this.reset('users');
this.reset('roles');
return;
}
if (target == 'roles') {
this.roles = [];
} else if (target == 'users') {
this.users = [];
this.userIds.clear();
}
},
},
});
export function userToUserDisplay(user: IUser): IUserDisplay {
return {
id: user.id,
firstName: user.first_name,
lastName: user.last_name,
fullNameSearch: (user.last_name + user.first_name).toLowerCase(),
fullName: user.last_name + ' ' + user.first_name,
email: user.email,
isActive: user.is_active,
isSuperuser: user.is_superuser,
};
}
export function userToUserRolesDisplay(user: IUser): IUserRolesDisplay {
if (!user) return {} as IUserRolesDisplay;
const userRolesDisplay = userToUserDisplay(user) as IUserRolesDisplay;
const roles: IRolesDisplay[] = [];
const roleIds: number[] = [];
user.roles.sort((a, b) => a.level - b.level).forEach((role) => {
roles.push(roleToRoleDisplay(role));
roleIds.push(role.id);
});
userRolesDisplay.roles = roles;
userRolesDisplay.roleIds = roleIds;
return userRolesDisplay;
}
export function roleToRoleDisplay(roles: IRoles): IRolesDisplay {
return {
id: roles.id,
name: roles.description,
key: roles.name,
level: roles.level
};
}

View File

@@ -129,7 +129,7 @@ export class ActionNode implements IActionNode {
id: string; id: string;
name: string; name: string;
get title(): string { get title(): string {
const prop = this.actionProps.find((prop) => prop.props.name === "displayName"); const prop = this.actionProps.find((prop) => prop.props.name === 'displayName');
return prop?.props.modelValue; return prop?.props.modelValue;
}; };
get subTitle(): string { get subTitle(): string {
@@ -138,7 +138,7 @@ export class ActionNode implements IActionNode {
//変数名 //変数名
get varName():IProp|undefined{ get varName():IProp|undefined{
const prop = this.actionProps.find((prop) => prop.props.name === "verName"); const prop = this.actionProps.find((prop) => prop.props.name === 'verName');
return prop?.props; return prop?.props;
} }

View File

@@ -1,10 +1,56 @@
import { IUser } from './UserTypes'; import { IUser, IUserDisplay } from './UserTypes';
export interface IManagedApp { export interface IManagedApp {
appid: string; appid: string;
appname: string; appname: string;
domainurl: string; domainurl: string;
version: string; version: number;
updateuser: IUser; versionname?: string;
update_time: string; user: IUser;
} updateuser: IUser;
create_time: string;
update_time: string;
is_saved: boolean;
}
export interface IAppDisplay{
id:string;
sortId: number;
name:string;
url:string;
updateUser: IUserDisplay;
updateTime:string;
version:number;
versionName?: string;
versionChanged: boolean;
}
export interface IVersionSubmit {
appId: string;
name?: string;
comment?: string;
}
export interface IAppVersion {
id: number;
version: number;
appid: string;
versionname: string
comment: string;
updateuser: IUser;
update_time: string;
createuser: IUser;
create_time: string;
}
export interface IAppVersionDisplay {
id: number;
version: number;
appid: string;
name: string
comment: string;
updater: IUserDisplay;
updateTime: string;
creator: IUserDisplay;
createTime: string;
}

View File

@@ -0,0 +1,12 @@
export interface IResponse<T = any> {
code: number;
data: T;
msg: string;
}
export interface IResponsePage<T = any> extends IResponse<T> {
page: number;
size: number;
total: number;
total_pages: number;
}

Some files were not shown because too many files have changed in this diff Show More