auths.py 8.88 KB
Newer Older
1
2
import logging

liu.vaayne's avatar
liu.vaayne committed
3
4
from fastapi import Request
from fastapi import Depends, HTTPException, status
5

liu.vaayne's avatar
liu.vaayne committed
6
from fastapi import APIRouter
7
from pydantic import BaseModel
Timothy J. Baek's avatar
Timothy J. Baek committed
8
import re
Timothy J. Baek's avatar
Timothy J. Baek committed
9
import uuid
10
11
12
13

from apps.web.models.auths import (
    SigninForm,
    SignupForm,
14
    UpdateProfileForm,
15
    UpdatePasswordForm,
16
17
18
    UserResponse,
    SigninResponse,
    Auths,
Timothy J. Baek's avatar
Timothy J. Baek committed
19
    ApiKey,
20
21
22
)
from apps.web.models.users import Users

Timothy J. Baek's avatar
Timothy J. Baek committed
23
24
25
26
27
from utils.utils import (
    get_password_hash,
    get_current_user,
    get_admin_user,
    create_token,
Timothy J. Baek's avatar
Timothy J. Baek committed
28
    create_api_key,
Timothy J. Baek's avatar
Timothy J. Baek committed
29
)
Timothy J. Baek's avatar
Timothy J. Baek committed
30
from utils.misc import parse_duration, validate_email_format
Timothy J. Baek's avatar
Timothy J. Baek committed
31
32
from utils.webhook import post_webhook
from constants import ERROR_MESSAGES, WEBHOOK_MESSAGES
33
from config import WEBUI_AUTH_TRUSTED_EMAIL_HEADER
34

Timothy J. Baek's avatar
Timothy J. Baek committed
35
36
router = APIRouter()

37
38
39
40
41
############################
# GetSessionUser
############################


42
@router.get("/", response_model=UserResponse)
43
44
45
46
47
48
49
50
async def get_session_user(user=Depends(get_current_user)):
    return {
        "id": user.id,
        "email": user.email,
        "name": user.name,
        "role": user.role,
        "profile_image_url": user.profile_image_url,
    }
51
52


53
############################
54
# Update Profile
55
56
57
58
############################


@router.post("/update/profile", response_model=UserResponse)
59
60
async def update_profile(
    form_data: UpdateProfileForm, session_user=Depends(get_current_user)
61
62
):
    if session_user:
63
64
65
        user = Users.update_user_by_id(
            session_user.id,
            {"profile_image_url": form_data.profile_image_url, "name": form_data.name},
66
67
68
69
70
71
72
73
74
        )
        if user:
            return user
        else:
            raise HTTPException(400, detail=ERROR_MESSAGES.DEFAULT())
    else:
        raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_CRED)


75
76
77
78
79
############################
# Update Password
############################


80
@router.post("/update/password", response_model=bool)
81
82
83
async def update_password(
    form_data: UpdatePasswordForm, session_user=Depends(get_current_user)
):
84
85
    if WEBUI_AUTH_TRUSTED_EMAIL_HEADER:
        raise HTTPException(400, detail=ERROR_MESSAGES.ACTION_PROHIBITED)
86
87
    if session_user:
        user = Auths.authenticate_user(session_user.email, form_data.password)
88

89
90
        if user:
            hashed = get_password_hash(form_data.new_password)
Timothy J. Baek's avatar
Timothy J. Baek committed
91
            return Auths.update_user_password_by_id(user.id, hashed)
92
93
        else:
            raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_PASSWORD)
94
95
96
97
    else:
        raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_CRED)


98
99
100
101
102
103
############################
# SignIn
############################


@router.post("/signin", response_model=SigninResponse)
Timothy J. Baek's avatar
Timothy J. Baek committed
104
async def signin(request: Request, form_data: SigninForm):
105
106
    if WEBUI_AUTH_TRUSTED_EMAIL_HEADER:
        if WEBUI_AUTH_TRUSTED_EMAIL_HEADER not in request.headers:
Timothy J. Baek's avatar
Timothy J. Baek committed
107
108
            raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_TRUSTED_HEADER)

109
110
        trusted_email = request.headers[WEBUI_AUTH_TRUSTED_EMAIL_HEADER].lower()
        if not Users.get_user_by_email(trusted_email.lower()):
Timothy J. Baek's avatar
Timothy J. Baek committed
111
112
113
114
115
116
            await signup(
                request,
                SignupForm(
                    email=trusted_email, password=str(uuid.uuid4()), name=trusted_email
                ),
            )
117
118
        user = Auths.authenticate_user_by_trusted_header(trusted_email)
    else:
Timothy J. Baek's avatar
Timothy J. Baek committed
119
        user = Auths.authenticate_user(form_data.email.lower(), form_data.password)
120
121

    if user:
Timothy J. Baek's avatar
Timothy J. Baek committed
122
123
124
125
        token = create_token(
            data={"id": user.id},
            expires_delta=parse_duration(request.app.state.JWT_EXPIRES_IN),
        )
126
127
128
129
130
131
132
133

        return {
            "token": token,
            "token_type": "Bearer",
            "id": user.id,
            "email": user.email,
            "name": user.name,
            "role": user.role,
Timothy J. Baek's avatar
Timothy J. Baek committed
134
            "profile_image_url": user.profile_image_url,
135
136
        }
    else:
Timothy J. Baek's avatar
Timothy J. Baek committed
137
        raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_CRED)
138
139
140
141
142
143
144
145


############################
# SignUp
############################


@router.post("/signup", response_model=SigninResponse)
146
async def signup(request: Request, form_data: SignupForm):
147
    if not request.app.state.ENABLE_SIGNUP:
Timothy J. Baek's avatar
Timothy J. Baek committed
148
149
150
        raise HTTPException(
            status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.ACCESS_PROHIBITED
        )
151

152
    if not validate_email_format(form_data.email.lower()):
Timothy J. Baek's avatar
Timothy J. Baek committed
153
154
155
        raise HTTPException(
            status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.INVALID_EMAIL_FORMAT
        )
156

157
158
    if Users.get_user_by_email(form_data.email.lower()):
        raise HTTPException(400, detail=ERROR_MESSAGES.EMAIL_TAKEN)
159

160
    try:
Timothy J. Baek's avatar
Timothy J. Baek committed
161
162
163
164
165
        role = (
            "admin"
            if Users.get_num_users() == 0
            else request.app.state.DEFAULT_USER_ROLE
        )
166
        hashed = get_password_hash(form_data.password)
167
        user = Auths.insert_new_auth(
Danny Liu's avatar
Danny Liu committed
168
169
170
171
172
            form_data.email.lower(),
            hashed,
            form_data.name,
            form_data.profile_image_url,
            role,
173
        )
174

175
        if user:
Timothy J. Baek's avatar
Timothy J. Baek committed
176
177
178
179
            token = create_token(
                data={"id": user.id},
                expires_delta=parse_duration(request.app.state.JWT_EXPIRES_IN),
            )
180
181
            # response.set_cookie(key='token', value=token, httponly=True)

Timothy J. Baek's avatar
Timothy J. Baek committed
182
183
184
            if request.app.state.WEBHOOK_URL:
                post_webhook(
                    request.app.state.WEBHOOK_URL,
Timothy J. Baek's avatar
Timothy J. Baek committed
185
                    WEBHOOK_MESSAGES.USER_SIGNUP(user.name),
Timothy J. Baek's avatar
Timothy J. Baek committed
186
187
188
189
190
191
192
                    {
                        "action": "signup",
                        "message": WEBHOOK_MESSAGES.USER_SIGNUP(user.name),
                        "user": user.model_dump_json(exclude_none=True),
                    },
                )

193
194
195
196
197
198
199
200
201
202
            return {
                "token": token,
                "token_type": "Bearer",
                "id": user.id,
                "email": user.email,
                "name": user.name,
                "role": user.role,
                "profile_image_url": user.profile_image_url,
            }
        else:
203
            raise HTTPException(500, detail=ERROR_MESSAGES.CREATE_USER_ERROR)
204
    except Exception as err:
205
206
        raise HTTPException(500, detail=ERROR_MESSAGES.DEFAULT(err))

207
208
209
210
211
212
213

############################
# ToggleSignUp
############################


@router.get("/signup/enabled", response_model=bool)
214
215
async def get_sign_up_status(request: Request, user=Depends(get_admin_user)):
    return request.app.state.ENABLE_SIGNUP
216
217
218


@router.get("/signup/enabled/toggle", response_model=bool)
219
220
221
async def toggle_sign_up(request: Request, user=Depends(get_admin_user)):
    request.app.state.ENABLE_SIGNUP = not request.app.state.ENABLE_SIGNUP
    return request.app.state.ENABLE_SIGNUP
Timothy J. Baek's avatar
Timothy J. Baek committed
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244


############################
# Default User Role
############################


@router.get("/signup/user/role")
async def get_default_user_role(request: Request, user=Depends(get_admin_user)):
    return request.app.state.DEFAULT_USER_ROLE


class UpdateRoleForm(BaseModel):
    role: str


@router.post("/signup/user/role")
async def update_default_user_role(
    request: Request, form_data: UpdateRoleForm, user=Depends(get_admin_user)
):
    if form_data.role in ["pending", "user", "admin"]:
        request.app.state.DEFAULT_USER_ROLE = form_data.role
    return request.app.state.DEFAULT_USER_ROLE
Timothy J. Baek's avatar
Timothy J. Baek committed
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274


############################
# 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
liu.vaayne's avatar
liu.vaayne committed
275
276
277
278
279
280
281
282
283
284
285


############################
# API Key
############################


# create api key
@router.post("/api_key", response_model=ApiKey)
async def create_api_key_(user=Depends(get_current_user)):
    api_key = create_api_key()
Timothy J. Baek's avatar
refac  
Timothy J. Baek committed
286
    success = Users.update_user_api_key_by_id(user.id, api_key)
liu.vaayne's avatar
liu.vaayne committed
287
288
289
290
291
292
293
294
295
296
297
    if success:
        return {
            "api_key": api_key,
        }
    else:
        raise HTTPException(500, detail=ERROR_MESSAGES.CREATE_API_KEY_ERROR)


# delete api key
@router.delete("/api_key", response_model=bool)
async def delete_api_key(user=Depends(get_current_user)):
Timothy J. Baek's avatar
refac  
Timothy J. Baek committed
298
    success = Users.update_user_api_key_by_id(user.id, None)
liu.vaayne's avatar
liu.vaayne committed
299
300
301
302
303
304
    return success


# get api key
@router.get("/api_key", response_model=ApiKey)
async def get_api_key(user=Depends(get_current_user)):
Timothy J. Baek's avatar
refac  
Timothy J. Baek committed
305
    api_key = Users.get_user_api_key_by_id(user.id)
liu.vaayne's avatar
liu.vaayne committed
306
307
308
309
310
311
    if api_key:
        return {
            "api_key": api_key,
        }
    else:
        raise HTTPException(404, detail=ERROR_MESSAGES.API_KEY_NOT_FOUND)