Compare commits

..

1 Commits

Author SHA1 Message Date
xue jiahao
2b804d7608 Add save version dialog 2024-11-22 15:10:43 +08:00
117 changed files with 1576 additions and 15607 deletions

1
.gitignore vendored
View File

@@ -5,4 +5,3 @@ backend/pyvenv.cfg
backend/Include/
backend/Scripts/
log/api.log

View File

@@ -1,2 +0,0 @@
[config]
SCM_DO_BUILD_DURING_DEPLOYMENT=true

File diff suppressed because one or more lines are too long

View File

@@ -1,58 +1,40 @@
from fastapi import Request
from fastapi.responses import JSONResponse
from fastapi.security import OAuth2PasswordRequestForm
from fastapi import APIRouter, Depends, HTTPException, status
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 import security,tenantCacheService
from app.core.dbmanager import get_db
from sqlalchemy.orm import Session
auth_router = r = APIRouter()
@r.post("/token")
async def login(request: Request,db:Session= Depends(get_db) ,form_data: OAuth2PasswordRequestForm = Depends()):
if not db :
async def login(
db=Depends(get_db), form_data: OAuth2PasswordRequestForm = Depends()
):
user = authenticate_user(db, form_data.username, form_data.password)
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password",
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(
minutes=security.ACCESS_TOKEN_EXPIRE_MINUTES
)
if user.is_superuser:
roles = "super"
permissions = "ALL"
permissions = "admin"
else:
roles = ";".join(role.name for role in user.roles)
perlst = [perm.privilege for role in user.roles for perm in role.permissions]
permissions =";".join(list(set(perlst)))
permissions = "user"
access_token = security.create_access_token(
data={"sub": user.id,"roles":roles,"permissions": permissions,"tenant":user.tenantid,},
data={"sub": user.id, "permissions": permissions},
expires_delta=access_token_expires,
)
request.state.user = user.id
return JSONResponse(
status_code=200,
content={"access_token": access_token, "token_type": "bearer","user_name":user.first_name + " " + user.last_name}
)
return {"access_token": access_token, "token_type": "bearer","user_name":user.first_name + " " + user.last_name}
@r.post("/signup")
async def signup(

View File

@@ -3,32 +3,30 @@ from io import BytesIO
import typing as t
import pandas as pd
import json
import base64
import httpx
import deepdiff
import app.core.config as config
import os
from pathlib import Path
from app.core.dbmanager import get_db
from app.db.crud import get_flows_by_app,get_kintoneformat
from app.db.session import SessionLocal
from app.db.crud import get_flows_by_app,get_activedomain,get_kintoneformat
from app.core.auth import get_current_active_user,get_current_user
from app.core.apiexception import APIException
from app.db.cruddb import domainService,appService
kinton_router = r = APIRouter()
def getkintoneenv(user = Depends(get_current_user),db = Depends(get_db)):
#db = SessionLocal()
domain = domainService.get_default_domain(db,user.id) #get_activedomain(db, user.id)
#db.close()
def getkintoneenv(user = Depends(get_current_user)):
db = SessionLocal()
domain = get_activedomain(db, user.id)
db.close()
kintoneevn = config.KINTONE_ENV(domain)
return kintoneevn
def getkintoneformat(db,user = Depends(get_current_user)):
#db = SessionLocal()
def getkintoneformat():
db = SessionLocal()
formats = get_kintoneformat(db)
#db.close()
db.close()
return formats
@@ -453,10 +451,10 @@ def getTempPath(filename):
fpath = os.path.join(rootdir,"Temp",filename)
return fpath
def createappjs(domain_url,app,db):
#db = SessionLocal()
flows = appService.get_flow(db,domain_url,app) #get_flows_by_app(db,domain_url,app)
#db.close()
def createappjs(domain_url,app):
db = SessionLocal()
flows = get_flows_by_app(db,domain_url,app)
db.close()
content={}
for flow in flows:
content[flow.eventid] = {'flowid':flow.flowid,'name':flow.name,'content':flow.content}
@@ -494,17 +492,6 @@ async def test(file:UploadFile= File(...),app:str=None):
return test
@r.get("/group")
async def group(request:Request,kintoneurl:str,kintoneuser:str,kintonepwd:str):
try:
auth_value = base64.b64encode(bytes(f"{kintoneuser}:{kintonepwd}","utf-8"))
headers={config.API_V1_AUTH_KEY:auth_value}
url = f"{kintoneurl}/v1/user/groups.json?code={kintoneuser}"
r = httpx.get(url,headers=headers)
return r.json()
except Exception as e:
raise APIException('kintone:group',request.url._url, f"Error occurred while get group(url:{kintoneurl} user:{kintoneuser}):",e)
@r.post("/download",)
async def download(request:Request,key,c:config.KINTONE_ENV=Depends(getkintoneenv)):
try:
@@ -514,7 +501,7 @@ async def download(request:Request,key,c:config.KINTONE_ENV=Depends(getkintoneen
r = httpx.get(url,headers=headers,params=params)
return r.json()
except Exception as e:
raise APIException('kintone:download',request.url._url,f"Error occurred while download file.json:",e)
raise APIException('kintone:upload',request.url._url,f"Error occurred while download file.json:",e)
@r.post("/upload")
async def upload(request:Request,files:t.List[UploadFile] = File(...)):
@@ -637,21 +624,11 @@ async def createapp(request:Request,name:str,env:config.KINTONE_ENV=Depends(getk
except Exception as e:
raise APIException('kintone:createapp',request.url._url, f"Error occurred while create app({env.DOMAIN_NAME}->{name}):",e)
@r.get("/defaultgroup")
async def currentgroup(request:Request,env:config.KINTONE_ENV=Depends(getkintoneenv)):
try:
auth_value = env.API_V1_AUTH_VALUE
headers={config.API_V1_AUTH_KEY:auth_value}
url = f"{env.BASE_URL}/v1/user/groups.json?code={env.KINTONE_USER}"
r = httpx.get(url,headers=headers)
return r.json()
except Exception as e:
raise APIException('kintone:currentgroup',request.url._url, f"Error occurred while get default domain group(domain:{env.DOMAIN_NAME} url:{env.BASE_URL} user:{env.KINTONE_USER}):",e)
@r.post("/createappfromexcel",)
async def createappfromexcel(request:Request,files:t.List[UploadFile] = File(...),format:int = 0,env = Depends(getkintoneenv),db = Depends(get_db)):
async def createappfromexcel(request:Request,files:t.List[UploadFile] = File(...),format:int = 0,env = Depends(getkintoneenv)):
try:
mapping = getkintoneformat(db)[format]
mapping = getkintoneformat()[format]
except Exception as e:
raise APIException('kintone:createappfromexcel',request.url._url, f"Error occurred while get kintone format:",e)
@@ -688,9 +665,9 @@ async def createappfromexcel(request:Request,files:t.List[UploadFile] = File(...
@r.post("/updateappfromexcel")
async def updateappfromexcel(request:Request,app:str,files:t.List[UploadFile] = File(...),format:int = 0,env = Depends(getkintoneenv),db = Depends(get_db)):
async def updateappfromexcel(request:Request,app:str,files:t.List[UploadFile] = File(...),format:int = 0,env = Depends(getkintoneenv)):
try:
mapping = getkintoneformat(db)[format]
mapping = getkintoneformat()[format]
except Exception as e:
raise APIException('kintone:updateappfromexcel',request.url._url, f"Error occurred while get kintone format:",e)
@@ -780,11 +757,11 @@ async def updateprocessfromexcel(request:Request,app:str,env = Depends(getkinton
@r.post("/createjstokintone",)
async def createjstokintone(request:Request,app:str,env:config.KINTONE_ENV = Depends(getkintoneenv),db = Depends(get_db)):
async def createjstokintone(request:Request,app:str,env:config.KINTONE_ENV = Depends(getkintoneenv)):
try:
jscs=[]
files=[]
files.append(createappjs(env.BASE_URL, app, db))
files.append(createappjs(env.BASE_URL, app))
files.append(getTempPath('alc_runtime.js'))
files.append(getTempPath('alc_runtime.css'))
for file in files:

View File

@@ -2,52 +2,34 @@ from http import HTTPStatus
from fastapi import Query, Request,Depends, APIRouter, UploadFile,HTTPException,File
from fastapi.responses import JSONResponse
# from app.core.operation import log_operation
# from app.db import Base,engine
from app.core.dbmanager import get_db
from app.db import Base,engine
from app.db.session import get_db
from app.db.crud import *
from app.db.schemas import *
from typing import List, Optional
from app.core.auth import get_current_active_user,get_current_user
from app.core.apiexception import APIException
from app.core.common import ApiReturnModel,ApiReturnPage
#from fastapi_pagination import Page
from app.db.cruddb import domainService,appService
import httpx
import app.core.config as config
from app.core import domainCacheService,tenantCacheService
platform_router = r = APIRouter()
@r.get(
"/test",
response_model_exclude_none=True,
)
async def test(
request: Request,
user = Depends(get_current_active_user),
db=Depends(get_db),
):
domainService.select(db,{"tenantid":1,"name":["b","c"]})
@r.get(
"/apps",tags=["App"],
response_model=ApiReturnModel[List[AppList]|None],
"/apps",
response_model=List[AppList],
response_model_exclude_none=True,
)
async def apps_list(
request: Request,
user = Depends(get_current_active_user),
user = Depends(get_current_user),
db=Depends(get_db),
):
try:
domain = domainService.get_default_domain(db,user.id) #get_activedomain(db, user.id)
if not domain:
return ApiReturnModel(data = None)
filtered_apps = []
platformapps = appService.get_apps(db,domain.url)
domain = get_activedomain(db, user.id)
platformapps = get_apps(db,domain.url)
kintoneevn = config.KINTONE_ENV(domain)
headers={config.API_V1_AUTH_KEY:kintoneevn.API_V1_AUTH_VALUE}
url = f"{kintoneevn.BASE_URL}{config.API_V1_STR}/apps.json"
@@ -65,88 +47,28 @@ async def apps_list(
offset += limit
kintone_apps_dict = {app['appId']: app for app in all_apps}
filtered_apps = []
for papp in platformapps:
if papp.appid in kintone_apps_dict:
papp.appname = kintone_apps_dict[papp.appid]["name"]
filtered_apps.append(papp)
return ApiReturnModel(data = filtered_apps)
return filtered_apps
except Exception as e:
raise APIException('platform:apps',request.url._url,f"Error occurred while get apps:",e)
@r.post("/apps", tags=["App"],
response_model=ApiReturnModel[AppList|None],
response_model_exclude_none=True)
@r.post("/apps", response_model=AppList, response_model_exclude_none=True)
async def apps_update(
request: Request,
app: VersionUpdate,
user=Depends(get_current_active_user),
app: AppVersion,
user=Depends(get_current_user),
db=Depends(get_db),
):
try:
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))
return update_appversion(db, app,user.id)
except Exception as e:
raise APIException('platform:apps',request.url._url,f"Error occurred while get create app :",e)
@r.delete("/apps/{appid}",tags=["App"],
response_model=ApiReturnModel[AppList|None],
response_model_exclude_none=True
)
async def apps_delete(
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 ApiReturnModel(data = None)
return ApiReturnModel(data =appService.delete_app(db, domainurl,appid))
except Exception as 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(
"/appsettings/{id}",
response_model=App,
@@ -237,177 +159,131 @@ async def action_data(
raise APIException('platform:actions',request.url._url,f"Error occurred while get actions:",e)
@r.get(
"/flow/{appid}",tags=["App"],
response_model=ApiReturnModel[List[Flow]|None],
"/flow/{flowid}",
response_model=Flow,
response_model_exclude_none=True,
)
async def flow_details(
request: Request,
appid: str,
user=Depends(get_current_active_user),
flowid: str,
db=Depends(get_db),
):
try:
domainurl = domainCacheService.get_default_domainurl(db,user.id)
if not domainurl:
return ApiReturnModel(data = None)
return ApiReturnModel(data = appService.get_flow(db, domainurl, appid))
app = get_flow(db, flowid)
return app
except Exception as e:
raise APIException('platform:flow',request.url._url,f"Error occurred while get flow by flowid:",e)
@r.get(
"/flows/{appid}", tags=["App"],
response_model=List[Flow|None],
"/flows/{appid}",
response_model=List[Flow],
response_model_exclude_none=True,
)
async def flow_list(
request: Request,
appid: str,
user=Depends(get_current_active_user),
user=Depends(get_current_user),
db=Depends(get_db),
):
try:
domainurl = domainCacheService.get_default_domainurl(db,user.id) #get_activedomain(db, user.id)
if not domainurl:
return []
#flows = get_flows_by_app(db, domainurl, appid)
flows = appService.get_flow(db,domainurl,appid)
domain = get_activedomain(db, user.id)
print("domain=>",domain)
flows = get_flows_by_app(db, domain.url, appid)
return flows
except Exception as e:
raise APIException('platform:flow',request.url._url,f"Error occurred while get flow by appid:",e)
@r.post("/flow", tags=["App"],
response_model=ApiReturnModel[Flow|None],
response_model_exclude_none=True)
@r.post("/flow", response_model=Flow, response_model_exclude_none=True)
async def flow_create(
request: Request,
flow: FlowIn,
user=Depends(get_current_active_user),
flow: FlowBase,
user=Depends(get_current_user),
db=Depends(get_db),
):
try:
domainurl = domainCacheService.get_default_domainurl(db,user.id) #get_activedomain(db, user.id)
if not domainurl:
return ApiReturnModel(data = None)
return ApiReturnModel(data = appService.create_flow(db, domainurl, flow,user.id))
domain = get_activedomain(db, user.id)
return create_flow(db, domain.url, flow)
except Exception as e:
raise APIException('platform:flow',request.url._url,f"Error occurred while create flow:",e)
@r.put(
"/flow", tags=["App"],
response_model=ApiReturnModel[Flow|None],
response_model_exclude_none=True
"/flow/{flowid}", response_model=Flow, response_model_exclude_none=True
)
async def flow_edit(
request: Request,
flow: FlowIn,
user=Depends(get_current_active_user),
flowid: str,
flow: FlowBase,
user=Depends(get_current_user),
db=Depends(get_db),
):
try:
domainurl = domainCacheService.get_default_domainurl(db,user.id)
if not domainurl:
return ApiReturnModel(data = None)
return ApiReturnModel(data = appService.edit_flow(db,domainurl, flow,user.id))
domain = get_activedomain(db, user.id)
return edit_flow(db,domain.url, flow)
except Exception as e:
raise APIException('platform:flow',request.url._url,f"Error occurred while edit flow:",e)
@r.delete(
"/flow/{flowid}", tags=["App"],
response_model=ApiReturnModel[Flow|None],
response_model_exclude_none=True
"/flow/{flowid}", response_model=Flow, response_model_exclude_none=True
)
async def flow_delete(
request: Request,
flowid: str,
user=Depends(get_current_active_user),
db=Depends(get_db),
):
try:
domainurl = domainCacheService.get_default_domainurl(db,user.id)
if not domainurl:
return ApiReturnModel(data = None)
return ApiReturnModel(data = appService.delete_flow(db, flowid))
return delete_flow(db, flowid)
except Exception as e:
raise APIException('platform:flow',request.url._url,f"Error occurred while delete flow:",e)
@r.get(
"/domains",tags=["Domain"],
response_model=ApiReturnPage[Domain],
"/domains/{tenantid}",
response_model=List[Domain],
response_model_exclude_none=True,
)
async def domain_list(
async def domain_details(
request: Request,
user=Depends(get_current_active_user),
tenantid:str,
db=Depends(get_db),
):
try:
if user.is_superuser:
domains = domainService.get_domains(db)
else:
domains = domainService.get_domains_by_manage(db,user.id)
return domains
domains = get_domains(db,tenantid)
return domains
except Exception as 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"],
response_model=ApiReturnModel[Domain],
response_model_exclude_none=True)
@r.post("/domain", response_model=Domain, response_model_exclude_none=True)
async def domain_create(
request: Request,
domain: DomainIn,
user=Depends(get_current_active_user),
domain: DomainBase,
user=Depends(get_current_user),
db=Depends(get_db),
):
try:
return ApiReturnModel(data = domainService.create_domain(db, domain,user.id))
return create_domain(db, domain,user.id)
except Exception as e:
raise APIException('platform:domain',request.url._url,f"Error occurred while create domain:",e)
@r.put(
"/domain", tags=["Domain"],
response_model=ApiReturnModel[Domain|None],
response_model_exclude_none=True
"/domain", response_model=Domain, response_model_exclude_none=True
)
async def domain_edit(
request: Request,
domain: DomainIn,
user=Depends(get_current_active_user),
domain: DomainBase,
db=Depends(get_db),
):
try:
domain = domainService.edit_domain(db, domain,user.id)
if domain :
domainCacheService.clear_default_domainurl()
return ApiReturnModel(data = domain)
return edit_domain(db, domain)
except Exception as e:
raise APIException('platform:domain',request.url._url,f"Error occurred while edit domain:",e)
@r.delete(
"/domain/{id}",tags=["Domain"],
response_model=ApiReturnModel[DomainOut|None],
response_model_exclude_none=True,
"/domain/{id}", response_model=Domain, response_model_exclude_none=True
)
async def domain_delete(
request: Request,
@@ -415,175 +291,94 @@ async def domain_delete(
db=Depends(get_db),
):
try:
return ApiReturnModel(data = domainService.delete_domain(db,id))
return delete_domain(db,id)
except Exception as e:
raise APIException('platform:domain',request.url._url,f"Error occurred while delete domain({id}):",e)
raise APIException('platform:domain',request.url._url,f"Error occurred while delete domain:",e)
@r.get(
"/domain",
response_model=List[Domain],
# response_model=List[Domain],
response_model_exclude_none=True,
)
async def userdomain_details(
request: Request,
userId: Optional[int] = Query(None, alias="userId"),
user=Depends(get_current_active_user),
user=Depends(get_current_user),
db=Depends(get_db),
):
try:
domains = get_domain(db, userId if userId is not None else user.id)
return domains
except Exception as e:
raise APIException('platform:userdomain',request.url._url,f"Error occurred while get user({user.id}) domain:",e)
raise APIException('platform:domain',request.url._url,f"Error occurred while get user({user.id}) domain:",e)
@r.post(
"/userdomain",tags=["Domain"],
response_model=ApiReturnModel[DomainOut|None],
"/domain/{userid}",
response_model_exclude_none=True,
)
async def create_userdomain(
request: Request,
userdomain:UserDomainParam,
user=Depends(get_current_active_user),
userid: int,
domainids:List[int] ,
db=Depends(get_db),
):
try:
userid = userdomain.userid
domainid = userdomain.domainid
if user.is_superuser:
domain = domainService.add_userdomain(db,user.id,userid,domainid)
else:
domain = domainService.add_userdomain_by_owner(db,user.id,userid,domainid)
return ApiReturnModel(data = domain)
domain = add_userdomain(db, userid,domainids)
return domain
except Exception as e:
raise APIException('platform:userdomain',request.url._url,f"Error occurred while add user({userid}) domain({domainid}):",e)
raise APIException('platform:domain',request.url._url,f"Error occurred while add user({userid}) domain:",e)
@r.delete(
"/domain/{domainid}/{userid}",tags=["Domain"],
response_model=ApiReturnModel[DomainOut|None],
response_model_exclude_none=True,
"/domain/{domainid}/{userid}", response_model_exclude_none=True
)
async def delete_userdomain(
async def userdomain_delete(
request: Request,
domainid:int,
userid: int,
user=Depends(get_current_active_user),
db=Depends(get_db),
):
try:
return ApiReturnModel(data = domainService.delete_userdomain(db,userid,domainid))
return delete_userdomain(db, userid,domainid)
except Exception as e:
raise APIException('platform:userdomain',request.url._url,f"Error occurred while delete user({userid}) domain({domainid}):",e)
raise APIException('platform:delete',request.url._url,f"Error occurred while delete user({userid}) domain:",e)
@r.get(
"/managedomainuser/{domainid}",tags=["Domain"],
response_model=ApiReturnPage[UserOut|None],
"/activedomain",
response_model=Domain,
response_model_exclude_none=True,
)
async def get_managedomainuser(
async def get_useractivedomain(
request: Request,
domainid:int,
user=Depends(get_current_active_user),
userId: Optional[int] = Query(None, alias="userId"),
user=Depends(get_current_user),
db=Depends(get_db),
):
try:
return domainService.get_managedomain_users(db,domainid)
# domain = get_activedomain(db, user.id)
domain = get_activedomain(db, userId if userId is not None else user.id)
if domain is None:
return JSONResponse(content=None,status_code=HTTPStatus.OK)
return domain
except Exception as e:
raise APIException('platform:managedomain',request.url._url,f"Error occurred while get managedomain({user.id}) user:",e)
raise APIException('platform:activedomain',request.url._url,f"Error occurred while get user({user.id}) activedomain:",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(
"/defaultdomain",tags=["Domain"],
response_model=ApiReturnModel[DomainOut|None],
response_model_exclude_none=True,
)
async def get_defaultuserdomain(
request: Request,
user=Depends(get_current_active_user),
db=Depends(get_db),
):
try:
return ApiReturnModel(data =domainService.get_default_domain(db,user.id))
except Exception as e:
raise APIException('platform:defaultdomain',request.url._url,f"Error occurred while get user({user.id}) defaultdomain:",e)
@r.put(
"/defaultdomain/{domainid}",tags=["Domain"],
response_model=ApiReturnModel[DomainOut|None],
"/activedomain/{domainid}",
response_model_exclude_none=True,
)
async def set_defualtuserdomain(
async def update_activeuserdomain(
request: Request,
domainid:int,
user=Depends(get_current_active_user),
userId: Optional[int] = Query(None, alias="userId"),
user=Depends(get_current_user),
db=Depends(get_db),
):
try:
domain = domainCacheService.set_default_domain(db,user.id,domainid)
return ApiReturnModel(data= domain)
domain = active_userdomain(db, userId if userId is not None else user.id,domainid)
return domain
except Exception as e:
raise APIException('platform:defaultdomain',request.url._url,f"Error occurred while update user({user.id}) defaultdomain:",e)
@r.get(
"/domainshareduser/{domainid}",tags=["Domain"],
response_model=ApiReturnPage[UserOut|None],
response_model_exclude_none=True,
)
async def get_domainshareduser(
request: Request,
domainid:int,
user=Depends(get_current_active_user),
db=Depends(get_db),
):
try:
return domainService.get_shareddomain_users(db,domainid)
except Exception as e:
raise APIException('platform:sharedomain',request.url._url,f"Error occurred while get user({user.id}) sharedomain:",e)
raise APIException('platform:activedomain',request.url._url,f"Error occurred while update user({user.id}) activedomain:",e)
@r.get(
"/events",

View File

@@ -1,187 +1,107 @@
from fastapi import APIRouter, Request, Depends, Response, Security, encoders
from fastapi import APIRouter, Request, Depends, Response, encoders
import typing as t
from app.core.common import ApiReturnModel,ApiReturnPage
from app.core.apiexception import APIException
from app.core.dbmanager import get_db
from app.db.session import get_db
from app.db.crud import (
get_allusers,
get_users,
get_user,
create_user,
delete_user,
edit_user,
assign_userrole,
get_roles,
)
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.db.cruddb import userService
from app.core import tenantCacheService
from app.db.schemas import UserCreate, UserEdit, User, UserOut
from app.core.auth import get_current_active_user, get_current_active_superuser
users_router = r = APIRouter()
@r.get(
"/users",tags=["User"],
response_model=ApiReturnPage[User],
"/users",
response_model=t.List[User],
response_model_exclude_none=True,
)
async def users_list(
request: Request,
response: Response,
db=Depends(get_db),
current_user=Depends(get_current_active_user),
current_user=Depends(get_current_active_superuser),
):
try:
if current_user.is_superuser:
users = userService.get_users(db)
else:
users = userService.get_users_not_admin(db)
return users
except Exception as e:
raise APIException('user:users',request.url._url,f"Error occurred while get user list",e)
"""
Get all users
"""
users = get_users(db)
# This is necessary for react-admin to work
response.headers["Content-Range"] = f"0-9/{len(users)}"
return users
@r.get("/users/me", tags=["User"],
response_model=ApiReturnModel[User],
response_model_exclude_none=True,
)
@r.get("/users/me", response_model=User, response_model_exclude_none=True)
async def user_me(current_user=Depends(get_current_active_user)):
return ApiReturnModel(data = current_user)
"""
Get own user
"""
return current_user
@r.get(
"/users/{user_id}",tags=["User"],
response_model=ApiReturnModel[User|None],
"/users/{user_id}",
response_model=User,
response_model_exclude_none=True,
)
async def user_details(
request: Request,
user_id: int,
db=Depends(get_db),
current_user=Depends(get_current_active_user),
current_user=Depends(get_current_active_superuser),
):
try:
user = userService.get(db, user_id)
if user:
if user.is_superuser and not current_user.is_superuser:
user = None
return ApiReturnModel(data = user)
except Exception as e:
raise APIException('user:users',request.url._url,f"Error occurred while get user({user_id}) detail:",e)
"""
Get any user details
"""
user = get_user(db, user_id)
return user
# return encoders.jsonable_encoder(
# user, skip_defaults=True, exclude_none=True,
# )
@r.post("/users", tags=["User"],
response_model=ApiReturnModel[User|None],
response_model_exclude_none=True,
)
@r.post("/users", response_model=User, response_model_exclude_none=True)
async def user_create(
request: Request,
user: UserCreate,
db=Depends(get_db),
current_user=Depends(get_current_active_user),
current_user=Depends(get_current_active_superuser),
):
try:
if user.is_superuser and not current_user.is_superuser:
return ApiReturnModel(data = None)
return ApiReturnModel(data =userService.create_user(db, user,current_user.id))
except Exception as e:
raise APIException('user:users',request.url._url,f"Error occurred while create user({user.email}):",e)
"""
Create a new user
"""
return create_user(db, user)
@r.put(
"/users/{user_id}", tags=["User"],
response_model=ApiReturnModel[User|None],
response_model_exclude_none=True,
"/users/{user_id}", response_model=User, response_model_exclude_none=True
)
async def user_edit(
request: Request,
user_id: int,
user: UserEdit,
db=Depends(get_db),
current_user=Depends(get_current_active_user),
current_user=Depends(get_current_active_superuser),
):
try:
if user.is_superuser and not current_user.is_superuser:
return ApiReturnModel(data = None)
return ApiReturnModel(data = userService.edit_user(db,user_id,user,current_user.id))
except Exception as e:
raise APIException('user:users',request.url._url,f"Error occurred while edit user({user_id}):",e)
"""
Update existing user
"""
return edit_user(db, user_id, user)
@r.delete(
"/users/{user_id}", tags=["User"],
response_model=ApiReturnModel[UserOut|None],
response_model_exclude_none=True
"/users/{user_id}", response_model=User, response_model_exclude_none=True
)
async def user_delete(
request: Request,
user_id: int,
db=Depends(get_db),
current_user=Depends(get_current_active_user),
current_user=Depends(get_current_active_superuser),
):
try:
user = userService.get(db,user_id)
if user.is_superuser and not current_user.is_superuser:
return ApiReturnModel(data = None)
return ApiReturnModel(data = userService.delete_user(db, user_id))
except Exception as e:
raise APIException('user:users',request.url._url,f"Error occurred while delete user({user_id}):",e)
@r.post("/userrole",tags=["User"],
response_model=ApiReturnModel[User],
response_model_exclude_none=True,)
async def assign_role(
request: Request,
userroles:AssignUserRoles,
db=Depends(get_db)
):
try:
return ApiReturnModel(data = userService.assign_userrole(db,userroles.userid,userroles.roleids))
except Exception as e:
raise APIException('user:userrole',request.url._url,f"Error occurred while assign user({userroles.userid}) roles({userroles.roleids}):",e)
@r.get(
"/roles",tags=["User"],
response_model=ApiReturnModel[t.List[RoleBase]|None],
response_model_exclude_none=True,
)
async def roles_list(
request: Request,
db=Depends(get_db),
current_user=Depends(get_current_active_user),
#current_user=Security(get_current_active_user, scopes=["role_list"]),
):
try:
if current_user.is_superuser:
roles = userService.get_roles(db)
else:
if len(current_user.roles)>0:
roles = userService.get_roles_by_level(db,current_user.roles)
else:
roles = []
return ApiReturnModel(data = roles)
except Exception as e:
raise APIException('user:roles',request.url._url,f"Error occurred while get roles:",e)
@r.get(
"/userpermssions",tags=["User"],
response_model=ApiReturnModel[t.List[Permission]|None],
response_model_exclude_none=True,
)
async def permssions_list(
request: Request,
db=Depends(get_db),
current_user=Depends(get_current_active_user),
#current_user=Security(get_current_active_user, scopes=["role_list"]),
):
try:
if current_user.is_superuser:
permissions = userService.get_permissions(db)
else:
if len(current_user.roles)>0:
permissions = userService.get_user_permissions(db,current_user.id)
else:
permissions = []
return ApiReturnModel(data = permissions)
except Exception as e:
raise APIException('user:userpermssions',request.url._url,f"Error occurred while get user({current_user.id}) permissions:",e)
"""
Delete existing user
"""
return delete_user(db, user_id)

View File

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

View File

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

View File

@@ -1,16 +1,14 @@
from fastapi.security import SecurityScopes
import jwt
from fastapi import Depends, HTTPException, Request, Security, status
from fastapi import Depends, HTTPException, status
from jwt import PyJWTError
from app.db import models, schemas
from app.db import models, schemas, session
from app.db.crud import get_user_by_email, create_user,get_user
from app.core import security
from app.db.cruddb import userService
from app.core.dbmanager import get_db
async def get_current_user(request: Request,security_scopes: SecurityScopes,
db=Depends(get_db), token: str = Depends(security.oauth2_scheme)
async def get_current_user(
db=Depends(session.get_db), token: str = Depends(security.oauth2_scheme)
):
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
@@ -24,25 +22,13 @@ async def get_current_user(request: Request,security_scopes: SecurityScopes,
id: int = payload.get("sub")
if id is None:
raise credentials_exception
tenant:str = payload.get("tenant")
if tenant is None:
raise credentials_exception
permissions: str = payload.get("permissions")
if not permissions =="ALL":
for scope in security_scopes.scopes:
if scope not in permissions.split(";"):
raise HTTPException(
status_code=403, detail="The user doesn't have enough privileges"
)
token_data = schemas.TokenData(id = id, permissions=permissions)
except PyJWTError:
raise credentials_exception
user = userService.get_user(db, token_data.id)
user = get_user(db, token_data.id)
if user is None:
raise credentials_exception
request.state.user = user.id
return user
async def get_current_active_user(
@@ -64,11 +50,11 @@ async def get_current_active_superuser(
def authenticate_user(db, email: str, password: str):
user = userService.get_user_by_email(db,email) #get_user_by_email(db, email)
user = get_user_by_email(db, email)
if not user:
return None
return False
if not security.verify_password(password, user.hashed_password):
return None
return False
return user

View File

@@ -1,72 +0,0 @@
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

@@ -1,49 +0,0 @@
import math
from fastapi import Query
from fastapi_pagination.bases import AbstractPage,AbstractParams,RawParams
from pydantic import BaseModel
from typing import Any, Generic, List, Type,TypeVar,Generic,Sequence
from fastapi_pagination import Page,utils
T = TypeVar('T')
class ApiReturnModel(BaseModel,Generic[T]):
code:int = 0
msg:str ="OK"
data:T
class ApiReturnError(BaseModel):
code:int = -1
msg:str =""
class Params(BaseModel, AbstractParams):
page:int = Query(1,get=1, description="Page number")
size:int = Query(20,get=0, le=100,description="Page size")
def to_raw_params(self) -> RawParams:
return RawParams(
limit=self.size,
offset=self.size*(self.page-1)
)
class ApiReturnPage(AbstractPage[T],Generic[T]):
code:int =0
msg:str ="OK"
data:Sequence[T]
total:int
page:int
size:int
# next:str
# previous:str
total_pages:int
__params_type__ =Params
@classmethod
def create(cls,items:Sequence[T],params:Params,**kwargs: Any) -> Type[Page[T]]:
total = kwargs.get('total', 0)
total_pages = math.ceil(total/params.size)
return utils.create_pydantic_model(cls,data=items,total=total,page=params.page,size=params.size,total_pages=total_pages)

View File

@@ -1,20 +0,0 @@
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

@@ -1,131 +0,0 @@
from urllib.parse import parse_qs, urlencode
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"):
content_type = request.headers.get('content-type', '')
if content_type.startswith('multipart/form-data'):
request.state.body = None
else:
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
def sanitize_password(self,data):
"""
データ内の password パラメータをフィルタリングする機能。
dict、JSON 文字列、URL エンコード文字列、QueryDict をサポート。
"""
if data is None:
return data
elif isinstance(data, dict):
data.pop('password', None)
return data
elif isinstance(data, list):
return [self.sanitize_password(item) for item in data]
elif isinstance(data, (str, bytes)):
if isinstance(data, bytes):
data = data.decode('utf-8') # bytes to str
# JSON解析
try:
parsed_json = json.loads(data)
sanitized_json = self.sanitize_password(parsed_json)
return json.dumps(sanitized_json, separators=(',', ':'))
except json.JSONDecodeError:
# URL 解析
try:
parsed_dict = parse_qs(data)
parsed_dict.pop('password', None)
return urlencode(parsed_dict, doseq=True)
except:
parts = data.split('&')
filtered_parts = []
for part in parts:
if '=' in part:
key, _ = part.split('=', 1)
if key == 'password':
continue
filtered_parts.append(part)
return '&'.join(filtered_parts)
# QueryDict 例えば FastAPI の request.query_params
elif hasattr(data, 'items'):
return {k: v for k, v in data.items() if k != 'password'}
return data
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
# passwordのパラメータを除外する
safe_query = self.sanitize_password(request.query_params.items())
# passwordのパラメータを除外する
safe_body = self.sanitize_password(request.state.body)
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": safe_query,
"body": safe_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
from fastapi.security import OAuth2PasswordBearer
from passlib.context import CryptContext
from datetime import datetime, timedelta,timezone
from datetime import datetime, timedelta
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms
import os
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):
to_encode = data.copy()
if expires_delta:
expire = datetime.now(timezone.utc) + expires_delta
expire = datetime.utcnow() + expires_delta
else:
expire = datetime.now(timezone.utc) + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
to_encode.update({"exp": expire})
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
return encoded_jwt

View File

@@ -1,4 +1,3 @@
from datetime import datetime
from fastapi import HTTPException, status
from sqlalchemy.orm import Session
from sqlalchemy import and_
@@ -19,15 +18,10 @@ def get_user_by_email(db: Session, email: str) -> schemas.UserBase:
return db.query(models.User).filter(models.User.email == email).first()
def get_allusers(
db: Session
) -> t.List[schemas.UserOut]:
return db.query(models.User).all()
def get_users(
db: Session
db: Session, skip: int = 0, limit: int = 100
) -> t.List[schemas.UserOut]:
return db.query(models.User).filter(models.User.is_superuser == False)
return db.query(models.User).offset(skip).limit(limit).all()
def create_user(db: Session, user: schemas.UserCreate):
@@ -75,48 +69,28 @@ def edit_user(
db.refresh(db_user)
return db_user
def get_roles(
db: Session
) -> t.List[schemas.RoleBase]:
return db.query(models.Role).all()
def assign_userrole( db: Session, user_id: int, roles: t.List[int]):
db_user = db.query(models.User).get(user_id)
if db_user:
for role in db_user.roles:
db_user.roles.remove(role)
for roleid in roles:
role = db.query(models.Role).get(roleid)
if role:
db_user.roles.append(role)
db.commit()
db.refresh(db_user)
return db_user
def get_apps(
db: Session,
domainurl:str
domain_url:str
) -> t.List[schemas.AppList]:
return db.query(models.App).filter(models.App.domainurl == domainurl).all()
return db.query(models.App).filter(models.App.domainurl == domain_url).all()
def update_appversion(db: Session, appedit: schemas.AppVersion,userid:int):
db_app = db.query(models.App).filter(and_(models.App.domainurl == appedit.domainurl,models.App.appid == appedit.appid)).first()
if not db_app:
raise HTTPException(status.HTTP_404_NOT_FOUND, detail="User not found")
db_app.version = db_app.version + 1
appversion = models.AppVersion(
app = db.query(models.App).filter(and_(models.App.domainurl == appedit.domainurl,models.App.appid == appedit.appid)).first()
if app:
app.version = app.version + 1
db_app = app
appver = app.version
else:
appver = 1
db_app = models.App(
domainurl = appedit.domainurl,
appid=appedit.appid,
appname=db_app.appname,
version = db_app.version,
versionname = appedit.versionname,
comment = appedit.comment,
updateuserid = userid,
createuserid = userid
)
db.add(appversion)
appname=appedit.appname,
version = 1,
updateuser= userid
)
db.add(db_app)
flows = db.query(models.Flow).filter(and_(models.Flow.domainurl == appedit.domainurl,models.App.appid == appedit.appid))
@@ -129,9 +103,7 @@ def update_appversion(db: Session, appedit: schemas.AppVersion,userid:int):
name = flow.name,
content = flow.content,
createuser = userid,
version = db_app.version,
updateuserid = userid,
createuserid = userid
version = appver
)
db.add(db_flowhistory)
@@ -139,17 +111,6 @@ def update_appversion(db: Session, appedit: schemas.AppVersion,userid:int):
db.refresh(db_app)
return db_app
def delete_apps(db: Session, domainurl: str,appid: str ):
db_app = db.query(models.App).filter(and_(models.App.domainurl == domainurl,models.App.appid ==appid)).first()
if not db_app:
raise HTTPException(status.HTTP_404_NOT_FOUND, detail="App not found")
db.delete(db_app)
db_flows = db.query(models.Flow).filter(and_(models.Flow.domainurl == domainurl,models.Flow.appid ==appid))
for flow in db_flows:
db.delete(flow)
db.commit()
return db_app
def get_appsetting(db: Session, id: int):
app = db.query(models.AppSetting).get(id)
if not app:
@@ -205,28 +166,16 @@ def get_actions(db: Session):
return actions
def create_flow(db: Session, domainurl: str, flow: schemas.FlowIn,userid:int):
def create_flow(db: Session, domainurl: str, flow: schemas.FlowBase):
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
content=flow.content
)
db.add(db_flow)
db_app = db.query(models.App).filter(and_(models.App.domainurl == domainurl,models.App.appid == flow.appid)).first()
if not db_app:
db_app = models.App(
domainurl = domainurl,
appid=flow.appid,
appname=flow.appname,
version = 0,
createuserid= userid,
updateuserid = userid
)
db.commit()
db.refresh(db_flow)
return db_flow
@@ -241,20 +190,18 @@ def delete_flow(db: Session, flowid: str):
def edit_flow(
db: Session, domainurl: str, flow: schemas.FlowIn,userid:int
db: Session, domainurl: str, flow: schemas.FlowBase
) -> schemas.Flow:
db_flow = get_flow(db, flow.flowid)
if not db_flow:
#見つからない時新規作成
return 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
return create_flow(db,domainurl,flow)
update_data = flow.dict(exclude_unset=True)
for key, value in update_data.items():
setattr(db_flow, key, value)
db.add(db_flow)
db.commit()
db.refresh(db_flow)
@@ -279,78 +226,54 @@ def get_flows_by_app(db: Session,domainurl: str, appid: str):
raise Exception("Data not found")
return flows
def create_domain(db: Session, domain: schemas.DomainIn,userid:int):
def create_domain(db: Session, domain: schemas.DomainBase,userid:int):
domain.encrypt_kintonepwd()
db_domain = models.Domain(
tenantid = domain.tenantid,
name=domain.name,
url=domain.url,
is_active=domain.is_active,
kintoneuser=domain.kintoneuser,
kintonepwd=domain.kintonepwd,
createuserid = userid,
updateuserid = userid,
ownerid = domain.ownerid
kintonepwd=domain.kintonepwd
)
db.add(db_domain)
#add_userdomain(db,userid,db_domain.id)
db.flush()
add_userdomain(db,userid,db_domain.id)
db.commit()
db.refresh(db_domain)
return db_domain
def delete_domain(db: Session,id: int):
db_domain = db.query(models.Domain).get(id)
#if not db_domain:
# raise HTTPException(status.HTTP_404_NOT_FOUND, detail="Domain not found")
if db_domain:
db.delete(db_domain)
db.commit()
return True
if not db_domain:
raise HTTPException(status.HTTP_404_NOT_FOUND, detail="Domain not found")
db.delete(db_domain)
db.commit()
return db_domain
def edit_domain(
db: Session, domain: schemas.DomainIn,userid:int
db: Session, domain: schemas.DomainBase
) -> schemas.Domain:
if domain.kintonepwd != "":
domain.encrypt_kintonepwd()
domain.encrypt_kintonepwd()
db_domain = db.query(models.Domain).get(domain.id)
if not db_domain:
raise HTTPException(status.HTTP_404_NOT_FOUND, detail="Domain not found")
db_domain.tenantid = domain.tenantid
db_domain.name=domain.name
db_domain.url=domain.url
if db_domain.is_active == True and domain.is_active == False:
db_userdomains = db.query(models.UserDomain).filter(and_(models.UserDomain.domainid == db_domain.id,models.UserDomain.active == True)).all()
for userdomain in db_userdomains:
userdomain.active = False
db.add(userdomain)
db_domain.is_active=domain.is_active
db_domain.kintoneuser=domain.kintoneuser
if domain.kintonepwd != "":
db_domain.kintonepwd = domain.kintonepwd
db_domain.updateuserid = userid
db_domain.ownerid = domain.ownerid
raise HTTPException(status.HTTP_404_NOT_FOUND, detail="Domain not found")
update_data = domain.dict(exclude_unset=True)
for key, value in update_data.items():
if key != "id" and not (key == "kintonepwd" and (value is None or value == "")):
setattr(db_domain, key, value)
print(str(db_domain))
db.add(db_domain)
db.commit()
db.refresh(db_domain)
return db_domain
def add_admindomain(db: Session,userid:int,domainid:int):
db_domain = db.query(models.Domain).filter(and_(models.Domain.id == domainid,models.Domain.is_active)).first()
if db_domain:
user_domain = models.UserDomain(userid = userid, domainid = domainid )
db.add(user_domain)
db.commit()
return db_domain
def add_userdomain(db: Session,ownerid:int, userid:int,domainid:int):
db_domain = db.query(models.Domain).filter(and_(models.Domain.id == domainid,models.Domain.ownerid == ownerid,models.Domain.is_active)).first()
if db_domain:
user_domain = models.UserDomain(userid = userid, domainid = domainid )
db.add(user_domain)
db.commit()
return db_domain
def add_userdomain(db: Session, userid:int,domainid:int):
user_domain = models.UserDomain(userid = userid, domainid = domainid )
db.add(user_domain)
return user_domain
def add_userdomains(db: Session, userid:int,domainids:list[str]):
dbCommits = list(map(lambda domainid: models.UserDomain(userid = userid, domainid = domainid ), domainids))
@@ -360,40 +283,35 @@ def add_userdomains(db: Session, userid:int,domainids:list[str]):
def delete_userdomain(db: Session, userid: int,domainid: int):
db_domain = db.query(models.UserDomain).filter(and_(models.UserDomain.userid == userid,models.UserDomain.domainid == domainid)).first()
#if not db_domain:
# raise HTTPException(status.HTTP_404_NOT_FOUND, detail="Domain not found")
if db_domain:
db.delete(db_domain)
db.commit()
return True
def active_userdomain(db: Session, userid: int,domainid: int):
db_domain = db.query(models.Domain).filter(and_(models.Domain.id == domainid,models.Domain.is_active)).first()
if db_domain:
db_userdomains = db.query(models.UserDomain).filter(models.UserDomain.userid == userid).all()
# if not db_userdomains:
# raise HTTPException(status.HTTP_404_NOT_FOUND, detail="Domain not found")
for domain in db_userdomains:
if domain.domainid == domainid:
domain.active = True
else:
domain.active = False
db.add(domain)
db.commit()
if not db_domain:
raise HTTPException(status.HTTP_404_NOT_FOUND, detail="Domain not found")
db.delete(db_domain)
db.commit()
return db_domain
def get_activedomain(db: Session, userid: int):
# user_domains = (db.query(models.Domain,models.UserDomain.active)
# .join(models.UserDomain,models.UserDomain.domainid == models.Domain.id )
# .filter(models.UserDomain.userid == userid)
# .all())
db_domain=(db.query(models.Domain).filter(models.Domain.is_active)
.join(models.UserDomain,models.UserDomain.domainid == models.Domain.id).filter(and_(models.UserDomain.active,models.UserDomain.userid == userid)).first())
# if len(user_domains)==1:
# db_domain = user_domains[0][0];
# else:
# db_domain = next((domain for domain,active in user_domains if active),None)
def active_userdomain(db: Session, userid: int,domainid: int):
db_userdomains = db.query(models.UserDomain).filter(models.UserDomain.userid == userid).all()
if not db_userdomains:
raise HTTPException(status.HTTP_404_NOT_FOUND, detail="Domain not found")
for domain in db_userdomains:
if domain.domainid == domainid:
domain.active = True
else:
domain.active = False
db.add(domain)
db.commit()
return db_userdomains
def get_activedomain(db: Session, userid: int)-> t.Optional[models.Domain]:
user_domains = (db.query(models.Domain,models.UserDomain.active)
.join(models.UserDomain,models.UserDomain.domainid == models.Domain.id )
.filter(models.UserDomain.userid == userid)
.all())
db_domain=None
if len(user_domains)==1:
db_domain = user_domains[0][0];
else:
db_domain = next((domain for domain,active in user_domains if active),None)
# raise HTTPException(status.HTTP_404_NOT_FOUND, detail="Domain not found")
return db_domain
@@ -406,12 +324,13 @@ def get_domain(db: Session, userid: str):
# domain.kintonepwd = decrypted_pwd
return domains
def get_alldomains(db: Session):
domains = db.query(models.Domain).all()
return domains
def get_domains(db: Session,userid:int):
domains = db.query(models.Domain).filter(models.Domain.ownerid == userid ).all()
def get_domains(db: Session,tenantid:str):
domains = db.query(models.Domain).filter(models.Domain.tenantid == tenantid ).all()
if not domains:
raise HTTPException(status_code=404, detail="Data not found")
# for domain in domains:
# decrypted_pwd = chacha20Decrypt(domain.kintonepwd)
# domain.kintonepwd = decrypted_pwd
return domains
def get_events(db: Session):

View File

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

View File

@@ -1,104 +0,0 @@
from sqlalchemy import asc, desc, select
from sqlalchemy.orm import Session
from sqlalchemy.orm.query import Query
from typing import Type, List, Optional
from app.core.common import ApiReturnPage
from sqlalchemy import and_ ,or_
from pydantic import BaseModel
from app.db import models
class crudbase:
def __init__(self, model: Type[models.Base]):
self.model = model
def _apply_filters(self, query: Query, filters: dict) -> Query:
and_conditions = []
or_conditions = []
for column_name, value in filters.items():
column = getattr(self.model, column_name, None)
if column:
if isinstance(value, dict):
if 'operator' in value:
operator = value['operator']
filter_value = value['value']
if operator == '!=':
and_conditions.append(column != filter_value)
elif operator == 'like':
and_conditions.append(column.like(f"%{filter_value}%"))
elif operator == '=':
and_conditions.append(column == filter_value)
elif operator == '>':
and_conditions.append(column > filter_value)
elif operator == '>=':
and_conditions.append(column >= filter_value)
elif operator == '<':
and_conditions.append(column < filter_value)
elif operator == '<=':
and_conditions.append(column <= filter_value)
elif operator == 'in':
if isinstance(filter_value, list):
or_conditions.append(column.in_(filter_value))
else:
and_conditions.append(column == filter_value)
else:
and_conditions.append(column == value)
else:
and_conditions.append(column == value)
if and_conditions:
query = query.where(and_(*and_conditions))
if or_conditions:
query = query.where(or_(*or_conditions))
return query
def _apply_sorting(self, query: Query, sort_by: Optional[str], sort_order: Optional[str]) -> Query:
if sort_by:
column = getattr(self.model, sort_by, None)
if column:
if sort_order == "desc":
query = query.order_by(desc(column))
else:
query = query.order_by(asc(column))
return query
def get_all(self) -> Query:
return select(self.model)
def get(self, db: Session, item_id: int) -> Optional[models.Base]:
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:
db_obj = self.model(**obj_in.model_dump())
db.add(db_obj)
db.commit()
db.refresh(db_obj)
return db_obj
def update(self, db: Session, item_id: int, obj_in: BaseModel) -> Optional[models.Base]:
db_obj = self.get(db,item_id)
if db_obj:
for key, value in obj_in.model_dump(exclude_unset=True).items():
setattr(db_obj, key, value)
db.commit()
db.refresh(db_obj)
return db_obj
return None
def delete(self, db: Session, item_id: int) -> Optional[models.Base]:
db_obj = self.get(db,item_id)
if db_obj:
db.delete(db_obj)
db.commit()
return db_obj
return None
def get_by_conditions(self, filters: Optional[dict] = None, sort_by: Optional[str] = None,
sort_order: Optional[str] = "asc") -> Query:
query = select(self.model)
if filters:
query = self._apply_filters(query, filters)
if sort_by:
query = self._apply_sorting(query, sort_by, sort_order)
print(str(query))
return query

View File

@@ -1,219 +0,0 @@
from datetime import datetime
from fastapi import HTTPException, status
from sqlalchemy.orm import Session
from sqlalchemy.orm.attributes import flag_modified
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
flag_modified(db_app, 'is_saved')
db_app.updateuserid = userid
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,218 +0,0 @@
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 dbuserdomain(crudbase):
def __init__(self):
super().__init__(model=models.UserDomain)
def get_userdomain(self,db: Session,userid:int,domainid:int):
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):
return super().get_by_conditions({"domainid":domainid})
def get_default_domains(self,db: Session,domainid:int):
return db.execute(super().get_by_conditions({"domainid":domainid,"is_default":True})).scalars().all()
def get_user_default_domain(self,db: Session,userid:int):
return db.execute(super().get_by_conditions({"userid":userid,"is_default":True})).scalars().first()
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):
def __init__(self):
super().__init__(model=models.Domain)
def get_domains(self,db: Session)-> ApiReturnPage[models.Base]:
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]:
return paginate(db,super().get_by_conditions({"ownerid":ownerid}))
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()
#if not db_domain:
domain.encrypt_kintonepwd()
db_domain = models.Domain(
tenantid = domain.tenantid,
name = domain.name,
url = domain.url,
kintoneuser = domain.kintoneuser,
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):
db_managedomains = dbmanagedomain.get_managedomain_by_domain(db,id)
for manage in db_managedomains:
db.delete(manage)
return super().delete(db,id)
def edit_domain(self,db: Session, domain: schemas.DomainIn,userid:int) -> schemas.DomainOut:
db_domain = super().get(db,domain.id)
if db_domain:
db_domain.tenantid = domain.tenantid
db_domain.name=domain.name
db_domain.url=domain.url
if db_domain.is_active == True and domain.is_active == False:
db_userdomains = dbuserdomain.get_default_domains(db,domain.id)
for userdomain in db_userdomains:
userdomain.is_default = False
db.add(userdomain)
db_domain.is_active=domain.is_active
db_domain.kintoneuser=domain.kintoneuser
if domain.kintonepwd != "" and domain.kintonepwd != None:
domain.encrypt_kintonepwd()
db_domain.kintonepwd = domain.kintonepwd
db_domain.updateuserid = userid
db.add(db_domain)
db.commit()
db.refresh(db_domain)
return db_domain
return None
def add_userdomain(self,db: Session,ownerid:int,userid:int,domainid:int) -> schemas.DomainOut:
db_domain = super().get(db,domainid)
if db_domain and db_domain.is_active:
db_userdomain = dbuserdomain.get_userdomain(db,userid,domainid)
if not db_userdomain:
user_domain = models.UserDomain(userid = userid, domainid = domainid ,createuserid = ownerid,updateuserid = ownerid)
db.add(user_domain)
db.commit()
return db_domain
return None
def add_userdomain_by_owner(self,db: Session,ownerid:int, userid:int,domainid:int) -> schemas.DomainOut:
db_domain = db.execute(super().get_by_conditions({"id":domainid,"is_active":True})).scalars().first()
if db_domain:
db_userdomain = dbuserdomain.get_userdomain(db,userid,domainid)
if not db_userdomain:
user_domain = models.UserDomain(userid = userid, domainid = domainid ,createuserid =ownerid,updateuserid = ownerid)
db.add(user_domain)
db.commit()
return db_domain
return None
def delete_userdomain(self,db: Session, userid: int,domainid: int) -> schemas.DomainOut:
db_userdomain = dbuserdomain.get_userdomain(db,userid,domainid)
if db_userdomain:
domain = db_userdomain.domain
db.delete(db_userdomain)
db.commit()
return domain
return None
def get_default_domain(self,db: Session, userid: int) -> schemas.DomainOut:
userdomain = dbuserdomain.get_user_default_domain(db,userid)
if userdomain:
return userdomain.domain
else:
return None
def set_default_domain(self,db: Session, userid: int,domainid: int):
db_domain =db.execute(super().get_by_conditions({"id":domainid,"is_active":True})).scalars().first()
if db_domain:
db_default_domain = dbuserdomain.get_user_default_domain(db,userid)
db_userdomain =dbuserdomain.get_userdomain(db,userid,domainid)
if db_default_domain:
if db_default_domain.domainid != domainid:
db_default_domain.is_default = False
db_default_domain.updateuserid = userid
db.add(db_default_domain)
else:
return db_domain
if db_userdomain:
db_userdomain.is_default = True
db_userdomain.updateuserid = userid
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()
return db_domain
else:
return None
def get_shareddomain_users(self,db: Session,domainid: int) -> ApiReturnPage[models.Base]:
users = select(models.User).join(models.UserDomain,models.UserDomain.userid == models.User.id).filter(models.UserDomain.domainid ==domainid)
return paginate(db,users)
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

@@ -1,13 +0,0 @@
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

@@ -1,98 +0,0 @@
from datetime import datetime
from fastapi import HTTPException, status
from sqlalchemy.orm import Session
from sqlalchemy import 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 dbpermission(crudbase):
def __init__(self):
super().__init__(model=models.Permission)
dbpermission = dbpermission()
class dbrole(crudbase):
def __init__(self):
super().__init__(model=models.Role)
dbrole = dbrole()
class dbuser(crudbase):
def __init__(self):
super().__init__(model=models.User)
def get_user(self,db: Session, user_id: int) -> schemas.User:
return super().get(db,user_id)
def get_user_by_email(self,db: Session, email: str) -> schemas.User:
return db.execute(super().get_by_conditions({"email":email})).scalars().first()
def get_users(self,db: Session) -> ApiReturnPage[models.Base]:
return paginate(db,super().get_all())
def get_users_not_admin(self,db: Session) -> ApiReturnPage[models.Base]:
return paginate(db,super().get_by_conditions({"is_superuser":False}))
def create_user(self,db: Session, user: schemas.UserCreate,userid:int):
hashed_password = get_password_hash(user.password)
user.hashed_password = hashed_password
user.createuserid = userid
user.updateuserid = userid
del user.password
return super().create(db,user)
def delete_user(self,db: Session, user_id: int):
return super().delete(db,user_id)
def edit_user(self,db: Session, user_id:int,user: schemas.UserEdit,userid: int) -> schemas.User:
if not user.password is None and user.password != "":
user.hashed_password = get_password_hash(user.password)
del user.password
user.updateuserid = userid
return super().update(db,user_id,user)
def get_roles(self,db: Session) -> t.List[schemas.RoleBase]:
return db.execute(dbrole.get_all()).scalars().all()
#return dbrole.get_all().all()
def get_roles_by_level(self,db: Session,roles:t.List[models.Role]) -> t.List[schemas.RoleBase]:
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]):
db_user = super().get(db,user_id)
if db_user:
for role in db_user.roles:
if role.id not in roles:
db_user.roles.remove(role)
for roleid in roles:
role = dbrole.get(db,roleid)
if role not in db_user.roles:
db_user.roles.append(role)
db.commit()
db.refresh(db_user)
return db_user
def get_permissions(self,db: Session) -> t.List[schemas.Permission]:
return db.execute(dbpermission.get_all()).scalars().all()
def get_user_permissions(self,db: Session,user_id: int) -> t.List[schemas.Permission]:
permissions =[]
db_user = super().get(db,user_id)
if db_user:
for role in db_user.roles:
permissions += role.permissions
return list(set(permissions))
userService = dbuser()

View File

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

View File

@@ -8,57 +8,25 @@ class Base(BaseModel):
create_time: datetime
update_time: datetime
class Permission(BaseModel):
id: int
menu:str
function:str
link:str
privilege:str
class RoleBase(BaseModel):
id: int
name:str
description:str
level:int
class RoleWithPermission(RoleBase):
permissions:t.List[Permission] = []
class AssignUserRoles(BaseModel):
userid:int
roleids:t.List[int]
class UserBase(BaseModel):
email: str
is_active: bool = True
is_superuser: bool = False
first_name: str = None
last_name: str = None
roles:t.List[RoleBase] = []
class UserOut(BaseModel):
id: int
email: str
is_active: bool = True
is_superuser: bool = False
first_name: str = None
last_name: str = None
class UserOut(UserBase):
pass
class UserCreate(UserBase):
email:str
password: str
hashed_password :str = None
first_name: str
last_name: str
is_active:bool
is_superuser:bool
tenantid:t.Optional[str] = "1"
createuserid:t.Optional[int] = None
updateuserid:t.Optional[int] = None
class ConfigDict:
orm_mode = True
@@ -66,8 +34,6 @@ class UserCreate(UserBase):
class UserEdit(UserBase):
password: t.Optional[str] = None
hashed_password :str = None
updateuserid:t.Optional[int] = None
class ConfigDict:
orm_mode = True
@@ -89,27 +55,12 @@ class AppList(Base):
appname: str
appid:str
version:int
is_saved:bool
versionname: t.Optional[str] = None
updateuser: UserOut
createuser: UserOut
user:UserOut
class AppVersion(Base):
class AppVersion(BaseModel):
domainurl: str
appname: str
versionname: str
comment:str
appid:str
version:t.Optional[int] = None
updateuser: UserOut
createuser: UserOut
class VersionUpdate(BaseModel):
appid:str
versionname: str
comment:str
class TokenData(BaseModel):
id:int = 0
@@ -155,11 +106,9 @@ class Action(BaseModel):
class ConfigDict:
orm_mode = True
class FlowIn(BaseModel):
class FlowBase(BaseModel):
flowid: str
# domainurl:str
appid: str
appname:str
eventid: str
name: str = None
content: str = None
@@ -176,63 +125,28 @@ class Flow(Base):
class ConfigDict:
orm_mode = True
class DomainIn(BaseModel):
class DomainBase(BaseModel):
id: int
tenantid: str
name: str
url: str
kintoneuser: str
kintonepwd: t.Optional[str] = None
is_active: bool
createuserid:t.Optional[int] = None
updateuserid:t.Optional[int] = None
ownerid:t.Optional[int] = None
kintonepwd: str
def encrypt_kintonepwd(self):
encrypted_pwd = chacha20Encrypt(self.kintonepwd)
self.kintonepwd = encrypted_pwd
class DomainOut(BaseModel):
id: int
tenantid: str
name: str
url: str
kintoneuser: str
is_active: bool
ownerid:int
class ConfigDict:
orm_mode = True
class UserDomainParam(BaseModel):
userid:int
domainid:int
class UserDomain(BaseModel):
id: int
is_default: bool
domain:DomainOut
user:UserOut
class UserDomainIn(BaseModel):
is_default: bool
domainid:int
userid:int
class Domain(Base):
id: int
tenantid: str
name: str
url: str
kintoneuser: str
is_active: bool
updateuser:UserOut
owner:UserOut
kintonepwd: str
class ConfigDict:
orm_mode = True
class Event(Base):
id: int
category: str

View File

@@ -1,37 +1,21 @@
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker, declarative_base, Session
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
from app.core import config
engine = create_engine(
config.SQLALCHEMY_DATABASE_URI,
)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()
# engine = create_engine(
# config.SQLALCHEMY_DATABASE_URI,
# )
# SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
class Database:
def __init__(self, database_url: str):
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()
# Dependency
def get_db():
db = SessionLocal()
try:
yield db
yield db
finally:
db.close()
def get_user_db(database_url: str):
database = Database(database_url)
db = database.get_db()
return db
db.close()

View File

@@ -1,6 +1,5 @@
import os
from fastapi import FastAPI, Depends
from fastapi_pagination import add_pagination
from starlette.requests import Request
import uvicorn
from app.api.api_v1.routers.kintone import kinton_router
@@ -8,29 +7,21 @@ 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.platform import platform_router
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.celery_app import celery_app
from app import tasks
from fastapi.middleware.cors import CORSMiddleware
import logging
from app.core.apiexception import APIException, writedblog
from app.core.common import ApiReturnError
from app.db.crud import create_log
from fastapi.responses import JSONResponse
import asyncio
from contextlib import asynccontextmanager
from app.core.operation import LoggingMiddleware
#Base.metadata.create_all(bind=engine)
@asynccontextmanager
async def lifespan(app: FastAPI):
startup_event()
yield
Base.metadata.create_all(bind=engine)
app = FastAPI(
title=config.PROJECT_NAME, docs_url="/api/docs", openapi_url="/api",lifespan=lifespan
title=config.PROJECT_NAME, docs_url="/api/docs", openapi_url="/api"
)
origins = [
@@ -45,10 +36,6 @@ app.add_middleware(
allow_headers=["*"],
)
app.add_middleware(LoggingMiddleware)
add_pagination(app)
# @app.middleware("http")
# async def db_session_middleware(request: Request, call_next):
# request.state.db = SessionLocal()
@@ -56,11 +43,12 @@ add_pagination(app)
# request.state.db.close()
# return response
def startup_event():
@app.on_event("startup")
async def startup_event():
log_dir="log"
if not os.path.exists(log_dir):
os.makedirs(log_dir)
logger = logging.getLogger("uvicorn.access")
handler = logging.handlers.RotatingFileHandler(f"{log_dir}/api.log",mode="a",maxBytes = 100*1024, backupCount = 3)
handler.setFormatter(logging.Formatter("%(asctime)s - %(levelname)s - %(message)s"))
@@ -72,7 +60,7 @@ async def api_exception_handler(request: Request, exc: APIException):
loop.run_in_executor(None,writedblog,exc)
return JSONResponse(
status_code=exc.status_code,
content= ApiReturnError(msg = f"{exc.detail}").model_dump(),
content={"detail": f"{exc.detail}"},
)
@app.get("/api/v1")

View File

@@ -1,252 +0,0 @@
import pytest
from sqlalchemy import create_engine, event
from sqlalchemy.orm import sessionmaker
from fastapi.testclient import TestClient
import typing as t
from app.core import config, security
from app.core.dbmanager import get_db
from app.db import models,schemas
from app.main import app
from app.core import security
import jwt
SQLALCHEMY_DATABASE_URI = f"postgresql+psycopg2://kabAdmin:P%40ssw0rd!@kintonetooldb.postgres.database.azure.com/test"
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():
connection = engine.connect()
transaction = connection.begin()
test_session = test_session_maker(bind=connection)
yield test_session
test_session.close()
transaction.rollback()
#transaction.commit()
connection.close()
@pytest.fixture(scope="session")
def test_client(test_db):
def get_test_db():
try:
yield test_db
finally:
test_db.close()
app.dependency_overrides[get_db] = get_test_db
with TestClient(app) as 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
# def test_password() -> str:
# return "securepassword"
# def get_password_hash() -> str:
# """
# Password hashing can be expensive so a mock will be much faster
# """
# return "supersecrethash"
# @pytest.fixture
# def test_user(test_db) -> models.User:
# """
# Make a test user in the database
# """
# user = models.User(
# email="fake@email.com",
# hashed_password=get_password_hash(),
# is_active=True,
# )
# test_db.add(user)
# test_db.commit()
# return user
# @pytest.fixture
# def test_superuser(test_db) -> models.User:
# """
# Superuser for testing
# """
# user = models.User(
# email="fakeadmin@email.com",
# hashed_password=get_password_hash(),
# is_superuser=True,
# )
# test_db.add(user)
# test_db.commit()
# return user
# def verify_password_mock(first: str, second: str) -> bool:
# return True
# @pytest.fixture
# def user_token_headers(
# client: TestClient, test_user, test_password, monkeypatch
# ) -> t.Dict[str, str]:
# monkeypatch.setattr(security, "verify_password", verify_password_mock)
# login_data = {
# "username": test_user.email,
# "password": test_password,
# }
# r = client.post("/api/token", data=login_data)
# tokens = r.json()
# a_token = tokens["access_token"]
# headers = {"Authorization": f"Bearer {a_token}"}
# return headers
# @pytest.fixture
# def superuser_token_headers(
# client: TestClient, test_superuser, test_password, monkeypatch
# ) -> t.Dict[str, str]:
# monkeypatch.setattr(security, "verify_password", verify_password_mock)
# login_data = {
# "username": test_superuser.email,
# "password": test_password,
# }
# r = client.post("/api/token", data=login_data)
# tokens = r.json()
# a_token = tokens["access_token"]
# headers = {"Authorization": f"Bearer {a_token}"}
# return headers

View File

@@ -1,5 +0,0 @@
[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

@@ -1,11 +0,0 @@
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

@@ -1,175 +0,0 @@
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

@@ -1,6 +1,4 @@
def test_read_main(test_client):
response = test_client.get("/api/v1")
def test_read_main(client):
response = client.get("/api/v1")
assert response.status_code == 200
assert response.json() == {"message": "success"}
assert response.json() == {"message": "Hello World"}

View File

@@ -1,156 +0,0 @@
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

@@ -1,121 +0,0 @@
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

@@ -1,9 +0,0 @@
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

169
backend/conftest.py Normal file
View File

@@ -0,0 +1,169 @@
import pytest
from sqlalchemy import create_engine, event
from sqlalchemy.orm import sessionmaker
from sqlalchemy_utils import database_exists, create_database, drop_database
from fastapi.testclient import TestClient
import typing as t
from app.core import config, security
from app.db.session import Base, get_db
from app.db import models
from app.main import app
def get_test_db_url() -> str:
return f"{config.SQLALCHEMY_DATABASE_URI}_test"
@pytest.fixture
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()
trans = connection.begin()
# 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
# Roll back the parent transaction after the test is complete
test_session.close()
trans.rollback()
connection.close()
@pytest.fixture(scope="session", autouse=True)
def create_test_db():
"""
Create a test database and use it for the whole test session.
"""
test_db_url = get_test_db_url()
# Create the test database
assert not database_exists(
test_db_url
), "Test database already exists. Aborting tests."
create_database(test_db_url)
test_engine = create_engine(test_db_url)
Base.metadata.create_all(test_engine)
# Run the tests
yield
# Drop the test database
drop_database(test_db_url)
@pytest.fixture
def client(test_db):
"""
Get a TestClient instance that reads/write to the test database.
"""
def get_test_db():
yield test_db
app.dependency_overrides[get_db] = get_test_db
yield TestClient(app)
@pytest.fixture
def test_password() -> str:
return "securepassword"
def get_password_hash() -> str:
"""
Password hashing can be expensive so a mock will be much faster
"""
return "supersecrethash"
@pytest.fixture
def test_user(test_db) -> models.User:
"""
Make a test user in the database
"""
user = models.User(
email="fake@email.com",
hashed_password=get_password_hash(),
is_active=True,
)
test_db.add(user)
test_db.commit()
return user
@pytest.fixture
def test_superuser(test_db) -> models.User:
"""
Superuser for testing
"""
user = models.User(
email="fakeadmin@email.com",
hashed_password=get_password_hash(),
is_superuser=True,
)
test_db.add(user)
test_db.commit()
return user
def verify_password_mock(first: str, second: str) -> bool:
return True
@pytest.fixture
def user_token_headers(
client: TestClient, test_user, test_password, monkeypatch
) -> t.Dict[str, str]:
monkeypatch.setattr(security, "verify_password", verify_password_mock)
login_data = {
"username": test_user.email,
"password": test_password,
}
r = client.post("/api/token", data=login_data)
tokens = r.json()
a_token = tokens["access_token"]
headers = {"Authorization": f"Bearer {a_token}"}
return headers
@pytest.fixture
def superuser_token_headers(
client: TestClient, test_superuser, test_password, monkeypatch
) -> t.Dict[str, str]:
monkeypatch.setattr(security, "verify_password", verify_password_mock)
login_data = {
"username": test_superuser.email,
"password": test_password,
}
r = client.post("/api/token", data=login_data)
tokens = r.json()
a_token = tokens["access_token"]
headers = {"Authorization": f"Bearer {a_token}"}
return headers

View File

@@ -29,11 +29,3 @@ python -m venv env
```bash
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

File diff suppressed because one or more lines are too long

Binary file not shown.

View File

@@ -1,5 +1,5 @@
#開発環境
#KAB_BACKEND_URL="https://ktune-backend-dev-eba8fkeyffegc3cz.japanwest-01.azurewebsites.net/"
#KAB_BACKEND_URL="https://kab-backend.azurewebsites.net/"
#単体テスト環境
#KAB_BACKEND_URL="https://kab-backend-unittest.azurewebsites.net/"
#ローカル開発環境

View File

@@ -1,6 +1,6 @@
{
"name": "k-tune",
"version": "2.0.0 Beta",
"version": "0.2.0",
"description": "Kintoneアプリの自動生成とデプロイを支援ツールです",
"productName": "k-tune | kintoneジェネレーター",
"author": "maxiaozhe@alicorns.co.jp <maxiaozhe@alicorns.co.jp>",

View File

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

View File

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

View File

@@ -4,14 +4,14 @@ import { Router } from 'vue-router';
import { App } from 'vue';
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) => {
if (err.response && err.response.status === 401) {
// 認証エラーの場合再ログインする
console.error('(; ゚Д゚)/認証エラー(401)', err, info);
localStorage.removeItem('token');
router.replace({
path:'/login',
path:"/login",
query:{redirect:router.currentRoute.value.fullPath}
});
} else {

View File

@@ -1,75 +0,0 @@
// 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

@@ -18,7 +18,6 @@
indicator-color="primary"
active-bg-color="primary"
class="bg-grey-2 text-grey-8"
@update:model-value="() => selected = []"
dense
>
<q-tab :name="cate"
@@ -53,7 +52,7 @@ export default {
filter:String
},
emits:[
'clearFilter'
"clearFilter"
],
setup(props,{emit}) {
const isLoaded=ref(false);

View File

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

View File

@@ -1,67 +1,64 @@
<template>
<detail-field-table
detailField="description"
:name="name"
:type="type"
:filter="filter"
:columns="columns"
:fetchData="fetchApps"
@update:selected="(item) => { selected = item }"
/>
<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;">
<template v-slot:body-cell-description="props">
<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>
<script lang="ts">
import { ref, PropType, watchEffect } from 'vue';
import { ref, onMounted, reactive, watchEffect } from 'vue'
import { api } from 'boot/axios';
import DetailFieldTable from './dialog/DetailFieldTable.vue';
interface IAppDisplay {
id: string;
name: string;
description: string;
createdate: string;
}
export default {
name: 'AppSelectBox',
components: {
DetailFieldTable
},
props: {
name: String,
type: String,
filter: String,
filterInitRowsFunc: {
type: Function as PropType<(app: IAppDisplay) => boolean>,
},
updateSelectApp: {
type: Function
}
},
setup(props) {
const selected = ref<IAppDisplay[]>([]);
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 },
{ name: 'name', label: 'アプリ名', field: 'name', sortable: true, align: 'left' },
{ name: 'description', label: '概要', field: 'description', align: 'left', sortable: false },
{ name: 'createdate', label: '作成日時', field: 'createdate', align: 'left' }
];
]
const isLoaded = ref(false);
const rows: any[] = reactive([]);
const selected = ref([])
watchEffect(()=>{
if (selected.value && selected.value[0] && props.updateSelectApp) {
props.updateSelectApp(selected.value[0])
}
});
const fetchApps = async () => {
const res = await api.get('api/v1/allapps');
return res.data.apps.map((item: any) => ({
id: item.appId,
name: item.name,
description: item.description,
createdate: dateFormat(item.createdAt)
})).filter(app => !props.filterInitRowsFunc || props.filterInitRowsFunc(app));
};
onMounted(() => {
api.get('api/v1/allapps').then(res => {
res.data.apps.forEach((item: any) => {
rows.push({
id: item.appId,
name: item.name,
description: item.description,
createdate: dateFormat(item.createdAt)
});
});
isLoaded.value = true;
});
});
const dateFormat = (dateStr: string) => {
const date = new Date(dateStr);
@@ -73,13 +70,31 @@ export default {
const minutes = pad(date.getMinutes());
const seconds = pad(date.getSeconds());
return `${year}/${month}/${day} ${hours}:${minutes}:${seconds}`;
};
}
return {
columns,
fetchApps,
selected
};
}
};
</script>
rows,
selected,
isLoaded,
pagination: ref({
rowsPerPage: 10
})
}
},
}
</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) {
$q.notify({
type: 'negative',
caption: 'エラー',
caption: "エラー",
message: 'データソースを現在のアプリにすることはできません。'
});
} else if (selected.id !== data.value.sourceApp.id) {
@@ -208,7 +208,7 @@ export default defineComponent({
if (isDuplicate) {
$q.notify({
type: 'negative',
caption: 'エラー',
caption: "エラー",
message: '重複したフィールドは選択できません'
});
} else {

View File

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

View File

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

View File

@@ -1,105 +1,98 @@
<template>
<div class="q-pa-md">
<q-uploader
style="max-width: 400px"
:url="uploadUrl"
:label="title"
:headers="headers"
accept=".xlsx"
v-on:rejected="onRejected"
v-on:uploaded="onUploadFinished"
v-on:failed="onFailed"
field-name="files"
style="max-width: 400px"
:url="uploadUrl"
:label="title"
:headers="headers"
accept=".xlsx"
v-on:rejected="onRejected"
v-on:uploaded="onUploadFinished"
v-on:failed="onFailed"
field-name="files"
></q-uploader>
</div>
</template>
<script setup lang="ts">
import { useQuasar } from 'quasar';
import { createUploaderComponent, useQuasar } from 'quasar';
import { useAuthStore } from 'src/stores/useAuthStore';
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 {
title?: string;
uploadUrl?: string;
}
/**
* 例外発生時、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;
}
}
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
});
}
const props = withDefaults(defineProps<Props>(), {
title: '設計書から導入する(Excel)',
uploadUrl: `${process.env.KAB_BACKEND_URL}api/v1/createappfromexcel?format=1`,
});
interface Props {
title: string;
uploadUrl:string;
}
/**
* ファイルアップロードを拒否する時の処理
* @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 headers = ref([{name:"Authorization",value:'Bearer ' + authStore.token}]);
const props = withDefaults(defineProps<Props>(), {
title:"設計書から導入する(Excel)",
uploadUrl: `${process.env.KAB_BACKEND_URL}api/v1/createappfromexcel?format=1`
/**
* ファイルアップロード成功時の処理
*/
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>
<style lang="scss"></style>
<style lang="scss">
</style>

View File

@@ -1,78 +1,37 @@
<template>
<div class="q-pa-md">
<q-table :loading="loading" :title="name+'一覧'" :selection="type" v-model:selected="selected" :columns="columns" :rows="rows">
<template v-slot:body-cell-name="p">
<q-td class="content-box flex justify-between items-center" :props="p">
{{ p.row.name }}
<q-badge v-if="!p.row.domainActive" color="grey">未启用</q-badge>
<q-badge v-if="p.row.id == currentDomainId" color="primary">現在</q-badge>
</q-td>
</template>
</q-table>
<q-table :title="name+'一覧'" :selection="type" v-model:selected="selected" :columns="columns" :rows="rows" />
</div>
</template>
<script>
import { ref,onMounted,reactive, computed } from 'vue'
import { ref,onMounted,reactive } from 'vue'
import { api } from 'boot/axios';
import { useAuthStore } from 'src/stores/useAuthStore';
export default {
name: 'DomainSelect',
props: {
name: String,
type: String,
filterInitRowsFunc: {
type: Function,
},
type: String
},
setup(props) {
const authStore = useAuthStore();
const currentDomainId = computed(() => authStore.currentDomain.id);
const loading = ref(true);
const inactiveRowClass = (row) => row.domainActive ? '' : 'inactive-row';
const columns = [
{ name: 'id', label: 'ID', field: 'id', align: 'left', sortable: true, classes: inactiveRowClass },
{ name: 'name', align: 'left', label: 'ドメイン', field: 'name', 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: 'owner', label: '所有者', field: row => row.owner.fullName, align: 'left', classes: inactiveRowClass },
]
const rows = reactive([]);
onMounted(() => {
loading.value = true;
api.get('api/domains').then(res =>{
res.data.data.forEach((data) => {
const item = {
id: data.id,
tenantid: data.tenantid,
domainActive: data.is_active,
name: data.name,
url: data.url,
user: data.kintoneuser,
owner: {
id: data.owner.id,
firstName: data.owner.first_name,
lastName: data.owner.last_name,
fullNameSearch: (data.owner.last_name + data.owner.first_name).toLowerCase(),
fullName: data.owner.last_name + ' ' + data.owner.first_name,
email: data.owner.email,
isActive: data.owner.is_active,
isSuperuser: data.owner.is_superuser,
}
}
if (props.filterInitRowsFunc && !props.filterInitRowsFunc(item)) {
return;
}
rows.push(item);
})
loading.value = false;
setup() {
const columns = [
{ name: 'id'},
{ name: 'tenantid', required: true,label: 'テナント',align: 'left',field: 'tenantid',sortable: true},
{ name: 'name', align: 'center', label: 'ドメイン', field: 'name', sortable: true },
{ name: 'url', label: 'URL', field: 'url', sortable: true },
{ name: 'kintoneuser', label: 'アカウント', field: 'kintoneuser' }
]
const rows = reactive([])
onMounted( () => {
api.get(`api/domains/1`).then(res =>{
res.data.forEach((item) =>
{
rows.push({id:item.id,tenantid:item.tenantid,name:item.name,url:item.url,kintoneuser:item.kintoneuser});
}
)
});
});
});
return {
loading,
currentDomainId,
columns,
rows,
selected: ref([]),
@@ -81,11 +40,3 @@ export default {
}
</script>
<style lang="scss">
.q-table td.inactive-row {
color: #aaa;
}
.q-table .content-box {
box-sizing: content-box;
}
</style>

View File

@@ -7,26 +7,54 @@
icon="share"
size="md"
:label="userStore.currentDomain.domainName"
:disable-dropdown="true"
dropdown-icon="none"
:disable="true"
:disable-dropdown="isUnclickable"
:dropdown-icon="isUnclickable ? 'none' : ''"
:disable="isUnclickable"
>
<q-list>
<q-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="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>
</template>
<script setup lang="ts">
import { useAuthStore } from 'stores/useAuthStore';
</template>
<script setup lang="ts" >
import { IDomainInfo } from 'src/types/ActionTypes';
import { useAuthStore,IUserState } from 'stores/useAuthStore';
import { ref, computed } from 'vue';
import { useRoute } from 'vue-router';
const userStore = useAuthStore();
const route = useRoute()
const domains = ref<IDomainInfo[]>([]);
(async ()=>{
domains.value = await userStore.getUserDomains();
})();
const isUnclickable = computed(()=>{
return route.path.startsWith('/FlowChart/') || domains.value === undefined || domains.value.length === 0;
});
const onItemClick=(domain:IDomainInfo)=>{
console.log(domain);
userStore.setCurrentDomain(domain);
}
</script>
<style lang="scss">
.q-btn.disabled.customized-disabled-btn {
opacity: 1 !important;
cursor: default !important;
.q-icon.q-btn-dropdown__arrow {
display: none;
}
* {
cursor: default !important;
}
}
.q-btn.disabled.customized-disabled-btn * {
cursor: default !important;
}
</style>

View File

@@ -1,11 +1,9 @@
<template>
<q-item
v-permissions="permission"
clickable
tag="a"
:target="target?target:'_blank'"
:href="link"
:disable="disable"
v-if="!isSeparator"
>
<q-item-section
@@ -35,8 +33,6 @@ export interface EssentialLinkProps {
icon?: string;
isSeparator?: boolean;
target?:string;
disable?:boolean;
permission?: string|null;
}
withDefaults(defineProps<EssentialLinkProps>(), {
caption: '',

View File

@@ -73,14 +73,14 @@ export default {
if(!props.blackListLabel.find(blackListItem => blackListItem === fld.label)){
if(props.fieldTypes.length===0 || props.fieldTypes.includes(fld.type)){
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 });
}
}
} else {
if(props.fieldTypes.length===0 || props.fieldTypes.includes(fld.type)){
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 });
}
}

View File

@@ -1,24 +0,0 @@
<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

@@ -1,276 +0,0 @@
<template>
<q-dialog :auto-close="false" :model-value="visible" persistent bordered>
<q-card class="dialog-content" >
<q-toolbar class="bg-grey-4">
<q-toolbar-title>{{ dialogTitle }}</q-toolbar-title>
<q-btn flat round dense icon="close" @click="close" />
</q-toolbar>
<q-card-section class="q-mx-md " >
<q-select
v-permissions="props.actionPermissions.edit"
class="q-mt-md"
:disable="loading||!domain.domainActive"
filled
dense
v-model="canSharedUserFilter"
use-input
input-debounce="0"
:options="canSharedUserFilteredOptions"
clearable
:placeholder="canSharedUserFilter ? '' : domain.domainActive ? '権限を付与するユーザーを選択' : '接続先が無効なため、権限を付与できません'"
@filter="filterFn">
<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>
<q-btn :disable="!canSharedUserFilter" :loading="addLoading" label="付与" color="primary" @click="shareTo(canSharedUserFilter as IUserDisplayWithShareRole)" />
</template>
<template v-slot:option="scope">
<q-item v-bind="scope.itemProps">
<q-item-section avatar>{{scope.opt.id}}</q-item-section>
<q-item-section>{{scope.opt.fullName}}</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>
</template>
</q-select>
<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 }">
<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>
</sharing-user-list>
</q-card-section>
<q-card-actions align="right" class="text-primary">
<q-btn flat label="確定" @click="checkClose" />
<q-btn flat label="キャンセル" @click="close" />
</q-card-actions>
</q-card>
</q-dialog>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue';
import { IDomainOwnerDisplay } from '../../types/DomainTypes';
import { IUser, IUserDisplay } from '../../types/UserTypes';
import { api } from 'boot/axios';
import { useAuthStore } from 'stores/useAuthStore';
import SharingUserList from 'components/ShareDomain/SharingUserList.vue';
import RoleLabel from 'components/ShareDomain/RoleLabel.vue';
import { Dialog } from 'quasar'
const authStore = useAuthStore();
interface Props {
modelValue: boolean;
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 emit = defineEmits<{
(e: 'update:modelValue', value: boolean): void;
(e: 'close'): void;
}>();
const addLoading = ref(false);
const loading = ref(true);
const visible = ref(props.modelValue);
const allUsers = ref<IUserDisplayWithShareRole[]>([]);
const sharedUsers = ref<IUserDisplayWithShareRole[]>([]);
const sharedUsersIdSet = new Set<number>();
const canSharedUsers = ref<IUserDisplayWithShareRole[]>([]);
const canSharedUserFilter = ref<IUserDisplayWithShareRole>();
const canSharedUserFilteredOptions = ref<IUserDisplayWithShareRole[]>([]);
const filterFn = (val:string, update: (cb: () => void) => void) => {
update(() => {
if (val === '') {
canSharedUserFilteredOptions.value = canSharedUsers.value;
return;
}
const needle = val.toLowerCase();
canSharedUserFilteredOptions.value = canSharedUsers.value.filter(v =>
v.email.toLowerCase().indexOf(needle) > -1 || v.fullNameSearch.toLowerCase().indexOf(needle) > -1);
})
}
watch(
() => props.modelValue,
async (newValue) => {
visible.value = newValue;
sharedUsers.value = [];
canSharedUserFilter.value = undefined
loading.value = false;
addLoading.value = false;
if (newValue) {
if (Object.keys(allUsers.value).length == 0) {
await getUsers();
}
await loadShared();
}
}
);
watch(
() => visible.value,
(newValue) => {
emit('update:modelValue', newValue);
}
);
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 = () => {
emit('close');
};
const shareTo = async (user: IUserDisplayWithShareRole) => {
addLoading.value = true;
loading.value = true;
await props.shareApi(user, props.domain);
await loadShared();
canSharedUserFilter.value = undefined;
loading.value = false;
addLoading.value = false;
}
const removeShareTo = async (user: IUserDisplayWithShareRole) => {
loading.value = true;
user.isRemoving = true;
await props.removeSharedApi(user, props.domain);
if (isCurrentDomain()) {
await authStore.loadCurrentDomain();
}
await loadShared();
loading.value = false;
};
const isCurrentDomain = () => {
return props.domain.id === authStore.currentDomain.id;
}
const loadShared = async () => {
loading.value = true;
sharedUsersIdSet.clear();
const { data } = await props.getSharedApi(props.domain);
sharedUsers.value = data.data.reduce((arr: IUserDisplayWithShareRole[], item: IUser) => {
const val = itemToDisplay(item);
if(!sharedUsersIdSet.has(val.id)) {
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));
canSharedUserFilteredOptions.value = canSharedUsers.value;
loading.value = false;
}
function isOwner(userId: number) {
return userId === props.domain?.owner?.id
}
function isManager(userId: number) {
return false // TODO
}
const getUsers = async () => {
loading.value = true;
const result = await api.get('api/v1/users');
allUsers.value = result.data.data.map(itemToDisplay);
loading.value = false;
}
const itemToDisplay = (item: IUser) => {
return {
id: item.id,
firstName: item.first_name,
lastName: item.last_name,
fullNameSearch: (item.last_name + item.first_name).toLowerCase(),
fullName: item.last_name + ' ' + item.first_name,
email: item.email,
isSuperuser: item.is_superuser,
isActive: item.is_active,
role: 0,
} as IUserDisplayWithShareRole
}
</script>
<style lang="scss">
.dialog-content {
width: 700px !important;
max-width: 80vw !important;
max-height: 80vh;
.q-select {
min-width: 0 !important;
}
}
</style>

View File

@@ -1,63 +0,0 @@
<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

@@ -1,58 +0,0 @@
<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

@@ -1,53 +0,0 @@
<template>
<q-table :rows="users" :filter="filter" dense :columns="columns" row-key="id" :loading="loading" :pagination="pagination">
<template v-slot:top>
<div class="h6 text-weight-bold">{{props.title}}</div>
<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-for="col in columns" :key="col.name" v-slot:[`body-cell-${col.name}`]="props">
<slot :name="`body-cell-${col.name}`" :row="props.row" :column="props.col">
<!-- 默认内容 -->
<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>
</q-table>
</template>
<script setup lang="ts">
import { ref, PropType } from 'vue';
import { IUserDisplay } from '../../types/UserTypes';
const props = defineProps({
users: {
type: Array as PropType<IUserDisplay[]>,
required: true,
},
loading: {
type: Boolean,
default: false,
},
title: String
});
const columns = [
{ name: 'id', label: 'ID', field: 'id', align: 'left', sortable: true },
{ name: 'fullName', label: '名前', field: 'fullName', 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 },
];
const filter = ref('');
const pagination = ref({ rowsPerPage: 10 });
</script>

View File

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

View File

@@ -1,92 +0,0 @@
<template>
<q-btn v-if="hasPermission()" flat padding="xs" round size="1em" icon="more_vert" class="action-menu">
<q-menu :max-width="maxWidth">
<q-list dense :style="{ 'min-width': minWidth }">
<template v-for="(item, index) in actions" :key="index" >
<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-icon size="1.2em" :name="item.icon" />
</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-separator v-else />
</template>
</q-list>
</q-menu>
</q-btn>
</template>
<script lang="ts">
import { PropType, getCurrentInstance } from 'vue';
import { IDomainOwnerDisplay } from '../types/DomainTypes';
interface Action {
label: string;
icon?: string;
tooltip?: string|((row: IDomainOwnerDisplay) => string);
disable?: boolean|((row: IDomainOwnerDisplay) => boolean);
permission?: string|object;
action: (row: any) => void|Promise<void>;
class?: string;
}
interface Separator {
separator: boolean;
}
type MenuItem = Action | Separator;
export default {
name: 'TableActionMenu',
props: {
row: {
type: Object as PropType<IDomainOwnerDisplay>,
required: true
},
maxWidth: {
type: String,
default: '150px'
},
minWidth: {
type: String,
default: '100px'
},
actions: {
type: Array as PropType<MenuItem[]>,
required: true
}
},
methods: {
isAction(item: MenuItem): item is Action {
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>
<style lang="scss" scoped>
.q-table tr > td:last-child .action-menu {
opacity: 0.25 !important;
}
.q-table tr:hover > td:last-child .action-menu:not([disabled]) {
opacity: 1 !important;
}
</style>

View File

@@ -1,74 +0,0 @@
<template>
<q-card :class="['domain-card', item.id == activeId ? 'default': '']">
<q-card-section>
<div class="row no-wrap">
<div class="col">
<div class="text-h6 ellipsis">{{ item.name }}</div>
<div class="text-subtitle2">{{ item.url }}</div>
</div>
<div class="col-auto">
<!-- <q-badge color="secondary" text-color="white" align="middle" class="q-mb-xs" label="他人の所有" /> -->
<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>
</q-card-section>
<q-card-section>
<div class="row items-center no-wrap">
<div class="col">
<div class="text-grey-7 text-caption text-weight-medium">
アカウント
</div>
<div class="smaller-font-size">{{ item.user }}</div>
</div>
<div class="col-auto">
<div class="text-grey-7 text-caption text-weight-medium">
所有者
</div>
<div class="smaller-font-size">{{ !isOwnerFunc(item.owner.id) ? item.owner.fullName : '自分' }}</div>
</div>
</div>
</q-card-section>
<q-separator v-if="$slots.actions" />
<slot name="actions" :item="item"></slot>
</q-card>
</template>
<script setup lang="ts">
import { defineProps, computed } from 'vue';
import { IDomainOwnerDisplay } from 'src/types/DomainTypes';
import { useAuthStore } from 'stores/useAuthStore';
const props = defineProps<{
item: IDomainOwnerDisplay;
activeId: number;
}>();
const authStore = useAuthStore();
const isOwnerFunc = computed(() => (ownerId: string) => {
return ownerId == authStore.userId;
});
</script>
<style lang="scss" scoped>
.domain-card.default {
box-shadow: 0 6px 6px -3px rgba(0, 0, 0, 0.2),
0 10px 14px 1px rgba(0, 0, 0, 0.14),
0 4px 18px 3px rgba(0, 0, 0, 0.12),
inset 0 0 0px 2px #1976D2;
}
.domain-card {
width: 22rem;
word-break: break-word;
.smaller-font-size {
font-size: 13px;
}
}
</style>

View File

@@ -1,40 +0,0 @@
<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

@@ -9,8 +9,8 @@ import { api } from 'boot/axios';
const props = defineProps<{filter:string}>()
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: 'firstName', label: '氏名', field: 'firstName', align: 'left', sortable: true },
{ name: 'lastName', label: '苗字', field: 'lastName', align: 'left', sortable: true },
{ name: 'email', label: '電子メール', field: 'email', align: 'left', sortable: true },
];
@@ -23,8 +23,8 @@ defineExpose({
})
const getUsers = async (filter = () => true) => {
loading.value = true;
const result = await api.get('api/v1/users');
rows.value = result.data.data.map((item) => {
const result = await api.get(`api/v1/users`);
rows.value = result.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;

View File

@@ -1,105 +0,0 @@
<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

@@ -1,71 +0,0 @@
<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

@@ -1,100 +0,0 @@
<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

@@ -1,56 +1,34 @@
<template>
<q-input
ref="nameRef"
v-model="versionInfo.name"
filled
autofocus
label="バージョン名"
:rules="[
val => !!val || 'バージョン名を入力してください。',
(val) => !val || val.length <= 80 || '80字以内で入力ください'
]"
:rules="[(val) => val.length <= 20 || '20字以内で入力ください']"
/>
<q-input
ref="commentRef"
v-model="versionInfo.comment"
v-model="versionInfo.desc"
filled
type="textarea"
:rules="[(val) => !val || val.length <= 300 || '300字以内で入力ください']"
:rules="[(val) => val.length <= 80 || '80字以内で入力ください']"
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;
};
import { IVersionInfo } from 'src/types/AppTypes';
const props = defineProps<{
modelValue: IVersionSubmit;
modelValue: IVersionInfo;
}>();
const defaultTitle = `${new Date().toLocaleString()}`;
const versionInfo = ref({
...props.modelValue,
name: props.modelValue.name || defaultTitle,
comment: props.modelValue.comment || '',
name: props.modelValue.name || `新バージョン ${new Date().toLocaleString()}`,
});
const emit = defineEmits(['update:modelValue']);
defineExpose({
isValid
})
watch(
versionInfo,
() => {

View File

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

View File

@@ -74,7 +74,7 @@ export default defineComponent({
const selectedEvent = ref<IKintoneEvent | undefined>(store.selectedEvent);
const selectedChangeEvent = ref<IKintoneEventGroup | undefined>(undefined);
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) => {
@@ -117,7 +117,7 @@ export default defineComponent({
$q.notify({
type: 'positive',
caption: '通知',
caption: "通知",
message: `イベント ${node.label} 削除`
})
}

View File

@@ -92,11 +92,11 @@ export default defineComponent({
},
emits: [
'addNode',
'nodeSelected',
'nodeEdit',
'deleteNode',
'deleteAllNextNodes',
'copyFlow'
"nodeSelected",
"nodeEdit",
"deleteNode",
"deleteAllNextNodes",
"copyFlow"
],
setup(props, context) {
const store = useFlowEditorStore();
@@ -204,7 +204,7 @@ export default defineComponent({
* 変数名取得
*/
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;
};
const copyFlow=()=>{

View File

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

View File

@@ -1,33 +0,0 @@
<template>
<q-table :rows="rows" :columns="columns" row-key="id" :filter="filter" :loading="loading" :pagination="pagination">
<template v-slot:top>
<q-btn color="primary" :disable="loading" label="新規" @click="addRow" />
<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-name="p">
<q-td class="flex justify-between items-center" :props="p">
{{ p.row.name }}
<q-badge v-if="!p.row.domainActive" color="grey">未启用</q-badge>
<q-badge v-if="p.row.id == currendDomainId" color="primary">現在</q-badge>
</q-td>
</template>
<template v-slot:body-cell-actions="p">
<q-td :props="p">
<q-btn-group flat>
<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>
</template>
</q-table>
</template>

View File

@@ -55,7 +55,7 @@ export default defineComponent({
});
const connectProps=(props:IProp)=>{
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){
let targetProp = componentData.value.find((prop)=>prop.props.name===connProp.propName);
if(targetProp){

View File

@@ -72,11 +72,11 @@ export default defineComponent({
}
},
setup(props, { emit }) {
const color = ref(props.modelValue??'');
const isSelected = computed(()=>props.modelValue && props.modelValue!=='');
const color = ref(props.modelValue??"");
const isSelected = computed(()=>props.modelValue && props.modelValue!=="");
const customExp = props.rules === undefined ? [] : eval(props.rules);
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];
watchEffect(()=>{
emit('update:modelValue', color.value);

View File

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

View File

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

View File

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

View File

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

View File

@@ -59,7 +59,7 @@ export default defineComponent({
const properties=ref(props.nodeProps);
const connectProps=(props:IProp)=>{
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){
let targetProp = properties.value.find((prop)=>prop.props.name===connProp.propName);
if(targetProp){

View File

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

View File

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

View File

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

View File

@@ -8,7 +8,7 @@
<q-badge align="top" outline>V{{ version }}</q-badge>
</q-toolbar-title>
<domain-selector></domain-selector>
<user-info-button />
<q-btn flat round dense icon="logout" @click="authStore.logout()" />
</q-toolbar>
</q-header>
@@ -19,6 +19,10 @@
</q-item-label>
<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-drawer>
@@ -30,27 +34,20 @@
</template>
<script setup lang="ts">
import { computed, onMounted, reactive, getCurrentInstance } from 'vue';
import { onMounted } from 'vue';
import EssentialLink, { EssentialLinkProps } from 'components/EssentialLink.vue';
import DomainSelector from 'components/DomainSelector.vue';
import UserInfoButton from 'components/UserInfoButton.vue';
import { useAuthStore } from 'stores/useAuthStore';
import { useRoute } from 'vue-router';
import { MenuMapping } from 'src/boot/permissions';
const authStore = useAuthStore();
const route = useRoute()
const noDomain = computed(() => !authStore.hasDomain);
const essentialLinks: EssentialLinkProps[] = reactive([
const essentialLinks: EssentialLinkProps[] = [
{
title: 'ホーム',
caption: '設計書から導入する',
icon: 'home',
link: '/',
target: '_self',
disable: noDomain,
permission: MenuMapping.home
target: '_self'
},
// {
// title: 'フローエディター',
@@ -60,13 +57,11 @@ const essentialLinks: EssentialLinkProps[] = reactive([
// target: '_self'
// },
{
title: 'アプリ',
caption: 'アプリのカスタマイズ',
title: 'アプリ管理',
caption: 'アプリを管理する',
icon: 'widgets',
link: '/#/app',
target: '_self',
disable: noDomain,
permission: MenuMapping.app
target: '_self'
},
// {
// title: '条件エディター',
@@ -79,40 +74,6 @@ const essentialLinks: EssentialLinkProps[] = reactive([
title: '',
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 ポータル',
// caption:'Kintone',
@@ -131,17 +92,47 @@ const essentialLinks: EssentialLinkProps[] = reactive([
// link:'https://cybozu.dev/ja/kintone/docs/',
// icon:'help_outline'
// },
]);
];
const domainLinks: EssentialLinkProps[] = [
{
title: 'ドメイン管理',
caption: 'kintoneのドメイン設定',
icon: 'domain',
link: '/#/domain',
target: '_self'
},
// {
// title: 'ドメイン適用',
// caption: 'ユーザー使用可能なドメインの設定',
// icon: 'assignment_ind',
// link: '/#/userDomain',
// target: '_self'
// },
];
const adminLinks: EssentialLinkProps[] = [
{
title: 'ユーザー管理',
caption: 'ユーザーを管理する',
icon: 'manage_accounts',
link: '/#/user',
target: '_self'
},
]
const version = process.env.version;
const productName = process.env.productName;
onMounted(() => {
authStore.setLeftMenu(!route.path.startsWith('/FlowChart/'));
authStore.toggleLeftMenu();
});
function toggleLeftDrawer() {
getCurrentInstance();
authStore.toggleLeftMenu();
}
function isAdmin(){
const permission = authStore.permissions;
return permission === 'admin'
}
</script>

View File

@@ -2,13 +2,13 @@
<div class="q-pa-md">
<div class="q-gutter-sm row items-start">
<q-breadcrumbs>
<q-breadcrumbs-el icon="widgets" label="アプリ" />
<q-breadcrumbs-el icon="widgets" label="アプリ管理" />
</q-breadcrumbs>
</div>
<q-table :rows="rows" :columns="columns" row-key="id" :filter="filter" :loading="loading" :pagination="pagination">
<q-table title="Treats" :rows="rows" :columns="columns" row-key="id" :filter="filter" :loading="loading" :pagination="pagination">
<template v-slot:top>
<q-btn color="primary" :disable="loading" label="新規" @click="showAddAppDialog" />
<q-btn disabled color="primary" :disable="loading" label="新規" @click="addRow" />
<q-space />
<q-input borderless dense filled debounce="300" v-model="filter" placeholder="検索">
<template v-slot:append>
@@ -16,11 +16,6 @@
</template>
</q-input>
</template>
<template v-slot:body-cell-name="prop">
<q-td :props="prop">
<q-btn flat dense :label="prop.row.name" @click="toEditFlowPage(prop.row)" ></q-btn>
</q-td>
</template>
<template v-slot:body-cell-url="prop">
<q-td :props="prop">
<a :href="prop.row.url" target="_blank" :title="prop.row.name" >
@@ -28,67 +23,39 @@
</a>
</q-td>
</template>
<template v-slot:body-cell-version="p">
<q-td :props="p">
<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 :props="p">
<q-btn-group flat>
<q-btn flat color="primary" padding="xs" size="1em" icon="edit_note" @click="editFlow(p.row)" />
<q-btn disabled flat color="primary" padding="xs" size="1em" icon="history" @click="showHistory(p.row)" />
<q-btn disabled flat color="negative" padding="xs" size="1em" icon="delete_outline" @click="removeRow(p.row)" />
</q-btn-group>
</q-td>
</template>
</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">
<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>
<app-select-box ref="appDialog" 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" />
<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>
</template>
<script setup lang="ts">
import { ref, onMounted, computed } from 'vue';
import { useAppStore } from 'stores/useAppStore';
import { ref, onMounted, watch, reactive } from 'vue';
import { api } from 'boot/axios';
import { useAuthStore } from 'stores/useAuthStore';
import { useFlowEditorStore } from 'stores/flowEditor';
import { router } from 'src/router';
import { IAppDisplay } from 'src/types/AppTypes';
import ShowDialog from 'src/components/ShowDialog.vue';
import AppSelectBox from 'src/components/AppSelectBox.vue';
import TableActionMenu from 'components/TableActionMenu.vue';
import { date } from 'quasar'
import { IManagedApp } from 'src/types/AppTypes';
interface IAppDisplay{
id:string;
name:string;
url:string;
user:string;
version:string;
updatetime:string;
}
const appStore = useAppStore();
const authStore = useAuthStore();
const numberStringSorting = (a: string, b: string) => parseInt(a, 10) - parseInt(b, 10);
@@ -96,95 +63,61 @@ const columns = [
{ name: 'id', label: 'アプリID', field: 'id', align: 'left', sortable: true, sort: numberStringSorting },
{ name: 'name', label: 'アプリ名', field: 'name', align: 'left', sortable: true },
{ name: 'url', label: 'URL', field: 'url', align: 'left', sortable: true },
{ name: 'updateUser', label: '最後更新者', field: '', align: 'left', sortable: true},
{ name: 'updateTime', label: '最後更新日', field: 'updateTime', align: 'left', sortable: true},
{ name: 'version', label: 'バージョン', field: '', align: 'left', sortable: true, style: 'max-width: 200px;',sort: numberStringSorting },
{ name: 'actions', label: '', field: 'actions' }
{ name: 'user', label: '最後更新者', field: 'user', align: 'left', sortable: true},
{ name: 'updatetime', label: '最後更新日', field: 'updatetime', align: 'left', sortable: true},
{ name: 'version', label: 'バージョン', field: 'version', align: 'left', sortable: true},
{ name: 'actions', label: '操作', field: 'actions' }
];
const pagination = ref({ sortBy: 'id', descending: true, rowsPerPage: 20 });
const loading = ref(false);
const filter = ref('');
const dgFilter = ref('');
const rows = computed(() => appStore.apps);
const targetRow = ref<IAppDisplay>();
const rows = ref<IAppDisplay[]>([]);
const store = useFlowEditorStore();
const appDialog = ref();
const showSelectApp=ref(false);
const isAdding = ref(false);
const deleteDialog = ref(false);
const deleteUserLoading = ref(false);
const actionList = [
{ label: 'フローの編集', icon: 'account_tree', action: toEditFlowPage },
{ label: 'バージョンの管理', icon: 'history', action: toVersionHistoryPage },
{ separator: true },
{ label: '削除', icon: 'delete_outline', class: 'text-red', action: removeRow },
];
const getApps = async () => {
loading.value = true;
await appStore.loadApps();
const result = await api.get('api/apps');
rows.value = result.data.map((item: IManagedApp) => {
return {
id: item.appid,
name: item.appname,
url: `${item.domainurl}/k/${item.appid}`,
user: `${item.user.first_name} ${item.user.last_name}` ,
updatetime:date.formatDate(item.update_time, 'YYYY/MM/DD HH:mm'),
version: Number(item.version)
}
}).sort((a: IAppDisplay, b: IAppDisplay) => numberStringSorting(a.id, b.id)); // set default order
loading.value = false;
}
function isVersionEditing(app: IAppDisplay) {
return !!app.versionChanged;
};
onMounted(async () => {
authStore.setLeftMenu(false);
await getApps();
});
const filterInitRows = (row: {id: string}) => {
return !appStore.rowIds.has(row.id);
watch(() => authStore.currentDomain.id, async () => {
await getApps();
});
const addRow = () => {
return
}
const showAddAppDialog = () => {
showSelectApp.value = true;
dgFilter.value = ''
const removeRow = (app:IAppDisplay) => {
return
}
const closeSelectAppDialog = async (val: 'OK'|'Cancel') => {
showSelectApp.value = true;
if (val == 'OK' && appDialog.value.selected[0]) {
isAdding.value = true;
toEditFlowPage(appDialog.value.selected[0]);
}
showSelectApp.value = false;
isAdding.value = false;
const showHistory = (app:IAppDisplay) => {
return
}
function removeRow(app:IAppDisplay) {
targetRow.value = app;
deleteDialog.value = true;
}
const deleteApp = async () => {
if (targetRow.value?.id) {
deleteUserLoading.value = true;
await appStore.deleteApp(targetRow.value)
await getApps();
deleteUserLoading.value = false;
deleteDialog.value = false;
}
}
async function toVersionHistoryPage(app:IAppDisplay) {
await router.push('/app/version/' + app.id).catch(err => {
console.error(err);
});
}
async function toEditFlowPage(app:IAppDisplay) {
const editFlow = (app:IAppDisplay) => {
store.setApp({
appId: app.id,
name: app.name
});
store.selectFlow(undefined);
await router.push('/FlowChart/' + app.id).catch(err => {
console.error(err);
});
router.push('/FlowChart/' + app.id);
};
</script>

View File

@@ -1,207 +0,0 @@
<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

@@ -16,10 +16,10 @@
<q-list>
<q-item clickable v-close-popup @click="onSaveVersion">
<q-item-section avatar >
<q-icon name="bookmark_border"></q-icon>
<q-icon name="history"></q-icon>
</q-item-section>
<q-item-section>
<q-item-label>保存して新バージョン</q-item-label>
<q-item-label>新バージョン保存</q-item-label>
</q-item-section>
</q-item>
@@ -51,10 +51,10 @@
@click="drawerLeft=!drawerLeft" class="expand" />
<q-breadcrumbs v-if="store.appInfo" class="fixed q-pl-md"
:style="{'left': fixedLeftPosition}">
<q-breadcrumbs-el icon="widgets" label="アプリ" to="/app" />
<q-breadcrumbs-el icon="widgets" label="アプリ管理" to="/app" />
<q-breadcrumbs-el>
<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 }}
<q-icon
class="q-ma-xs"
@@ -85,8 +85,8 @@
<action-select ref="appDg" name="model" :filter="filter" type="single" @clearFilter="onClearFilter" ></action-select>
</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 v-model:visible="saveVersionAction" name="新バージョン保存" @close="closeSaveVersionDg" min-width="500px">
<version-input v-model="versionInfo" />
</ShowDialog>
<q-inner-loading
:showing="initLoading"
@@ -99,12 +99,11 @@
<script setup lang="ts">
import { ref, reactive, computed, onMounted } from 'vue';
import { useRoute } from 'vue-router';
import { IActionNode, ActionNode, IActionFlow, ActionFlow, RootAction, IActionProperty, AppInfo } from 'src/types/ActionTypes';
import { IAppDisplay, IManagedApp, IVersionSubmit } from 'src/types/AppTypes';
import { IActionNode, ActionNode, IActionFlow, ActionFlow, RootAction, IActionProperty } from 'src/types/ActionTypes';
import { IManagedApp, IVersionInfo } from 'src/types/AppTypes';
import { storeToRefs } from 'pinia';
import { useFlowEditorStore } from 'stores/flowEditor';
import { useAuthStore } from 'stores/useAuthStore';
import { useAppStore } from 'stores/useAppStore';
import { api } from 'boot/axios';
import NodeItem from 'src/components/main/NodeItem.vue';
@@ -120,25 +119,23 @@ const deployLoading = ref(false);
const saveLoading = ref(false);
const initLoading = ref(true);
const drawerLeft = ref(false);
const versionSubmit = ref<IVersionSubmit>({} as IVersionSubmit);
const versionInfo = ref<IVersionInfo>();
const $q = useQuasar();
const store = useFlowEditorStore();
const authStore = useAuthStore();
const appStore = useAppStore();
const route = useRoute()
const appDg = ref();
const prevNodeIfo = ref({
prevNode: {} as IActionNode,
inputPoint: ''
inputPoint: ""
});
// const refFlow = ref<ActionFlow|null>(null);
const showAddAction = ref(false);
const saveVersionAction = ref(false);
const versionInputRef = ref();
const drawerRight = ref(false);
const filter=ref('');
const model = ref('');
const filter=ref("");
const model = ref("");
const rootNode = computed(()=>{
return store.currentFlow?.getRoot();
@@ -148,11 +145,11 @@ const minPanelWidth=computed(()=>{
if(store.currentFlow && root){
return store.currentFlow?.getColumns(root) * 300 + 'px';
}else{
return '300px';
return "300px";
}
});
const fixedLeftPosition = computed(()=>{
return drawerLeft.value?'300px':'0px';
return drawerLeft.value?"300px":"0px";
});
const addNode = (node: IActionNode, inputPoint: string) => {
@@ -195,12 +192,12 @@ const onDeleteAllNextNodes = (node: IActionNode) => {
store.currentFlow?.removeAllNext(node.id);
}
const closeDg = (val: any) => {
console.log('Dialog closed->', val);
console.log("Dialog closed->", val);
if (val == 'OK' && appDg?.value?.selected?.length > 0) {
const data = appDg.value.selected[0];
const actionProps = JSON.parse(data.property);
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);
}
}
@@ -227,57 +224,27 @@ const onDeploy = async () => {
if (store.appInfo === undefined || store.flows?.length === 0) {
$q.notify({
type: 'negative',
caption: 'エラー',
message: '設定されたフローがありません。'
caption: "エラー",
message: `設定されたフローがありません。`
});
return;
}
deployLoading.value = true;
try {
const { data }: {data: {code: string, groups: {code: string}[]}} = await api.get('api/v1/defaultgroup');
if (data.code === 'CB_WA01') {
$q.notify({
type: 'negative',
caption: 'エラー',
message: 'ユーザーのパスワード認証に失敗しました。'
});
deployLoading.value = false;
return;
} else if (!data.groups || !data.groups.some((group: {code: string}) => group.code === 'Administrators')){
$q.notify({
type: 'negative',
caption: 'エラー',
message: 'この操作には管理者権限が必要です。'
});
deployLoading.value = false;
return;
}
} catch (e) {
$q.notify({
type: 'negative',
caption: 'エラー',
message: 'サーバーに接続できませんでした。'
});
deployLoading.value = false;
return;
}
try {
deployLoading.value = true;
await store.deploy();
deployLoading.value = false;
$q.notify({
type: 'positive',
caption: '通知',
message: 'デプロイを成功しました。'
caption: "通知",
message: `デプロイを成功しました。`
});
} catch (error) {
console.error(error);
deployLoading.value = false;
$q.notify({
type: 'negative',
caption: 'エラー',
message: 'デプロイが失敗しました。'
caption: "エラー",
message: `デプロイが失敗しました。`
})
}
return;
@@ -288,30 +255,23 @@ const onSaveActionProps=(props:IActionProperty[])=>{
store.activeNode.actionProps=props;
$q.notify({
type: 'positive',
caption: '通知',
caption: "通知",
message: `${store.activeNode?.subTitle}の属性を設定しました。(保存はされていません)`
});
}
};
const onSaveVersion = async () => {
if (!store.appInfo) return;
versionSubmit.value = { appId: store.appInfo.appId }
versionInfo.value = {
id: '1' // TODO
}
saveVersionAction.value = true;
// await onSaveAllFlow();
}
const closeSaveVersionDg = async (val: 'OK'|'CANCEL') => {
saveVersionAction.value = true;
const closeSaveVersionDg = (val: 'OK'|'CANCEL') => {
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;
console.log(versionInfo.value);
}
}
@@ -321,7 +281,7 @@ const onSaveFlow = async () => {
$q.notify({
type: 'negative',
caption: 'エラー',
message: '選択中のフローがありません。'
message: `選択中のフローがありません。`
});
return;
}
@@ -331,7 +291,7 @@ const onSaveFlow = async () => {
saveLoading.value = false;
$q.notify({
type: 'positive',
caption: '通知',
caption: "通知",
message: `${targetFlow.getRoot()?.subTitle}のフロー設定を保存しました。`
});
} catch (error) {
@@ -339,7 +299,7 @@ const onSaveFlow = async () => {
saveLoading.value = false;
$q.notify({
type: 'negative',
caption: 'エラー',
caption: "エラー",
message: `${targetFlow.getRoot()?.subTitle}のフローの設定の保存が失敗しました。`
})
}
@@ -354,7 +314,7 @@ const onSaveAllFlow= async ()=>{
$q.notify({
type: 'negative',
caption: 'エラー',
message: '設定されたフローがありません。'
message: `設定されたフローがありません。`
});
return;
}
@@ -368,8 +328,8 @@ const onSaveAllFlow= async ()=>{
}
$q.notify({
type: 'positive',
caption: '通知',
message: 'すべてのフロー設定を保存しました。'
caption: "通知",
message: `すべてのフロー設定を保存しました。`
});
saveLoading.value = false;
}catch (error) {
@@ -377,8 +337,8 @@ const onSaveAllFlow= async ()=>{
saveLoading.value = false;
$q.notify({
type: 'negative',
caption: 'エラー',
message: 'フローの設定の保存が失敗しました。'
caption: "エラー",
message: `フローの設定の保存が失敗しました。`
});
}
}
@@ -386,9 +346,11 @@ const onSaveAllFlow= async ()=>{
const fetchData = async () => {
initLoading.value = true;
if (store.appInfo === undefined && route?.params?.id !== undefined) {
// only for page refreshed
const app = await fetchAppById(route.params.id as string);
store.setApp(app);
const { appid, appname } = await fetchAppById(route.params.id as string);
store.setApp({
appId: appid,
name: appname
});
};
await store.loadFlow();
initLoading.value = false
@@ -396,46 +358,17 @@ const fetchData = async () => {
}
const fetchAppById = async(id: string) => {
let result = await api.get('api/apps');
const app = result.data?.data?.find((item: IManagedApp) => item.appid === id ) as IManagedApp;
if (app) {
return convertManagedAppToAppInfo(app);
}
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;
const result = await api.get('api/apps');
return result.data.find((item: IManagedApp) => item.appid === id ) as IManagedApp;
}
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=()=>{
filter.value='';
}
onMounted(async () => {
onMounted(() => {
authStore.setLeftMenu(false);
await fetchData();
fetchData();
});
</script>

View File

@@ -27,23 +27,23 @@ import ActionSelect from 'components/ActionSelect.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 saibanProps:IActionProperty[]=[{
component:'InputText',
component:"InputText",
props:{
displayName:'フォーマット',
modelValue:'',
name:'format',
placeholder:'フォーマットを入力してください',
displayName:"フォーマット",
modelValue:"",
name:"format",
placeholder:"フォーマットを入力してください",
}
},{
component:'FieldInput',
component:"FieldInput",
props:{
displayName:'採番項目',
modelValue:'',
name:'field',
placeholder:'採番項目を選択してください',
displayName:"採番項目",
modelValue:"",
name:"field",
placeholder:"採番項目を選択してください",
}
}];
@@ -91,7 +91,7 @@ const onDeleteAllNextNodes=(node:IActionNode)=>{
refFlow.value.removeAllNext(node.id);
}
const closeDg=(val :any)=>{
console.log('Dialog closed->',val);
console.log("Dialog closed->",val);
}
</script>

View File

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

View File

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

View File

@@ -1,250 +0,0 @@
<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[];
}
const props = withDefaults(defineProps<Props>(), {
title: 'ルールエディター',
actions: () => ['フィールド制御', '一覧画面', 'その他']
title: "ルールエディター",
actions: () => ["フィールド制御", "一覧画面", "その他"]
});
function onItemClick(evt: Event) {
return;

View File

@@ -2,13 +2,13 @@
<div class="q-pa-md">
<div class="q-gutter-sm row items-start">
<q-breadcrumbs>
<q-breadcrumbs-el icon="domain" label="接続先管理" />
<q-breadcrumbs-el icon="domain" label="ドメイン管理" />
</q-breadcrumbs>
</div>
<q-table :rows="rows" :columns="columns" row-key="id" :filter="filter" :loading="loading" :pagination="pagination">
<q-table title="Treats" :rows="rows" :columns="columns" row-key="id" :filter="filter" :loading="loading" :pagination="pagination">
<template v-slot:top>
<q-btn v-permissions="Actions.domain.add" color="primary" :disable="loading" label="新規" @click="addRow" />
<q-btn color="primary" :disable="loading" label="新規" @click="addRow" />
<q-space />
<q-input borderless dense filled debounce="300" v-model="filter" placeholder="検索">
<template v-slot:append>
@@ -17,30 +17,12 @@
</q-input>
</template>
<template v-slot:header-cell-active="p">
<q-th auto-width :props="p">
<q-select class="filter-header" v-model="activeFilter" :options="activeOptions" @update:model-value="activeFilterUpdate" borderless
dense options-dense hide-bottom-space/>
</q-th>
</template>
<template v-slot:body-cell-active="p">
<q-td auto-width :props="p">
<q-badge v-if="!p.row.domainActive" color="grey">未使用</q-badge>
<q-badge v-if="p.row.id == currentDomainId" color="primary">既定</q-badge>
</q-td>
</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">
<q-td :props="p">
<table-action-menu :row="p.row" :actions="actionList" />
<q-btn-group flat>
<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>
</template>
@@ -53,86 +35,60 @@
<div class="text-h6 q-ma-sm">Kintone Account</div>
</q-card-section>
<q-card-section class="q-py-none q-mt-none">
<q-card-section class="q-pt-none q-mt-none">
<div class="q-gutter-lg">
<q-input filled v-model="tenantid" label="テナントID" hint="テナントIDを入力してください。" lazy-rules
:rules="[val => val && val.length > 0 || 'テナントIDを入力してください。']" />
<q-input filled v-model="name" label="環境名 *" hint="kintoneの環境名を入力してください" lazy-rules
:rules="[val => val && val.length > 0 || 'kintoneの環境名を入力してください。']" />
<q-input filled v-model.trim="url" :readonly="!isCreate" label="Kintone url" :hint="isCreate ? 'KintoneのURLを入力してください':'KintoneのURLは変更できません。新規作成してください。'" lazy-rules
:rules="[val => val && val.length > 0 || 'KintoneのURLを入力してください']" />
<q-input filled type="url" v-model="url" label="Kintone url" hint="KintoneのURLを入力してください" lazy-rules
:rules="[val => val && val.length > 0, isDomain || 'KintoneのURLを入力してください']" />
<q-input v-if="isCreate" filled v-model="kintoneuser" label="ログイン名 *" hint="Kintoneのログイン名を入力してください" lazy-rules
<q-input filled v-model="kintoneuser" label="ログイン名 *" hint="Kintoneのログイン名を入力してください" lazy-rules
:rules="[val => val && val.length > 0 || 'Kintoneのログイン名を入力してください']" autocomplete="new-password" />
<q-input v-if="isCreate" v-model="kintonepwd" filled :type="isPwd ? 'password' : 'text'"
hint="Kintoneのパスワードを入力してください" label="パスワード" :disable="!isCreate" lazy-rules
:rules="[val => val && val.length > 0 || 'Kintoneのパスワードを入力してください']" autocomplete="new-password">
hint="パスワード" label="パスワード" :disable="!isCreate" lazy-rules
:rules="[val => val && val.length > 0 || 'Please type something']" autocomplete="new-password">
<template v-slot:append>
<q-icon :name="isPwd ? 'visibility_off' : 'visibility'" class="cursor-pointer"
@click="isPwd = !isPwd" />
</template>
</q-input>
<q-item tag="label" class="q-pl-sm q-pr-none q-py-xs">
<q-item-section>
<q-item-label>接続先の有効化</q-item-label>
</q-item-section>
<q-item-section avatar>
<q-toggle v-model="domainActive" />
</q-item-section>
</q-item>
<div class="q-gutter-y-md" v-if="!isCreate">
<q-separator />
<q-item tag="label" class="q-pl-sm q-pr-none q-py-xs">
<q-item-section>
<q-item-label>管理者アカウントの変更</q-item-label>
<q-item-label>パスワードリセット</q-item-label>
</q-item-section>
<q-item-section avatar>
<q-toggle v-model="changeAccount" @update:model-value="updateChangeAccount" />
<q-toggle v-model="resetPsw" @update:model-value="updateResetPsw" />
</q-item-section>
</q-item>
<q-expansion-item
header-class="hidden"
class="q-mt-none"
v-model="changeAccount"
>
<q-input filled v-model="kintoneuser" label="ログイン名 *" hint="Kintoneのログイン名を入力してください" :disable="!changeAccount"
:rules="[val => val && val.length > 0 || 'Kintoneのログイン名を入力してください']" lazy-rules class="q-mt-md"/>
<q-input v-model="kintonepwd" filled :type="isPwd ? 'password' : 'text'" hint="Kintoneのパスワードを入力してください"
label="パスワード" :disable="!changeAccount" lazy-rules class="q-mt-lg q-mb-md"
:rules="[val => val && val.length > 0 || 'Kintoneのパスワードを入力してください']" autocomplete="new-password">
<template v-slot:append>
<q-icon :name="isPwd ? 'visibility_off' : 'visibility'" class="cursor-pointer"
@click="isPwd = !isPwd" />
</template>
</q-input>
<!-- <q-btn label="asdf"/> -->
</q-expansion-item>
<q-input v-model="kintonepwd" filled :type="isPwd ? 'password' : 'text'" hint="パスワードを入力してください"
label="パスワード" :disable="!resetPsw" lazy-rules
:rules="[val => val && val.length > 0 || 'Please type something']" autocomplete="new-password">
<template v-slot:append>
<q-icon :name="isPwd ? 'visibility_off' : 'visibility'" class="cursor-pointer"
@click="isPwd = !isPwd" />
</template>
</q-input>
<!-- <q-btn label="asdf"/> -->
</div>
</div>
</q-card-section>
<div class="q-mt-none">
<q-banner dense :class="['text-white q-mt-sm q-py-xs q-mx-md', testResult?.success ? 'bg-green':'bg-red', testResult?.msg ? '':'invisible']">
{{ testResult?.msg }}
</q-banner>
<q-card-actions class="text-primary q-mb-md q-mx-sm relative-position">
<q-btn v-if="isCreate || changeAccount" :disable="!isConnectable" :loading="!!connectLoading" padding="xs md" :label="!!connectLoading ? '確認中...' : '接続確認'"
color="primary" outline @click="tryConnect()"/>
<q-btn v-if="!!connectLoading" label="停止" color="negative" flat class="q-ml-sm" @click="stopConnect" />
<q-space />
<q-btn :loading="addEditLoading" padding="xs md" label="保存" type="submit" color="primary" />
<q-btn label="キャンセル" type="cancel" color="primary" flat class="q-ml-sm" @click="closeDg()" />
</q-card-actions>
</div>
<q-card-actions align="right" class="text-primary q-mb-md q-mx-sm">
<q-btn label="保存" type="submit" color="primary" />
<q-btn label="キャンセル" type="cancel" color="primary" flat class="q-ml-sm" @click="closeDg()" />
</q-card-actions>
</q-form>
</q-card>
@@ -140,161 +96,70 @@
<q-dialog v-model="confirm" persistent>
<q-card>
<!-- -1 loading -->
<q-card-section v-if="deleteLoadingState == -1" class="row items-center">
<q-spinner color="primary" size="2em"/>
<span class="q-ml-sm">接続先利用権限を確認中</span>
</q-card-section>
<!-- > 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-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="キャンセル" color="primary" v-close-popup />
<!-- > 0 can't delete -->
<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-btn flat label="Cancel" color="primary" v-close-popup />
<q-btn flat label="OK" color="primary" v-close-popup @click="deleteDomain()" />
</q-card-actions>
</q-card>
</q-dialog>
<share-usage-dialog v-model="shareDg" :domain="shareDomain" @close="shareDg = false" />
<share-manage-dialog v-model="shareManageDg" :domain="shareDomain" @close="shareManageDg = false" />
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, computed } from 'vue';
import { ref, onMounted, reactive } from 'vue';
import { api } from 'boot/axios';
import { Actions } from 'boot/permissions';
import { useAuthStore } from 'stores/useAuthStore';
import ShareUsageDialog from 'components/ShareDomain/ShareUsageDialog.vue';
import ShareManageDialog from 'components/ShareDomain/ShareManageDialog.vue';
import TableActionMenu from 'components/TableActionMenu.vue';
import { IDomain, IDomainDisplay, IDomainOwnerDisplay, IDomainSubmit } from '../types/DomainTypes';
const authStore = useAuthStore();
const inactiveRowClass = (row: IDomainOwnerDisplay) => row.domainActive ? '' : 'inactive-row';
const columns = [
{ name: 'id', label: 'ID', field: 'id', align: 'left', sortable: true, classes: inactiveRowClass },
// {
// name: 'tenantid',
// required: true,
// label: 'テナントID',
// field: 'tenantid',
// align: 'left',
// sortable: true,
// classes: inactiveRowClass
// },
{ name: 'name', label: '環境名', field: 'name', align: 'left', sortable: true, classes: inactiveRowClass },
{ name: 'active', label: '', align: 'left', field: 'domainActive', classes: inactiveRowClass },
{ name: 'url', label: 'URL', field: 'url', align: 'left', sortable: true, classes: inactiveRowClass },
{ name: 'user', label: 'ログイン名', field: 'user', align: 'left', classes: inactiveRowClass },
{ name: 'owner', label: '所有者', field: '', align: 'left', classes: inactiveRowClass },
{ name: 'actions', label: '', field: 'actions', classes: inactiveRowClass }
{ name: 'id', label: 'ID', field: 'id', align: 'left', sortable: true },
{
name: 'tenantid',
required: true,
label: 'テナントID',
field: row => row.tenantid,
format: val => `${val}`,
align: 'left',
sortable: true
},
{ name: 'name', label: '環境名', field: 'name', align: 'left', sortable: true },
{ name: 'url', label: 'URL', field: 'url', align: 'left', sortable: true },
{ name: 'user', label: 'ログイン名', field: 'user', align: 'left', },
{ name: 'actions', label: '操作', field: 'actions' }
];
const pagination = ref({ sortBy: 'id', descending: true, rowsPerPage: 20 });
const loading = ref(false);
const addEditLoading = ref(false);
const deleteLoadingState = ref<number>(-1); // -2: deleteLoading, -1: loading, 0: allow, > 0: user count
const connectLoading = ref<AbortController|null>(null);
const filter = ref('');
const rows = ref<IDomainOwnerDisplay[]>([]);
const rows = ref([]);
const show = ref(false);
const confirm = ref(false);
const changeAccount = ref(false);
const testResult = ref<{msg: string, success?: boolean}|null>(null);
const resetPsw = ref(false);
const currentDomainId = computed(() => authStore.currentDomain.id);
// const tenantid = ref(authStore.currentDomain.id);
const tenantid = ref(authStore.currentDomain.id);
const name = ref('');
const url = ref('');
const isPwd = ref(true);
const kintoneuser = ref('');
const kintoneuserBK = ref('');
const kintonepwd = ref('');
const domainActive = ref(true);
const kintonepwdBK = ref('');
const isCreate = ref(true);
let editId = ref(0);
const shareDg = ref(false);
const shareManageDg = ref(false);
const shareDomain = ref<IDomainOwnerDisplay>({} as IDomainOwnerDisplay);
const activeOptions = [
{ value: 0, label: 'すべて' },
{ value: 1, label: '使用' },
{ value: 2, label: '未使用'}
]
const activeFilter = ref(activeOptions[0]);
const activeFilterUpdate = (option: {value: number}) => {
switch (option.value) {
case 1:
getDomain((row) => row.domainActive)
break;
case 2:
getDomain((row) => !row.domainActive)
break;
default:
getDomain()
break;
}
}
const SHARE_USE = 'use';
const SHARE_MANAGE = 'manage';
const actionList = [
{ label: '編集', icon: 'edit_note', permission: Actions.domain.edit, action: editRow },
{ 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 },
{ 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 () => {
loading.value = true;
const { data } = await api.get<{data:IDomain[]}>('api/domains');
rows.value = data.data.map((item) => {
return {
id: item.id,
tenantid: item.tenantid,
domainActive: item.is_active,
name: item.name,
url: item.url,
user: item.kintoneuser,
password: item.kintonepwd,
owner: {
id: item.owner.id,
firstName: item.owner.first_name,
lastName: item.owner.last_name,
fullNameSearch: (item.owner.last_name + item.owner.first_name).toLowerCase(),
fullName: item.owner.last_name + ' ' + item.owner.first_name,
email: item.owner.email,
isActive: item.owner.is_active,
isSuperuser: item.owner.is_superuser,
}
}
}).filter(filter || (() => true));
const userId = authStore.userId;
const result = await api.get(`api/domain?userId=${userId}`);
rows.value = result.data.map((item) => {
return { id: item.id, tenantid: item.tenantid, name: item.name, url: item.url, user: item.kintoneuser, password: item.kintonepwd }
});
loading.value = false;
}
@@ -309,50 +174,38 @@ const addRow = () => {
show.value = true;
}
async function removeRow(row: IDomainOwnerDisplay) {
const removeRow = (row) => {
confirm.value = true;
deleteLoadingState.value = -1;
editId.value = row.id;
const { data } = await api.get(`/api/domainshareduser/${row.id}`);
deleteLoadingState.value = data.data.length;
}
const deleteDomain = () => {
deleteLoadingState.value = -2;
api.delete(`api/domain/${editId.value}`).then(({ data }) => {
if (!data.data) {
// TODO dialog
}
confirm.value = false;
deleteLoadingState.value = -1;
api.delete(`api/domain/${editId.value}`).then(() => {
getDomain();
// authStore.setCurrentDomain();
})
editId.value = 0; // set in removeRow()
editId.value = 0;
};
function editRow(row: any) {
const editRow = (row) => {
isCreate.value = false
editId.value = row.id;
// tenantid.value = row.tenantid;
tenantid.value = row.tenantid;
name.value = row.name;
url.value = row.url;
kintoneuser.value = row.user;
kintoneuserBK.value = row.user;
kintonepwd.value = ''
domainActive.value = row.domainActive;
kintonepwd.value = row.password;
isPwd.value = true;
show.value = true;
};
const updateChangeAccount = () => {
kintoneuser.value = kintoneuserBK.value;
kintonepwd.value = ''
connectLoading.value = null
isPwd.value = true
stopConnect();
testResult.value = null;
const updateResetPsw = (value: boolean) => {
if (value === true) {
kintonepwd.value = ''
isPwd.value = true
} else {
kintonepwd.value = kintonepwdBK.value
}
}
const closeDg = () => {
@@ -360,130 +213,46 @@ const closeDg = () => {
onReset();
}
const tryConnect = async (isTest = true) => {
testResult.value = null;
if (isTest) {
connectLoading.value = new AbortController();
}
try {
const { data }: {data: {code: string, groups: {code: string}[]}} = await api.get('api/v1/group', {
params:{
kintoneurl: ensureHttps(url.value),
kintoneuser: kintoneuser.value,
kintonepwd: kintonepwd.value,
},
signal: (isTest && connectLoading.value) ? connectLoading.value.signal : undefined
});
if (data.code === 'CB_WA01') {
testResult.value = { msg: 'ユーザーのパスワード認証に失敗しました。' }
} else if (data.groups && data.groups.some((group: {code: string}) => group.code === 'Administrators')){
testResult.value = { success: true, msg: isTest ? 'kintoneの管理者アカウントで接続に成功しました。' : '' }
return true;
} else {
testResult.value = { msg: 'このアカウントはkintoneの管理者アカウントではありません。' }
}
return false;
} catch (e) {
const error = e as { code?: string };
if (error.code === 'ERR_CANCELED') {
console.log('Aborted');
} else {
testResult.value = { msg: 'サーバーに接続できませんでした。' }
throw error;
}
return false;
} finally {
connectLoading.value = null;
}
}
const ensureHttps = (url: string) => {
return !/^https?:\/\//i.test(url) ? 'https://' + url : url;
}
const stopConnect = () => {
if (!connectLoading.value) return;
connectLoading.value?.abort();
connectLoading.value = null;
}
const onSubmit = async () => {
addEditLoading.value = true;
try {
await tryConnect(false);
} catch (e) {
addEditLoading.value = false;
return;
}
const method = editId.value !== 0 ? 'put' : 'post';
const param: IDomainSubmit = {
'id': editId.value,
'tenantid': '1', // TODO: テナントIDを取得する
'name': name.value,
'url': url.value,
'kintoneuser': kintoneuser.value,
'kintonepwd': ((isCreate.value && editId.value == 0) || changeAccount.value) ? kintonepwd.value : '',
'is_active': domainActive.value,
'ownerid': authStore.userId || ''
}
// for search: api.put(`api/domain`)、api.post(`api/domain`)
api[method].apply(api, ['api/domain', param]).then(async (resp: any) => {
const res = resp.data;
if (res.data.id === currentDomainId.value && !res.data.is_active) {
await authStore.setCurrentDomain();
}
const onSubmit = () => {
if (editId.value !== 0) {
api.put(`api/domain`, {
'id': editId.value,
'tenantid': tenantid.value,
'name': name.value,
'url': url.value,
'kintoneuser': kintoneuser.value,
'kintonepwd': isCreate.value || resetPsw.value ? kintonepwd.value : ''
}).then(() => {
getDomain();
closeDg();
onReset();
addEditLoading.value = false;
})
}
else {
api.post(`api/domain`, {
'id': 0,
'tenantid': tenantid.value,
'name': name.value,
'url': url.value,
'kintoneuser': kintoneuser.value,
'kintonepwd': kintonepwd.value
}).then(() => {
getDomain();
closeDg();
onReset();
})
}
}
function openShareDg(type: typeof SHARE_MANAGE|typeof SHARE_USE, row: IDomainOwnerDisplay|number) {
if (typeof row === 'number') {
row = rows.value.find(item => item.id === row) as IDomainOwnerDisplay;
}
shareDomain.value = row;
if (type === SHARE_USE) {
shareDg.value = true;
} else if (type === SHARE_MANAGE) {
shareManageDg.value = true;
}
};
const isConnectable = computed(()=> {
return url.value && kintoneuser.value && kintonepwd.value
})
const onReset = () => {
name.value = '';
url.value = '';
kintoneuser.value = '';
kintoneuserBK.value = '';
kintonepwd.value = '';
isPwd.value = true;
editId.value = 0;
isCreate.value = true;
domainActive.value = true;
changeAccount.value = false;
addEditLoading.value = false;
connectLoading.value = null
testResult.value = null;
resetPsw.value = false
}
</script>
<style lang="scss">
.filter-header .q-field__native {
font-size: 12px;
font-weight: 500;
}
.filter-header .q-icon {
width: 12px;
}
.q-table td.inactive-row {
color: #aaa;
background-color: #fafafa;
}
.q-table tr > td.inactive-row:last-child {
color: inherit;
}
</style>

View File

@@ -1,20 +1,29 @@
<template>
<div class="q-pa-md">
<div class="q-pa-lg">
<div class="q-gutter-sm row items-start">
<q-breadcrumbs>
<q-breadcrumbs-el icon="assignment_ind" label="接続先の割り当て" />
<q-breadcrumbs-el icon="assignment_ind" label="ドメイン適用" />
</q-breadcrumbs>
</div>
<q-table :loading="initLoading" grid grid-header title="Domain" selection="single" :rows="rows" :columns="columns" row-key="name"
<q-table grid grid-header title="Domain" selection="single" :rows="rows" :columns="columns" row-key="name"
:filter="userDomainTableFilter" virtual-scroll v-model:pagination="pagination">
<template v-slot:top>
<q-btn class="q-mx-none" color="primary" label="追加" @click="clickAddDomain()" />
<q-space />
<div class="row q-gutter-md">
<q-input borderless dense filled debounce="300" v-model="userDomainTableFilter" placeholder="検索">
<q-item v-if="authStore.permissions === 'admin'" tag="label" dense @click="clickSwitchUser()">
<q-item-section>
<q-item-label>適用するユーザ : </q-item-label>
</q-item-section>
<q-item-section avatar>
{{ currentUserName }}
</q-item-section>
</q-item>
<q-input borderless dense filled debounce="300" v-model="userDomainTableFilter" placeholder="Search">
<template v-slot:append>
<q-icon name="search" />
</template>
@@ -29,25 +38,67 @@
<template v-slot:item="props">
<div class="q-pa-sm">
<domain-card :item="props.row" :active-id="activeDomainId">
<template v-slot:actions>
<q-card-actions align="right">
<q-chip class="no-border" v-if="isActive(props.row.id)" outline color="primary" text-color="white" icon="done">
既定
</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 :disable="activeDomainLoadingId === props.row.id" :loading="deleteDomainLoadingId === props.row.id" @click="clickDeleteConfirm(props.row)">
削除
</q-btn>
</q-card-actions>
</template>
</domain-card>
<q-card>
<q-card-section>
<div class="q-table__grid-item-row">
<div class="q-table__grid-item-title">Domain</div>
<div class="q-table__grid-item-value">{{ props.row.name }}</div>
</div>
<div class="q-table__grid-item-row">
<div class="q-table__grid-item-title">URL</div>
<div class="q-table__grid-item-value" style="width: 22rem;">{{ props.row.url }}</div>
</div>
<div class="q-table__grid-item-row">
<div class="q-table__grid-item-title">Account</div>
<div class="q-table__grid-item-value">{{ props.row.kintoneuser }}</div>
</div>
</q-card-section>
<q-separator />
<q-card-actions align="right">
<div style="width: 98%;">
<div class="row items-center justify-between">
<div class="q-table__grid-item-value"
:class="isActive(props.row.id) ? 'text-positive' : 'text-negative'">{{
isActive(props.row.id)?'既定':'' }}</div>
<div class="col-auto">
<q-btn v-if="!isActive(props.row.id)" flat
@click="activeDomain(props.row.id)">既定にする</q-btn>
<q-btn flat @click="clickDeleteConfirm(props.row)">削除</q-btn>
</div>
</div>
</div>
</q-card-actions>
</q-card>
</div>
</template>
</q-table>
<show-dialog v-model:visible="showAddDomainDg" name="ドメイン" @close="addUserDomainFinished" :ok-btn-loading="addUserDomainLoading" :ok-btn-auto-close="false">
<domain-select ref="addDomainRef" name="ドメイン" type="single" :filterInitRowsFunc="filterAddDgInitRows"></domain-select>
<show-dialog v-model:visible="showAddDomainDg" name="ドメイン" @close="addUserDomainFinished">
<domain-select ref="addDomainRef" name="ドメイン" type="multiple"></domain-select>
</show-dialog>
<show-dialog v-model:visible="showSwitchUserDd" name="ドメイン" minWidth="35vw" @close="switchUserFinished">
<template v-slot:toolbar>
<q-input dense placeholder="検索" v-model="switchUserFilter">
<template v-slot:append>
<q-icon name="search" />
</template>
</q-input>
</template>
<div class="q-gutter-md">
<q-item tag="label" class="q-pl-sm q-pr-none q-py-xs">
<q-item-section>
<q-item-label>他のユーザーを選択する</q-item-label>
</q-item-section>
<q-item-section avatar>
<q-toggle v-model="useOtherUser" />
</q-item-section>
</q-item>
<div v-if="useOtherUser">
<user-list ref="switchUserRef" name="ドメイン" :filter="switchUserFilter" />
</div>
</div>
</show-dialog>
<q-dialog v-model="showDeleteConfirm" persistent>
@@ -72,64 +123,49 @@
import { ref, onMounted, computed } from 'vue'
import { api } from 'boot/axios';
import { useAuthStore } from 'stores/useAuthStore';
import { IDomainOwnerDisplay } from '../types/DomainTypes';
import ShowDialog from 'components/ShowDialog.vue';
import DomainCard from 'components/UserDomain/DomainCard.vue';
import DomainSelect from 'components/DomainSelect.vue';
import UserList from 'components/UserList.vue';
const authStore = useAuthStore();
const pagination = ref({ sortBy: 'id', rowsPerPage: 0 });
const rows = ref<IDomainOwnerDisplay[]>([]);
const rowIds = new Set<string>();
const initLoading = ref(true);
const addUserDomainLoading = ref(false);
const activeDomainLoadingId = ref<number|undefined>(undefined);
const deleteDomainLoadingId = ref<number|undefined>(undefined);
const rows = ref([] as any[]);
const columns = [
{ name: 'id' },
{ name: 'name', required: true, label: 'Name', align: 'left', field: 'name', sortable: true },
{ name: 'url', align: 'center', label: 'Domain', field: 'url', sortable: true },
{ name: 'kintoneuser', label: 'User', field: 'user', sortable: true },
{ name: 'kintoneuser', label: 'User', field: 'kintoneuser', sortable: true },
{ name: 'kintonepwd' },
{ name: 'active', field: 'active' }
];
const userDomainTableFilter = ref();
const currentUserName = ref('');
const useOtherUser = ref(false);
const otherUserId = ref('');
let editId = ref(0);
const showAddDomainDg = ref(false);
const addDomainRef = ref();
const filterAddDgInitRows = (row: {domainActive: boolean, id: string}) => {
return row.domainActive && !rowIds.has(row.id);
}
const clickAddDomain = () => {
editId.value = 0;
showAddDomainDg.value = true;
};
const addUserDomainFinished = async (val: string) => {
showAddDomainDg.value = true;
const selected = addDomainRef.value.selected;
if (val == 'OK' && selected.length > 0) {
addUserDomainLoading.value = true;
const { data } = await api.post('api/userdomain', {
userid: authStore.userId,
domainid: selected[0].id,
});
if (rows.value.length === 0 && data.data) {
const domain = data.data;
await authStore.setCurrentDomain({
id: domain.id,
kintoneUrl: domain.url,
domainName: domain.name
});
const addUserDomainFinished = (val: string) => {
if (val == 'OK') {
let dodmainids = [];
let domains = JSON.parse(JSON.stringify(addDomainRef.value.selected));
for (var key in domains) {
dodmainids.push(domains[key].id);
}
await getDomain();
api.post(`api/domain/${useOtherUser.value ? otherUserId.value : authStore.userId}`, dodmainids)
.then(() => { getDomain(useOtherUser.value ? otherUserId.value : undefined); });
}
addUserDomainLoading.value = false;
showAddDomainDg.value = false;
};
const showDeleteConfirm = ref(false);
@@ -139,26 +175,16 @@ const clickDeleteConfirm = (row: any) => {
editId.value = row.id;
};
const deleteDomainFinished = async () => {
deleteDomainLoadingId.value = editId.value;
const { data } = await api.delete(`api/domain/${editId.value}/${authStore.userId}`)
if (data.msg == 'OK' && authStore.currentDomain.id === editId.value) {
authStore.setCurrentDomain();
}
const deleteDomainFinished = () => {
api.delete(`api/domain/${editId.value}/${useOtherUser.value ? otherUserId.value : authStore.userId}`).then(() => {
getDomain(useOtherUser.value ? otherUserId.value : undefined);
})
editId.value = 0;
await getDomain();
deleteDomainLoadingId.value = undefined;
};
const activeDomain = async (domain: any) => {
activeDomainLoadingId.value = domain.id;
await authStore.setCurrentDomain({
id: domain.id,
kintoneUrl: domain.url,
domainName: domain.name
});
await getDomain();
activeDomainLoadingId.value = undefined;
const activeDomain = (id: number) => {
api.put(`api/activedomain/${id}${useOtherUser.value ? `?userId=${otherUserId.value}` : ''}`)
.then(() => { getDomain(useOtherUser.value ? otherUserId.value : undefined); })
};
let activeDomainId = ref(0);
@@ -167,54 +193,44 @@ const isActive = computed(() => (id: number) => {
return id == activeDomainId.value;
});
const getDomain = async (userId? : string) => {
rowIds.clear();
const resp = await api.get('api/defaultdomain');
activeDomainId.value = resp?.data?.data?.id;
const domainResult = userId ? await api.get(`api/domain?userId=${userId}`) : await api.get('api/domain');
const domains = domainResult.data as any[];
rows.value = domains.sort((a, b) => a.id - b.id).reduce((acc, item) => {
rowIds.add(item.id);
if (item.is_active) {
acc.push({
id: item.id,
tenantid: item.tenantid,
domainActive: item.is_active,
name: item.name,
url: item.url,
user: item.kintoneuser,
password: item.kintonepwd,
owner: {
id: item.owner.id,
firstName: item.owner.first_name,
lastName: item.owner.last_name,
fullNameSearch: (item.owner.last_name + item.owner.first_name).toLowerCase(),
fullName: item.owner.last_name + ' ' + item.owner.first_name,
email: item.owner.email,
isActive: item.owner.is_active,
isSuperuser: item.owner.is_superuser,
}
})
}
return acc;
}, []);
const showSwitchUserDd = ref(false);
const switchUserRef = ref();
const switchUserFilter = ref('')
const clickSwitchUser = () => {
showSwitchUserDd.value = true;
useOtherUser.value = false;
};
const switchUserFinished = async (val: string) => {
if (val == 'OK') {
if (useOtherUser.value) {
const user = switchUserRef.value.selected[0]
currentUserName.value = user.email;
otherUserId.value = user.id
await getDomain(user.id)
} else {
currentUserName.value = authStore.userInfo.email
await getDomain();
}
}
};
const getDomain = async (userId? : string) => {
const resp = await api.get(`api/activedomain${useOtherUser.value ? `?userId=${otherUserId.value}` : ''}`);
activeDomainId.value = resp?.data?.id;
const domainResult = userId ? await api.get(`api/domain?userId=${userId}`) : await api.get(`api/domain`);
const domains = domainResult.data as any[];
rows.value = domains.map((item) => {
return { id: item.id, name: item.name, url: item.url, kintoneuser: item.kintoneuser, kintonepwd: item.kintonepwd }
});
}
onMounted(async () => {
initLoading.value = true;
currentUserName.value = authStore.userInfo.email
await getDomain();
initLoading.value = false;
})
</script>
<style lang="scss" scoped>
.domain-card {
width: 22rem;
word-break: break-word;
.smaller-font-size {
font-size: 13px;
}
}
</style>

View File

@@ -9,7 +9,7 @@
:pagination="pagination" >
<template v-slot:top>
<q-btn v-permissions="Actions.user.add" color="primary" :disable="loading" label="新規" @click="addRow" />
<q-btn color="primary" :disable="loading" label="新規" @click="addRow" />
<q-space />
<q-input borderless dense filled debounce="300" v-model="filter" placeholder="検索">
<template v-slot:append>
@@ -27,18 +27,11 @@
<div v-else>
<q-chip square color="negative" text-color="white" icon="block" label="使用不可" size="sm" />
</div>
</div>
</q-td>
</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"
<q-chip v-if="props.row.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>
@@ -46,15 +39,18 @@
<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
<q-select v-model="statusFilter" :options="options" @update:model-value="updateStatusFilter" borderless
dense options-dense style="font-size: 12px; padding-top: 1px;" />
</div>
</q-th>
</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 :props="p">
<q-btn-group flat>
<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>
</template>
</q-table>
@@ -69,10 +65,10 @@
<q-card-section class="q-pt-none q-mt-none">
<div class="q-gutter-lg">
<q-input filled v-model="lastName" label="氏名 *" hint="ユーザーの氏名を入力してください" lazy-rules
<q-input filled v-model="firstName" label="氏名 *" hint="ユーザーの氏名を入力してください" lazy-rules
:rules="[val => val && val.length > 0 || 'ユーザーの氏名を入力してください。']" />
<q-input filled v-model="firstName" label="苗字 *" hint="ユーザーの苗字を入力してください" lazy-rules
<q-input filled v-model="lastName" label="苗字 *" hint="ユーザーの苗字を入力してください" lazy-rules
:rules="[val => val && val.length > 0 || 'ユーザーの苗字を入力してください']" />
<q-input filled type="email" v-model="email" label="電子メール *" hint="電子メール、ログインとしても使用" lazy-rules
@@ -80,14 +76,14 @@
<q-input v-if="isCreate" v-model="pwd" filled :type="isPwd ? 'password' : 'text'" hint="パスワード"
label="パスワード" :disable="!isCreate" lazy-rules
:rules="[val => val && val.length > 0 || 'パスワードを入力してください']" autocomplete="new-password">
:rules="[val => val && val.length > 0 || 'Please type something']" autocomplete="new-password">
<template v-slot:append>
<q-icon :name="isPwd ? 'visibility_off' : 'visibility'" class="cursor-pointer"
@click="isPwd = !isPwd" />
</template>
</q-input>
<q-item tag="label" v-if="authStore.isSuperAdmin" class="q-pl-sm q-pr-none q-py-xs">
<q-item tag="label" class="q-pl-sm q-pr-none q-py-xs">
<q-item-section>
<q-item-label>システム管理者</q-item-label>
</q-item-section>
@@ -118,7 +114,7 @@
</q-item>
<q-input v-model="pwd" filled :type="isPwd ? 'password' : 'text'" hint="パスワードを入力してください" label="パスワード"
:disable="!resetPsw" lazy-rules :rules="[val => val && val.length > 0 || 'パスワードを入力してください']"
:disable="!resetPsw" lazy-rules :rules="[val => val && val.length > 0 || 'Please type something']"
autocomplete="new-password">
<template v-slot:append>
<q-icon :name="isPwd ? 'visibility_off' : 'visibility'" class="cursor-pointer"
@@ -131,7 +127,7 @@
</q-card-section>
<q-card-actions align="right" class="text-primary q-mb-md q-mx-sm">
<q-btn :loading="addEditLoading" padding="xs md" label="保存" type="submit" color="primary" />
<q-btn label="保存" type="submit" color="primary" />
<q-btn label="キャンセル" type="cancel" color="primary" flat class="q-ml-sm" @click="closeDg()" />
</q-card-actions>
</q-form>
@@ -157,41 +153,24 @@
</template>
<script setup lang="ts">
import { ref, onMounted, computed } from 'vue';
import TableActionMenu from 'components/TableActionMenu.vue';
import { useUserStore } from 'stores/useUserStore';
import { useAuthStore } from 'stores/useAuthStore';
import { IUserDisplay, IUserRolesDisplay } from 'src/types/UserTypes';
import { Actions } from 'boot/permissions';
const userStore = useUserStore();
const authStore = useAuthStore();
import { ref, onMounted } from 'vue';
import { api } from 'boot/axios';
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: 'firstName', label: '氏名', field: 'firstName', align: 'left', sortable: true },
{ name: 'lastName', label: '苗字', field: 'lastName', align: 'left', sortable: true },
{ name: 'email', label: '電子メール', field: 'email', align: 'left', sortable: true },
{ name: 'status', label: '状況', field: 'status', align: 'left' },
{ name: 'roles', label: 'ロール', field: '', align: 'left' },
{ name: 'actions', label: '', field: 'actions' }
{ 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 loading = ref(false);
const addEditLoading = ref(false);
const filter = ref('');
const statusFilter = ref(statusFilterOptions[0]);
const rows = computed(() => userStore.users.filter(statusFilter.value.filter));
const statusFilter = ref('全データ');
const rows = ref([]);
const show = ref(false);
const confirm = ref(false);
const resetPsw = ref(false);
@@ -204,47 +183,69 @@ const isActive = ref(true);
const isPwd = ref(true);
const pwd = ref('');
const editId = ref(0);
const isCreate = computed(() => editId.value <= 0);
const isCreate = ref(true);
let editId = ref(0);
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 () => {
const getUsers = async (filter = () => true) => {
loading.value = true;
await userStore.loadUsers();
const result = await api.get(`api/v1/users`);
rows.value = result.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;
}
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 () => {
await getUsers();
})
const options = ['全データ', 'システム管理者のみ', '使用可能', '使用不可']
// emulate fetching data from server
const addRow = () => {
// editId.value
onReset();
show.value = true;
}
function showDeleteUserConfirm(row: IUserDisplay) {
const removeRow = (row) => {
confirm.value = true;
editId.value = row.id;
}
const deleteUser = async () => {
await userStore.deleteUser(editId.value);
getUsers();
onReset();
const deleteUser = () => {
api.delete(`api/v1/users/${editId.value}`).then(() => {
getUsers();
})
editId.value = 0;
};
function editRow(row: IUserDisplay) {
const editRow = (row) => {
isCreate.value = false
editId.value = row.id;
firstName.value = row.firstName;
lastName.value = row.lastName;
email.value = row.email;
pwd.value = row.password;
isSuperuser.value = row.isSuperuser;
isActive.value = row.isActive;
@@ -258,25 +259,37 @@ const closeDg = () => {
onReset();
}
const onSubmit = async () => {
addEditLoading.value = true;
const param = {
id: editId.value,
first_name: firstName.value,
last_name: lastName.value,
is_superuser: isSuperuser.value,
is_active: isActive.value,
email: email.value,
password: (isCreate.value || resetPsw.value) ? pwd.value : undefined
const onSubmit = () => {
if (editId.value !== 0) {
api.put(`api/v1/users/${editId.value}`, {
'first_name': firstName.value,
'last_name': lastName.value,
'is_superuser': isSuperuser.value,
'is_active': isActive.value,
'email': email.value,
...(isCreate.value || resetPsw.value ? { password: pwd.value } : {})
}).then(() => {
getUsers();
closeDg();
onReset();
})
}
if (isCreate.value) {
await userStore.addUser(param);
} else {
await userStore.editUser(param);
else {
api.post(`api/v1/users`, {
'id': 0,
'first_name': firstName.value,
'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 = () => {
@@ -288,7 +301,7 @@ const onReset = () => {
isSuperuser.value = false;
isPwd.value = true;
editId.value = 0;
resetPsw.value = false;
addEditLoading.value = false;
isCreate.value = true;
resetPsw.value = false
}
</script>

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