Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
D
dify
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
ai-tech
dify
Commits
4600c9f4
Commit
4600c9f4
authored
Jul 11, 2023
by
StyleZhang
Browse files
Options
Browse Files
Download
Plain Diff
resolve conflict
parents
abdd0d9e
6ba806e7
Changes
10
Hide whitespace changes
Inline
Side-by-side
Showing
10 changed files
with
163 additions
and
86 deletions
+163
-86
.gitignore
.gitignore
+1
-0
app.py
api/app.py
+1
-1
__init__.py
api/controllers/web/__init__.py
+1
-1
passport.py
api/controllers/web/passport.py
+64
-0
wraps.py
api/controllers/web/wraps.py
+16
-78
passport.py
api/libs/passport.py
+20
-0
requirements.txt
api/requirements.txt
+2
-1
index.tsx
web/app/components/share/chat/index.tsx
+42
-3
base.ts
web/service/base.ts
+10
-2
share.ts
web/service/share.ts
+6
-0
No files found.
.gitignore
View file @
4600c9f4
...
...
@@ -109,6 +109,7 @@ venv/
ENV/
env.bak/
venv.bak/
.conda/
# Spyder project settings
.spyderproject
...
...
api/app.py
View file @
4600c9f4
...
...
@@ -156,7 +156,7 @@ def register_blueprints(app):
resources
=
{
r"/*"
:
{
"origins"
:
app
.
config
[
'WEB_API_CORS_ALLOW_ORIGINS'
]}},
supports_credentials
=
True
,
allow_headers
=
[
'Content-Type'
,
'Authorization'
],
allow_headers
=
[
'Content-Type'
,
'Authorization'
,
'X-App-Code'
],
methods
=
[
'GET'
,
'PUT'
,
'POST'
,
'DELETE'
,
'OPTIONS'
,
'PATCH'
],
expose_headers
=
[
'X-Version'
,
'X-Env'
]
)
...
...
api/controllers/web/__init__.py
View file @
4600c9f4
...
...
@@ -7,4 +7,4 @@ bp = Blueprint('web', __name__, url_prefix='/api')
api
=
ExternalApi
(
bp
)
from
.
import
completion
,
app
,
conversation
,
message
,
site
,
saved_message
,
audio
from
.
import
completion
,
app
,
conversation
,
message
,
site
,
saved_message
,
audio
,
passport
api/controllers/web/passport.py
0 → 100644
View file @
4600c9f4
# -*- coding:utf-8 -*-
import
uuid
from
controllers.web
import
api
from
flask_restful
import
Resource
from
flask
import
request
from
werkzeug.exceptions
import
Unauthorized
,
NotFound
from
models.model
import
Site
,
EndUser
,
App
from
extensions.ext_database
import
db
from
libs.passport
import
PassportService
class
PassportResource
(
Resource
):
"""Base resource for passport."""
def
get
(
self
):
app_id
=
request
.
headers
.
get
(
'X-App-Code'
)
if
app_id
is
None
:
raise
Unauthorized
(
'X-App-Code header is missing.'
)
# get site from db and check if it is normal
site
=
db
.
session
.
query
(
Site
)
.
filter
(
Site
.
code
==
app_id
,
Site
.
status
==
'normal'
)
.
first
()
if
not
site
:
raise
NotFound
()
# get app from db and check if it is normal and enable_site
app_model
=
db
.
session
.
query
(
App
)
.
filter
(
App
.
id
==
site
.
app_id
)
.
first
()
if
not
app_model
or
app_model
.
status
!=
'normal'
or
not
app_model
.
enable_site
:
raise
NotFound
()
end_user
=
EndUser
(
tenant_id
=
app_model
.
tenant_id
,
app_id
=
app_model
.
id
,
type
=
'browser'
,
is_anonymous
=
True
,
session_id
=
generate_session_id
(),
)
db
.
session
.
add
(
end_user
)
db
.
session
.
commit
()
payload
=
{
"iss"
:
site
.
app_id
,
'sub'
:
'Web API Passport'
,
'app_id'
:
site
.
app_id
,
'end_user_id'
:
end_user
.
id
,
}
tk
=
PassportService
()
.
issue
(
payload
)
return
{
'access_token'
:
tk
,
}
api
.
add_resource
(
PassportResource
,
'/passport'
)
def
generate_session_id
():
"""
Generate a unique session ID.
"""
while
True
:
session_id
=
str
(
uuid
.
uuid4
())
existing_count
=
db
.
session
.
query
(
EndUser
)
\
.
filter
(
EndUser
.
session_id
==
session_id
)
.
count
()
if
existing_count
==
0
:
return
session_id
api/controllers/web/wraps.py
View file @
4600c9f4
# -*- coding:utf-8 -*-
import
uuid
from
functools
import
wraps
from
flask
import
request
,
session
from
flask
import
request
from
flask_restful
import
Resource
from
werkzeug.exceptions
import
NotFound
,
Unauthorized
from
extensions.ext_database
import
db
from
models.model
import
App
,
Site
,
EndUser
from
models.model
import
App
,
EndUser
from
libs.passport
import
PassportService
def
validate_token
(
view
=
None
):
def
validate_jwt_token
(
view
=
None
):
def
decorator
(
view
):
@
wraps
(
view
)
def
decorated
(
*
args
,
**
kwargs
):
site
=
validate_and_get_site
()
app_model
=
db
.
session
.
query
(
App
)
.
filter
(
App
.
id
==
site
.
app_id
)
.
first
()
if
not
app_model
:
raise
NotFound
()
if
app_model
.
status
!=
'normal'
:
raise
NotFound
()
if
not
app_model
.
enable_site
:
raise
NotFound
()
end_user
=
create_or_update_end_user_for_session
(
app_model
)
app_model
,
end_user
=
decode_jwt_token
()
return
view
(
app_model
,
end_user
,
*
args
,
**
kwargs
)
return
decorated
if
view
:
return
decorator
(
view
)
return
decorator
def
validate_and_get_site
():
"""
Validate and get API token.
"""
def
decode_jwt_token
():
auth_header
=
request
.
headers
.
get
(
'Authorization'
)
if
auth_header
is
None
:
raise
Unauthorized
(
'Authorization header is missing.'
)
if
' '
not
in
auth_header
:
raise
Unauthorized
(
'Invalid Authorization header format. Expected
\'
Bearer <api-key>
\'
format.'
)
auth_scheme
,
auth_token
=
auth_header
.
split
(
None
,
1
)
auth_scheme
,
tk
=
auth_header
.
split
(
None
,
1
)
auth_scheme
=
auth_scheme
.
lower
()
if
auth_scheme
!=
'bearer'
:
raise
Unauthorized
(
'Invalid Authorization header format. Expected
\'
Bearer <api-key>
\'
format.'
)
site
=
db
.
session
.
query
(
Site
)
.
filter
(
Site
.
code
==
auth_token
,
Site
.
status
==
'normal'
)
.
first
()
if
not
site
:
decoded
=
PassportService
()
.
verify
(
tk
)
app_model
=
db
.
session
.
query
(
App
)
.
filter
(
App
.
id
==
decoded
[
'app_id'
])
.
first
()
if
not
app_model
:
raise
NotFound
()
end_user
=
db
.
session
.
query
(
EndUser
)
.
filter
(
EndUser
.
id
==
decoded
[
'end_user_id'
])
.
first
()
if
not
end_user
:
raise
NotFound
()
return
site
def
create_or_update_end_user_for_session
(
app_model
):
"""
Create or update session terminal based on session ID.
"""
if
'session_id'
not
in
session
:
session
[
'session_id'
]
=
generate_session_id
()
session_id
=
session
.
get
(
'session_id'
)
end_user
=
db
.
session
.
query
(
EndUser
)
\
.
filter
(
EndUser
.
session_id
==
session_id
,
EndUser
.
type
==
'browser'
)
.
first
()
if
end_user
is
None
:
end_user
=
EndUser
(
tenant_id
=
app_model
.
tenant_id
,
app_id
=
app_model
.
id
,
type
=
'browser'
,
is_anonymous
=
True
,
session_id
=
session_id
)
db
.
session
.
add
(
end_user
)
db
.
session
.
commit
()
return
end_user
def
generate_session_id
():
"""
Generate a unique session ID.
"""
count
=
1
session_id
=
''
while
count
!=
0
:
session_id
=
str
(
uuid
.
uuid4
())
count
=
db
.
session
.
query
(
EndUser
)
\
.
filter
(
EndUser
.
session_id
==
session_id
)
.
count
()
return
session_id
return
app_model
,
end_user
class
WebApiResource
(
Resource
):
method_decorators
=
[
validate_token
]
method_decorators
=
[
validate_
jwt_
token
]
api/libs/passport.py
0 → 100644
View file @
4600c9f4
# -*- coding:utf-8 -*-
import
jwt
from
werkzeug.exceptions
import
Unauthorized
from
flask
import
current_app
class
PassportService
:
def
__init__
(
self
):
self
.
sk
=
current_app
.
config
.
get
(
'SECRET_KEY'
)
def
issue
(
self
,
payload
):
return
jwt
.
encode
(
payload
,
self
.
sk
,
algorithm
=
'HS256'
)
def
verify
(
self
,
token
):
try
:
return
jwt
.
decode
(
token
,
self
.
sk
,
algorithms
=
[
'HS256'
])
except
jwt
.
exceptions
.
InvalidSignatureError
:
raise
Unauthorized
(
'Invalid token signature.'
)
except
jwt
.
exceptions
.
DecodeError
:
raise
Unauthorized
(
'Invalid token.'
)
except
jwt
.
exceptions
.
ExpiredSignatureError
:
raise
Unauthorized
(
'Token has expired.'
)
api/requirements.txt
View file @
4600c9f4
...
...
@@ -33,4 +33,5 @@ openpyxl==3.1.2
chardet~=5.1.0
docx2txt==0.8
pypdfium2==4.16.0
resend~=0.5.1
\ No newline at end of file
resend~=0.5.1
pyjwt~=2.6.0
web/app/components/share/chat/index.tsx
View file @
4600c9f4
...
...
@@ -6,6 +6,7 @@ import cn from 'classnames'
import
{
useTranslation
}
from
'react-i18next'
import
{
useContext
}
from
'use-context-selector'
import
produce
from
'immer'
import
{
useParams
}
from
'next/navigation'
import
{
useBoolean
,
useGetState
}
from
'ahooks'
import
AppUnavailable
from
'../../base/app-unavailable'
import
useConversation
from
'./hooks/use-conversation'
...
...
@@ -14,7 +15,20 @@ import { ToastContext } from '@/app/components/base/toast'
import
Sidebar
from
'@/app/components/share/chat/sidebar'
import
ConfigSence
from
'@/app/components/share/chat/config-scence'
import
Header
from
'@/app/components/share/header'
import
{
delConversation
,
fetchAppInfo
,
fetchAppParams
,
fetchChatList
,
fetchConversations
,
fetchSuggestedQuestions
,
pinConversation
,
sendChatMessage
,
stopChatMessageResponding
,
unpinConversation
,
updateFeedback
}
from
'@/service/share'
import
{
delConversation
,
fetchAccessToken
,
fetchAppInfo
,
fetchAppParams
,
fetchChatList
,
fetchConversations
,
fetchSuggestedQuestions
,
pinConversation
,
sendChatMessage
,
stopChatMessageResponding
,
unpinConversation
,
updateFeedback
,
}
from
'@/service/share'
import
type
{
ConversationItem
,
SiteInfo
}
from
'@/models/share'
import
type
{
PromptConfig
,
SuggestedQuestionsAfterAnswerConfig
}
from
'@/models/debug'
import
type
{
Feedbacktype
,
IChatItem
}
from
'@/app/components/app/chat'
...
...
@@ -54,6 +68,7 @@ const Main: FC<IMainProps> = ({
// in mobile, show sidebar by click button
const
[
isShowSidebar
,
{
setTrue
:
showSidebar
,
setFalse
:
hideSidebar
}]
=
useBoolean
(
false
)
// Can Use metadata(https://beta.nextjs.org/docs/api-reference/metadata) to set title. But it only works in server side client.
const
params
=
useParams
()
useEffect
(()
=>
{
if
(
siteInfo
?.
title
)
{
if
(
plan
!==
'basic'
)
...
...
@@ -296,7 +311,31 @@ const Main: FC<IMainProps> = ({
return
fetchConversations
(
isInstalledApp
,
installedAppInfo
?.
id
,
undefined
,
undefined
,
100
)
}
const
fetchInitData
=
()
=>
{
const
fetchAndSetAccessToken
=
async
()
=>
{
const
sharedToken
=
params
.
token
const
accessToken
=
localStorage
.
getItem
(
'token'
)
||
JSON
.
stringify
({
[
sharedToken
]:
''
})
let
accessTokenJson
=
{
[
sharedToken
]:
''
}
try
{
accessTokenJson
=
JSON
.
parse
(
accessToken
)
}
catch
(
e
)
{
}
const
res
=
await
fetchAccessToken
(
sharedToken
)
accessTokenJson
[
sharedToken
]
=
res
.
access_token
localStorage
.
setItem
(
'token'
,
JSON
.
stringify
(
accessTokenJson
))
location
.
reload
()
}
const
fetchInitData
=
async
()
=>
{
let
appData
:
any
=
{}
try
{
appData
=
await
fetchAppInfo
()
}
catch
(
e
:
any
)
{
if
(
e
.
code
===
'unauthorized'
)
await
fetchAndSetAccessToken
()
}
return
Promise
.
all
([
isInstalledApp
?
{
app_id
:
installedAppInfo
?.
id
,
...
...
@@ -307,7 +346,7 @@ const Main: FC<IMainProps> = ({
},
plan
:
'basic'
,
}
:
fetchAppInfo
()
,
fetchAllConversations
(),
fetchAppParams
(
isInstalledApp
,
installedAppInfo
?.
id
)])
:
appData
,
fetchAllConversations
(),
fetchAppParams
(
isInstalledApp
,
installedAppInfo
?.
id
)])
}
// init
...
...
web/service/base.ts
View file @
4600c9f4
...
...
@@ -150,7 +150,15 @@ const baseFetch = (
const
options
=
Object
.
assign
({},
baseOptions
,
fetchOptions
)
if
(
isPublicAPI
)
{
const
sharedToken
=
globalThis
.
location
.
pathname
.
split
(
'/'
).
slice
(
-
1
)[
0
]
options
.
headers
.
set
(
'Authorization'
,
`bearer
${
sharedToken
}
`
)
const
accessToken
=
localStorage
.
getItem
(
'token'
)
||
JSON
.
stringify
({
[
sharedToken
]:
''
})
let
accessTokenJson
=
{
[
sharedToken
]:
''
}
try
{
accessTokenJson
=
JSON
.
parse
(
accessToken
)
}
catch
(
e
)
{
}
options
.
headers
.
set
(
'Authorization'
,
`Bearer
${
accessTokenJson
[
sharedToken
]}
`
)
}
if
(
deleteContentType
)
{
...
...
@@ -202,7 +210,7 @@ const baseFetch = (
case
401
:
{
if
(
isPublicAPI
)
{
Toast
.
notify
({
type
:
'error'
,
message
:
'Invalid token'
})
return
return
bodyJson
.
then
((
data
:
any
)
=>
Promise
.
reject
(
data
))
}
const
loginUrl
=
`
${
globalThis
.
location
.
origin
}
/signin`
if
(
IS_CE_EDITION
)
{
...
...
web/service/share.ts
View file @
4600c9f4
...
...
@@ -118,3 +118,9 @@ export const fetchSuggestedQuestions = (messageId: string, isInstalledApp: boole
export
const
audioToText
=
(
url
:
string
,
isPublicAPI
:
boolean
,
body
:
FormData
)
=>
{
return
(
getAction
(
'post'
,
!
isPublicAPI
))(
url
,
{
body
},
{
bodyStringify
:
false
,
deleteContentType
:
true
})
as
Promise
<
{
text
:
string
}
>
}
export
const
fetchAccessToken
=
async
(
appCode
:
string
)
=>
{
const
headers
=
new
Headers
()
headers
.
append
(
'X-App-Code'
,
appCode
)
return
get
(
'/passport'
,
{
headers
})
as
Promise
<
{
access_token
:
string
}
>
}
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment