Skip to content
GitLab
Menu
Projects
Groups
Snippets
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
Menu
Open sidebar
chenpangpang
open-webui
Commits
6d6ab645
Unverified
Commit
6d6ab645
authored
Feb 19, 2024
by
Timothy Jaeryang Baek
Committed by
GitHub
Feb 19, 2024
Browse files
Merge pull request #815 from open-webui/auth
feat: jwt utils
parents
d680d2cd
275523e3
Changes
7
Show whitespace changes
Inline
Side-by-side
Showing
7 changed files
with
273 additions
and
7 deletions
+273
-7
backend/apps/web/main.py
backend/apps/web/main.py
+2
-0
backend/apps/web/routers/auths.py
backend/apps/web/routers/auths.py
+41
-4
backend/utils/misc.py
backend/utils/misc.py
+33
-0
src/lib/apis/auths/index.ts
src/lib/apis/auths/index.ts
+57
-0
src/lib/components/admin/Settings/General.svelte
src/lib/components/admin/Settings/General.svelte
+33
-1
src/lib/components/chat/Settings/Account.svelte
src/lib/components/chat/Settings/Account.svelte
+105
-0
src/lib/components/chat/Settings/Account/UpdatePassword.svelte
...ib/components/chat/Settings/Account/UpdatePassword.svelte
+2
-2
No files found.
backend/apps/web/main.py
View file @
6d6ab645
...
@@ -26,6 +26,8 @@ app = FastAPI()
...
@@ -26,6 +26,8 @@ app = FastAPI()
origins
=
[
"*"
]
origins
=
[
"*"
]
app
.
state
.
ENABLE_SIGNUP
=
ENABLE_SIGNUP
app
.
state
.
ENABLE_SIGNUP
=
ENABLE_SIGNUP
app
.
state
.
JWT_EXPIRES_IN
=
"-1"
app
.
state
.
DEFAULT_MODELS
=
DEFAULT_MODELS
app
.
state
.
DEFAULT_MODELS
=
DEFAULT_MODELS
app
.
state
.
DEFAULT_PROMPT_SUGGESTIONS
=
DEFAULT_PROMPT_SUGGESTIONS
app
.
state
.
DEFAULT_PROMPT_SUGGESTIONS
=
DEFAULT_PROMPT_SUGGESTIONS
app
.
state
.
DEFAULT_USER_ROLE
=
DEFAULT_USER_ROLE
app
.
state
.
DEFAULT_USER_ROLE
=
DEFAULT_USER_ROLE
...
...
backend/apps/web/routers/auths.py
View file @
6d6ab645
...
@@ -7,6 +7,7 @@ from fastapi import APIRouter, status
...
@@ -7,6 +7,7 @@ from fastapi import APIRouter, status
from
pydantic
import
BaseModel
from
pydantic
import
BaseModel
import
time
import
time
import
uuid
import
uuid
import
re
from
apps.web.models.auths
import
(
from
apps.web.models.auths
import
(
SigninForm
,
SigninForm
,
...
@@ -25,7 +26,7 @@ from utils.utils import (
...
@@ -25,7 +26,7 @@ from utils.utils import (
get_admin_user
,
get_admin_user
,
create_token
,
create_token
,
)
)
from
utils.misc
import
get_gravatar_url
,
validate_email_format
from
utils.misc
import
parse_duration
,
validate_email_format
from
constants
import
ERROR_MESSAGES
from
constants
import
ERROR_MESSAGES
router
=
APIRouter
()
router
=
APIRouter
()
...
@@ -95,10 +96,13 @@ async def update_password(
...
@@ -95,10 +96,13 @@ async def update_password(
@
router
.
post
(
"/signin"
,
response_model
=
SigninResponse
)
@
router
.
post
(
"/signin"
,
response_model
=
SigninResponse
)
async
def
signin
(
form_data
:
SigninForm
):
async
def
signin
(
request
:
Request
,
form_data
:
SigninForm
):
user
=
Auths
.
authenticate_user
(
form_data
.
email
.
lower
(),
form_data
.
password
)
user
=
Auths
.
authenticate_user
(
form_data
.
email
.
lower
(),
form_data
.
password
)
if
user
:
if
user
:
token
=
create_token
(
data
=
{
"id"
:
user
.
id
})
token
=
create_token
(
data
=
{
"id"
:
user
.
id
},
expires_delta
=
parse_duration
(
request
.
app
.
state
.
JWT_EXPIRES_IN
),
)
return
{
return
{
"token"
:
token
,
"token"
:
token
,
...
@@ -145,7 +149,10 @@ async def signup(request: Request, form_data: SignupForm):
...
@@ -145,7 +149,10 @@ async def signup(request: Request, form_data: SignupForm):
)
)
if
user
:
if
user
:
token
=
create_token
(
data
=
{
"id"
:
user
.
id
})
token
=
create_token
(
data
=
{
"id"
:
user
.
id
},
expires_delta
=
parse_duration
(
request
.
app
.
state
.
JWT_EXPIRES_IN
),
)
# response.set_cookie(key='token', value=token, httponly=True)
# response.set_cookie(key='token', value=token, httponly=True)
return
{
return
{
...
@@ -200,3 +207,33 @@ async def update_default_user_role(
...
@@ -200,3 +207,33 @@ async def update_default_user_role(
if
form_data
.
role
in
[
"pending"
,
"user"
,
"admin"
]:
if
form_data
.
role
in
[
"pending"
,
"user"
,
"admin"
]:
request
.
app
.
state
.
DEFAULT_USER_ROLE
=
form_data
.
role
request
.
app
.
state
.
DEFAULT_USER_ROLE
=
form_data
.
role
return
request
.
app
.
state
.
DEFAULT_USER_ROLE
return
request
.
app
.
state
.
DEFAULT_USER_ROLE
############################
# JWT Expiration
############################
@
router
.
get
(
"/token/expires"
)
async
def
get_token_expires_duration
(
request
:
Request
,
user
=
Depends
(
get_admin_user
)):
return
request
.
app
.
state
.
JWT_EXPIRES_IN
class
UpdateJWTExpiresDurationForm
(
BaseModel
):
duration
:
str
@
router
.
post
(
"/token/expires/update"
)
async
def
update_token_expires_duration
(
request
:
Request
,
form_data
:
UpdateJWTExpiresDurationForm
,
user
=
Depends
(
get_admin_user
),
):
pattern
=
r
"^(-1|0|(-?\d+(\.\d+)?)(ms|s|m|h|d|w))$"
# Check if the input string matches the pattern
if
re
.
match
(
pattern
,
form_data
.
duration
):
request
.
app
.
state
.
JWT_EXPIRES_IN
=
form_data
.
duration
return
request
.
app
.
state
.
JWT_EXPIRES_IN
else
:
return
request
.
app
.
state
.
JWT_EXPIRES_IN
backend/utils/misc.py
View file @
6d6ab645
from
pathlib
import
Path
from
pathlib
import
Path
import
hashlib
import
hashlib
import
re
import
re
from
datetime
import
timedelta
from
typing
import
Optional
def
get_gravatar_url
(
email
):
def
get_gravatar_url
(
email
):
...
@@ -76,3 +78,34 @@ def extract_folders_after_data_docs(path):
...
@@ -76,3 +78,34 @@ def extract_folders_after_data_docs(path):
tags
.
append
(
"/"
.
join
(
folders
[:
idx
+
1
]))
tags
.
append
(
"/"
.
join
(
folders
[:
idx
+
1
]))
return
tags
return
tags
def
parse_duration
(
duration
:
str
)
->
Optional
[
timedelta
]:
if
duration
==
"-1"
or
duration
==
"0"
:
return
None
# Regular expression to find number and unit pairs
pattern
=
r
"(-?\d+(\.\d+)?)(ms|s|m|h|d|w)"
matches
=
re
.
findall
(
pattern
,
duration
)
if
not
matches
:
raise
ValueError
(
"Invalid duration string"
)
total_duration
=
timedelta
()
for
number
,
_
,
unit
in
matches
:
number
=
float
(
number
)
if
unit
==
"ms"
:
total_duration
+=
timedelta
(
milliseconds
=
number
)
elif
unit
==
"s"
:
total_duration
+=
timedelta
(
seconds
=
number
)
elif
unit
==
"m"
:
total_duration
+=
timedelta
(
minutes
=
number
)
elif
unit
==
"h"
:
total_duration
+=
timedelta
(
hours
=
number
)
elif
unit
==
"d"
:
total_duration
+=
timedelta
(
days
=
number
)
elif
unit
==
"w"
:
total_duration
+=
timedelta
(
weeks
=
number
)
return
total_duration
src/lib/apis/auths/index.ts
View file @
6d6ab645
...
@@ -261,3 +261,60 @@ export const toggleSignUpEnabledStatus = async (token: string) => {
...
@@ -261,3 +261,60 @@ export const toggleSignUpEnabledStatus = async (token: string) => {
return
res
;
return
res
;
};
};
export
const
getJWTExpiresDuration
=
async
(
token
:
string
)
=>
{
let
error
=
null
;
const
res
=
await
fetch
(
`
${
WEBUI_API_BASE_URL
}
/auths/token/expires`
,
{
method
:
'
GET
'
,
headers
:
{
'
Content-Type
'
:
'
application/json
'
,
Authorization
:
`Bearer
${
token
}
`
}
})
.
then
(
async
(
res
)
=>
{
if
(
!
res
.
ok
)
throw
await
res
.
json
();
return
res
.
json
();
})
.
catch
((
err
)
=>
{
console
.
log
(
err
);
error
=
err
.
detail
;
return
null
;
});
if
(
error
)
{
throw
error
;
}
return
res
;
};
export
const
updateJWTExpiresDuration
=
async
(
token
:
string
,
duration
:
string
)
=>
{
let
error
=
null
;
const
res
=
await
fetch
(
`
${
WEBUI_API_BASE_URL
}
/auths/token/expires/update`
,
{
method
:
'
POST
'
,
headers
:
{
'
Content-Type
'
:
'
application/json
'
,
Authorization
:
`Bearer
${
token
}
`
},
body
:
JSON
.
stringify
({
duration
:
duration
})
})
.
then
(
async
(
res
)
=>
{
if
(
!
res
.
ok
)
throw
await
res
.
json
();
return
res
.
json
();
})
.
catch
((
err
)
=>
{
console
.
log
(
err
);
error
=
err
.
detail
;
return
null
;
});
if
(
error
)
{
throw
error
;
}
return
res
;
};
src/lib/components/admin/Settings/General.svelte
View file @
6d6ab645
<script lang="ts">
<script lang="ts">
import {
import {
getDefaultUserRole,
getDefaultUserRole,
getJWTExpiresDuration,
getSignUpEnabledStatus,
getSignUpEnabledStatus,
toggleSignUpEnabledStatus,
toggleSignUpEnabledStatus,
updateDefaultUserRole
updateDefaultUserRole,
updateJWTExpiresDuration
} from '$lib/apis/auths';
} from '$lib/apis/auths';
import { onMount } from 'svelte';
import { onMount } from 'svelte';
export let saveHandler: Function;
export let saveHandler: Function;
let signUpEnabled = true;
let signUpEnabled = true;
let defaultUserRole = 'pending';
let defaultUserRole = 'pending';
let JWTExpiresIn = '';
const toggleSignUpEnabled = async () => {
const toggleSignUpEnabled = async () => {
signUpEnabled = await toggleSignUpEnabledStatus(localStorage.token);
signUpEnabled = await toggleSignUpEnabledStatus(localStorage.token);
...
@@ -19,9 +22,14 @@
...
@@ -19,9 +22,14 @@
defaultUserRole = await updateDefaultUserRole(localStorage.token, role);
defaultUserRole = await updateDefaultUserRole(localStorage.token, role);
};
};
const updateJWTExpiresDurationHandler = async (duration) => {
JWTExpiresIn = await updateJWTExpiresDuration(localStorage.token, duration);
};
onMount(async () => {
onMount(async () => {
signUpEnabled = await getSignUpEnabledStatus(localStorage.token);
signUpEnabled = await getSignUpEnabledStatus(localStorage.token);
defaultUserRole = await getDefaultUserRole(localStorage.token);
defaultUserRole = await getDefaultUserRole(localStorage.token);
JWTExpiresIn = await getJWTExpiresDuration(localStorage.token);
});
});
</script>
</script>
...
@@ -29,6 +37,7 @@
...
@@ -29,6 +37,7 @@
class="flex flex-col h-full justify-between space-y-3 text-sm"
class="flex flex-col h-full justify-between space-y-3 text-sm"
on:submit|preventDefault={() => {
on:submit|preventDefault={() => {
// console.log('submit');
// console.log('submit');
updateJWTExpiresDurationHandler(JWTExpiresIn);
saveHandler();
saveHandler();
}}
}}
>
>
...
@@ -94,6 +103,29 @@
...
@@ -94,6 +103,29 @@
</select>
</select>
</div>
</div>
</div>
</div>
<hr class=" dark:border-gray-700 my-3" />
<div class=" w-full justify-between">
<div class="flex w-full justify-between">
<div class=" self-center text-xs font-medium">JWT Expiration</div>
</div>
<div class="flex mt-2 space-x-2">
<input
class="w-full rounded py-1.5 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none border border-gray-100 dark:border-gray-600"
type="text"
placeholder={`e.g.) "30m","1h", "10d". `}
bind:value={JWTExpiresIn}
/>
</div>
<div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
Valid time units: <span class=" text-gray-300 font-medium"
>'s', 'm', 'h', 'd', 'w' or '-1' for no expiration.</span
>
</div>
</div>
</div>
</div>
</div>
</div>
...
...
src/lib/components/chat/Settings/Account.svelte
View file @
6d6ab645
...
@@ -7,11 +7,14 @@
...
@@ -7,11 +7,14 @@
import UpdatePassword from './Account/UpdatePassword.svelte';
import UpdatePassword from './Account/UpdatePassword.svelte';
import { getGravatarUrl } from '$lib/apis/utils';
import { getGravatarUrl } from '$lib/apis/utils';
import { copyToClipboard } from '$lib/utils';
export let saveHandler: Function;
export let saveHandler: Function;
let profileImageUrl = '';
let profileImageUrl = '';
let name = '';
let name = '';
let showJWTToken = false;
let JWTTokenCopied = false;
const submitHandler = async () => {
const submitHandler = async () => {
const updatedUser = await updateUserProfile(localStorage.token, name, profileImageUrl).catch(
const updatedUser = await updateUserProfile(localStorage.token, name, profileImageUrl).catch(
...
@@ -160,6 +163,108 @@
...
@@ -160,6 +163,108 @@
<hr class=" dark:border-gray-700 my-4" />
<hr class=" dark:border-gray-700 my-4" />
<UpdatePassword />
<UpdatePassword />
<hr class=" dark:border-gray-700 my-4" />
<div class=" w-full justify-between">
<div class="flex w-full justify-between">
<div class=" self-center text-xs font-medium">JWT Token</div>
</div>
<div class="flex mt-2">
<div class="flex w-full">
<input
class="w-full rounded-l-lg py-1.5 pl-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none"
type={showJWTToken ? 'text' : 'password'}
value={localStorage.token}
disabled
/>
<button
class="dark:bg-gray-800 px-2 transition rounded-r-lg"
on:click={() => {
showJWTToken = !showJWTToken;
}}
>
{#if showJWTToken}
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-4 h-4"
>
<path
fill-rule="evenodd"
d="M3.28 2.22a.75.75 0 0 0-1.06 1.06l10.5 10.5a.75.75 0 1 0 1.06-1.06l-1.322-1.323a7.012 7.012 0 0 0 2.16-3.11.87.87 0 0 0 0-.567A7.003 7.003 0 0 0 4.82 3.76l-1.54-1.54Zm3.196 3.195 1.135 1.136A1.502 1.502 0 0 1 9.45 8.389l1.136 1.135a3 3 0 0 0-4.109-4.109Z"
clip-rule="evenodd"
/>
<path
d="m7.812 10.994 1.816 1.816A7.003 7.003 0 0 1 1.38 8.28a.87.87 0 0 1 0-.566 6.985 6.985 0 0 1 1.113-2.039l2.513 2.513a3 3 0 0 0 2.806 2.806Z"
/>
</svg>
{:else}
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-4 h-4"
>
<path d="M8 9.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3Z" />
<path
fill-rule="evenodd"
d="M1.38 8.28a.87.87 0 0 1 0-.566 7.003 7.003 0 0 1 13.238.006.87.87 0 0 1 0 .566A7.003 7.003 0 0 1 1.379 8.28ZM11 8a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z"
clip-rule="evenodd"
/>
</svg>
{/if}
</button>
</div>
<button
class="ml-1.5 px-1.5 py-1 hover:bg-gray-800 transition rounded-lg"
on:click={() => {
copyToClipboard(localStorage.token);
JWTTokenCopied = true;
setTimeout(() => {
JWTTokenCopied = false;
}, 2000);
}}
>
{#if JWTTokenCopied}
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-4 h-4"
>
<path
fill-rule="evenodd"
d="M16.704 4.153a.75.75 0 01.143 1.052l-8 10.5a.75.75 0 01-1.127.075l-4.5-4.5a.75.75 0 011.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 011.05-.143z"
clip-rule="evenodd"
/>
</svg>
{:else}
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-4 h-4"
>
<path
fill-rule="evenodd"
d="M11.986 3H12a2 2 0 0 1 2 2v6a2 2 0 0 1-1.5 1.937V7A2.5 2.5 0 0 0 10 4.5H4.063A2 2 0 0 1 6 3h.014A2.25 2.25 0 0 1 8.25 1h1.5a2.25 2.25 0 0 1 2.236 2ZM10.5 4v-.75a.75.75 0 0 0-.75-.75h-1.5a.75.75 0 0 0-.75.75V4h3Z"
clip-rule="evenodd"
/>
<path
fill-rule="evenodd"
d="M3 6a1 1 0 0 0-1 1v7a1 1 0 0 0 1 1h7a1 1 0 0 0 1-1V7a1 1 0 0 0-1-1H3Zm1.75 2.5a.75.75 0 0 0 0 1.5h3.5a.75.75 0 0 0 0-1.5h-3.5ZM4 11.75a.75.75 0 0 1 .75-.75h3.5a.75.75 0 0 1 0 1.5h-3.5a.75.75 0 0 1-.75-.75Z"
clip-rule="evenodd"
/>
</svg>
{/if}
</button>
</div>
</div>
</div>
</div>
<div class="flex justify-end pt-3 text-sm font-medium">
<div class="flex justify-end pt-3 text-sm font-medium">
...
...
src/lib/components/chat/Settings/Account/UpdatePassword.svelte
View file @
6d6ab645
...
@@ -39,7 +39,7 @@
...
@@ -39,7 +39,7 @@
updatePasswordHandler();
updatePasswordHandler();
}}
}}
>
>
<div class="flex justify-between
mb-2.5
items-center text-sm">
<div class="flex justify-between items-center text-sm">
<div class=" font-medium">Change Password</div>
<div class=" font-medium">Change Password</div>
<button
<button
class=" text-xs font-medium text-gray-500"
class=" text-xs font-medium text-gray-500"
...
@@ -51,7 +51,7 @@
...
@@ -51,7 +51,7 @@
</div>
</div>
{#if show}
{#if show}
<div class=" space-y-1.5">
<div class="
py-2.5
space-y-1.5">
<div class="flex flex-col w-full">
<div class="flex flex-col w-full">
<div class=" mb-1 text-xs text-gray-500">Current Password</div>
<div class=" mb-1 text-xs text-gray-500">Current Password</div>
...
...
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