auths.py 12.8 KB
Newer Older
1
2
import logging

3
from fastapi import Request, UploadFile, File
liu.vaayne's avatar
liu.vaayne committed
4
from fastapi import Depends, HTTPException, status
Timothy J. Baek's avatar
Timothy J. Baek committed
5
from fastapi.responses import Response
6

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

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

Timothy J. Baek's avatar
Timothy J. Baek committed
26
27
28
29
30
from utils.utils import (
    get_password_hash,
    get_current_user,
    get_admin_user,
    create_token,
Timothy J. Baek's avatar
Timothy J. Baek committed
31
    create_api_key,
Timothy J. Baek's avatar
Timothy J. Baek committed
32
)
Timothy J. Baek's avatar
Timothy J. Baek committed
33
from utils.misc import parse_duration, validate_email_format
Timothy J. Baek's avatar
Timothy J. Baek committed
34
35
from utils.webhook import post_webhook
from constants import ERROR_MESSAGES, WEBHOOK_MESSAGES
36
37
38
from config import (
    WEBUI_AUTH,
    WEBUI_AUTH_TRUSTED_EMAIL_HEADER,
Timothy J. Baek's avatar
Timothy J. Baek committed
39
    WEBUI_AUTH_TRUSTED_NAME_HEADER,
40
)
41

Timothy J. Baek's avatar
Timothy J. Baek committed
42
43
router = APIRouter()

44
45
46
47
48
############################
# GetSessionUser
############################


49
@router.get("/", response_model=UserResponse)
Timothy J. Baek's avatar
Timothy J. Baek committed
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
async def get_session_user(
    request: Request, response: Response, user=Depends(get_current_user)
):
    token = create_token(
        data={"id": user.id},
        expires_delta=parse_duration(request.app.state.config.JWT_EXPIRES_IN),
    )

    # Set the cookie token
    response.set_cookie(
        key="token",
        value=token,
        httponly=True,  # Ensures the cookie is not accessible via JavaScript
    )

65
66
67
68
69
70
71
    return {
        "id": user.id,
        "email": user.email,
        "name": user.name,
        "role": user.role,
        "profile_image_url": user.profile_image_url,
    }
72
73


74
############################
75
# Update Profile
76
77
78
79
############################


@router.post("/update/profile", response_model=UserResponse)
80
81
async def update_profile(
    form_data: UpdateProfileForm, session_user=Depends(get_current_user)
82
83
):
    if session_user:
84
85
86
        user = Users.update_user_by_id(
            session_user.id,
            {"profile_image_url": form_data.profile_image_url, "name": form_data.name},
87
88
89
90
91
92
93
94
95
        )
        if user:
            return user
        else:
            raise HTTPException(400, detail=ERROR_MESSAGES.DEFAULT())
    else:
        raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_CRED)


96
97
98
99
100
############################
# Update Password
############################


101
@router.post("/update/password", response_model=bool)
102
103
104
async def update_password(
    form_data: UpdatePasswordForm, session_user=Depends(get_current_user)
):
105
106
    if WEBUI_AUTH_TRUSTED_EMAIL_HEADER:
        raise HTTPException(400, detail=ERROR_MESSAGES.ACTION_PROHIBITED)
107
108
    if session_user:
        user = Auths.authenticate_user(session_user.email, form_data.password)
109

110
111
        if user:
            hashed = get_password_hash(form_data.new_password)
Timothy J. Baek's avatar
Timothy J. Baek committed
112
            return Auths.update_user_password_by_id(user.id, hashed)
113
114
        else:
            raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_PASSWORD)
115
116
117
118
    else:
        raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_CRED)


119
120
121
122
123
124
############################
# SignIn
############################


@router.post("/signin", response_model=SigninResponse)
Timothy J. Baek's avatar
Timothy J. Baek committed
125
async def signin(request: Request, response: Response, form_data: SigninForm):
126
127
    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
128
129
            raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_TRUSTED_HEADER)

130
        trusted_email = request.headers[WEBUI_AUTH_TRUSTED_EMAIL_HEADER].lower()
131
132
        trusted_name = trusted_email
        if WEBUI_AUTH_TRUSTED_NAME_HEADER:
Timothy J. Baek's avatar
Timothy J. Baek committed
133
134
135
            trusted_name = request.headers.get(
                WEBUI_AUTH_TRUSTED_NAME_HEADER, trusted_email
            )
136
        if not Users.get_user_by_email(trusted_email.lower()):
Timothy J. Baek's avatar
Timothy J. Baek committed
137
138
139
            await signup(
                request,
                SignupForm(
140
                    email=trusted_email, password=str(uuid.uuid4()), name=trusted_name
Timothy J. Baek's avatar
Timothy J. Baek committed
141
142
                ),
            )
143
        user = Auths.authenticate_user_by_trusted_header(trusted_email)
Timothy J. Baek's avatar
fix  
Timothy J. Baek committed
144
145
146
    elif WEBUI_AUTH == False:
        admin_email = "admin@localhost"
        admin_password = "admin"
Timothy J. Baek's avatar
Timothy J. Baek committed
147

Timothy J. Baek's avatar
fix  
Timothy J. Baek committed
148
149
150
151
152
        if Users.get_user_by_email(admin_email.lower()):
            user = Auths.authenticate_user(admin_email.lower(), admin_password)
        else:
            if Users.get_num_users() != 0:
                raise HTTPException(400, detail=ERROR_MESSAGES.EXISTING_USERS)
Timothy J. Baek's avatar
fix  
Timothy J. Baek committed
153

Timothy J. Baek's avatar
fix  
Timothy J. Baek committed
154
155
156
157
            await signup(
                request,
                SignupForm(email=admin_email, password=admin_password, name="User"),
            )
158

Timothy J. Baek's avatar
fix  
Timothy J. Baek committed
159
160
161
            user = Auths.authenticate_user(admin_email.lower(), admin_password)
    else:
        user = Auths.authenticate_user(form_data.email.lower(), form_data.password)
162
163

    if user:
Timothy J. Baek's avatar
Timothy J. Baek committed
164
165
        token = create_token(
            data={"id": user.id},
166
            expires_delta=parse_duration(request.app.state.config.JWT_EXPIRES_IN),
Timothy J. Baek's avatar
Timothy J. Baek committed
167
        )
168

Timothy J. Baek's avatar
Timothy J. Baek committed
169
170
171
172
173
174
175
        # Set the cookie token
        response.set_cookie(
            key="token",
            value=token,
            httponly=True,  # Ensures the cookie is not accessible via JavaScript
        )

176
177
178
179
180
181
182
        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
183
            "profile_image_url": user.profile_image_url,
184
185
        }
    else:
Timothy J. Baek's avatar
Timothy J. Baek committed
186
        raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_CRED)
187
188
189
190
191
192
193
194


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


@router.post("/signup", response_model=SigninResponse)
Timothy J. Baek's avatar
Timothy J. Baek committed
195
async def signup(request: Request, response: Response, form_data: SignupForm):
196
    if not request.app.state.config.ENABLE_SIGNUP and WEBUI_AUTH:
Timothy J. Baek's avatar
Timothy J. Baek committed
197
198
199
        raise HTTPException(
            status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.ACCESS_PROHIBITED
        )
200

201
    if not validate_email_format(form_data.email.lower()):
Timothy J. Baek's avatar
Timothy J. Baek committed
202
203
204
        raise HTTPException(
            status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.INVALID_EMAIL_FORMAT
        )
205

206
207
    if Users.get_user_by_email(form_data.email.lower()):
        raise HTTPException(400, detail=ERROR_MESSAGES.EMAIL_TAKEN)
208

209
    try:
Timothy J. Baek's avatar
Timothy J. Baek committed
210
211
212
        role = (
            "admin"
            if Users.get_num_users() == 0
213
            else request.app.state.config.DEFAULT_USER_ROLE
Timothy J. Baek's avatar
Timothy J. Baek committed
214
        )
215
        hashed = get_password_hash(form_data.password)
216
        user = Auths.insert_new_auth(
Danny Liu's avatar
Danny Liu committed
217
218
219
220
221
            form_data.email.lower(),
            hashed,
            form_data.name,
            form_data.profile_image_url,
            role,
222
        )
223

224
        if user:
Timothy J. Baek's avatar
Timothy J. Baek committed
225
226
            token = create_token(
                data={"id": user.id},
227
                expires_delta=parse_duration(request.app.state.config.JWT_EXPIRES_IN),
Timothy J. Baek's avatar
Timothy J. Baek committed
228
            )
229
230
            # response.set_cookie(key='token', value=token, httponly=True)

Timothy J. Baek's avatar
Timothy J. Baek committed
231
232
233
234
235
236
237
            # Set the cookie token
            response.set_cookie(
                key="token",
                value=token,
                httponly=True,  # Ensures the cookie is not accessible via JavaScript
            )

238
            if request.app.state.config.WEBHOOK_URL:
Timothy J. Baek's avatar
Timothy J. Baek committed
239
                post_webhook(
240
                    request.app.state.config.WEBHOOK_URL,
Timothy J. Baek's avatar
Timothy J. Baek committed
241
                    WEBHOOK_MESSAGES.USER_SIGNUP(user.name),
Timothy J. Baek's avatar
Timothy J. Baek committed
242
243
244
245
246
247
248
                    {
                        "action": "signup",
                        "message": WEBHOOK_MESSAGES.USER_SIGNUP(user.name),
                        "user": user.model_dump_json(exclude_none=True),
                    },
                )

249
250
251
252
253
254
255
256
257
258
            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:
259
            raise HTTPException(500, detail=ERROR_MESSAGES.CREATE_USER_ERROR)
Timothy J. Baek's avatar
Timothy J. Baek committed
260
261
262
263
264
265
266
267
268
269
    except Exception as err:
        raise HTTPException(500, detail=ERROR_MESSAGES.DEFAULT(err))


############################
# AddUser
############################


@router.post("/add", response_model=SigninResponse)
270
async def add_user(form_data: AddUserForm, user=Depends(get_admin_user)):
Timothy J. Baek's avatar
Timothy J. Baek committed
271
272
273
274
275
276
277
278
279
280

    if not validate_email_format(form_data.email.lower()):
        raise HTTPException(
            status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.INVALID_EMAIL_FORMAT
        )

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

    try:
281
282

        print(form_data)
Timothy J. Baek's avatar
Timothy J. Baek committed
283
284
285
286
287
288
        hashed = get_password_hash(form_data.password)
        user = Auths.insert_new_auth(
            form_data.email.lower(),
            hashed,
            form_data.name,
            form_data.profile_image_url,
289
            form_data.role,
Timothy J. Baek's avatar
Timothy J. Baek committed
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
        )

        if user:
            token = create_token(data={"id": user.id})
            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:
            raise HTTPException(500, detail=ERROR_MESSAGES.CREATE_USER_ERROR)
305
    except Exception as err:
306
307
        raise HTTPException(500, detail=ERROR_MESSAGES.DEFAULT(err))

308
309

############################
310
# GetAdminDetails
311
312
313
############################


314
315
316
317
318
319
320
@router.get("/admin/details")
async def get_admin_details(request: Request, user=Depends(get_current_user)):
    if request.app.state.config.SHOW_ADMIN_DETAILS:
        admin_email = request.app.state.config.ADMIN_EMAIL
        admin_name = None

        print(admin_email, admin_name)
321

322
323
324
325
326
327
328
329
330
        if admin_email:
            admin = Users.get_user_by_email(admin_email)
            if admin:
                admin_name = admin.name
        else:
            admin = Users.get_first_user()
            if admin:
                admin_email = admin.email
                admin_name = admin.name
331

332
333
334
335
336
337
        return {
            "name": admin_name,
            "email": admin_email,
        }
    else:
        raise HTTPException(400, detail=ERROR_MESSAGES.ACTION_PROHIBITED)
Timothy J. Baek's avatar
Timothy J. Baek committed
338
339
340


############################
341
# ToggleSignUp
Timothy J. Baek's avatar
Timothy J. Baek committed
342
343
344
############################


345
346
347
348
349
350
351
352
353
@router.get("/admin/config")
async def get_admin_config(request: Request, user=Depends(get_admin_user)):
    return {
        "SHOW_ADMIN_DETAILS": request.app.state.config.SHOW_ADMIN_DETAILS,
        "ENABLE_SIGNUP": request.app.state.config.ENABLE_SIGNUP,
        "DEFAULT_USER_ROLE": request.app.state.config.DEFAULT_USER_ROLE,
        "JWT_EXPIRES_IN": request.app.state.config.JWT_EXPIRES_IN,
        "ENABLE_COMMUNITY_SHARING": request.app.state.config.ENABLE_COMMUNITY_SHARING,
    }
Timothy J. Baek's avatar
Timothy J. Baek committed
354
355


356
357
358
359
360
361
class AdminConfig(BaseModel):
    SHOW_ADMIN_DETAILS: bool
    ENABLE_SIGNUP: bool
    DEFAULT_USER_ROLE: str
    JWT_EXPIRES_IN: str
    ENABLE_COMMUNITY_SHARING: bool
Timothy J. Baek's avatar
Timothy J. Baek committed
362
363


364
365
366
@router.post("/admin/config")
async def update_admin_config(
    request: Request, form_data: AdminConfig, user=Depends(get_admin_user)
Timothy J. Baek's avatar
Timothy J. Baek committed
367
):
368
369
    request.app.state.config.SHOW_ADMIN_DETAILS = form_data.SHOW_ADMIN_DETAILS
    request.app.state.config.ENABLE_SIGNUP = form_data.ENABLE_SIGNUP
Timothy J. Baek's avatar
Timothy J. Baek committed
370

371
372
    if form_data.DEFAULT_USER_ROLE in ["pending", "user", "admin"]:
        request.app.state.config.DEFAULT_USER_ROLE = form_data.DEFAULT_USER_ROLE
Timothy J. Baek's avatar
Timothy J. Baek committed
373
374
375
376

    pattern = r"^(-1|0|(-?\d+(\.\d+)?)(ms|s|m|h|d|w))$"

    # Check if the input string matches the pattern
377
378
379
380
381
382
383
384
385
386
387
388
389
390
    if re.match(pattern, form_data.JWT_EXPIRES_IN):
        request.app.state.config.JWT_EXPIRES_IN = form_data.JWT_EXPIRES_IN

    request.app.state.config.ENABLE_COMMUNITY_SHARING = (
        form_data.ENABLE_COMMUNITY_SHARING
    )

    return {
        "SHOW_ADMIN_DETAILS": request.app.state.config.SHOW_ADMIN_DETAILS,
        "ENABLE_SIGNUP": request.app.state.config.ENABLE_SIGNUP,
        "DEFAULT_USER_ROLE": request.app.state.config.DEFAULT_USER_ROLE,
        "JWT_EXPIRES_IN": request.app.state.config.JWT_EXPIRES_IN,
        "ENABLE_COMMUNITY_SHARING": request.app.state.config.ENABLE_COMMUNITY_SHARING,
    }
liu.vaayne's avatar
liu.vaayne committed
391
392
393
394
395
396
397
398
399
400
401


############################
# 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
402
    success = Users.update_user_api_key_by_id(user.id, api_key)
liu.vaayne's avatar
liu.vaayne committed
403
404
405
406
407
408
409
410
411
412
413
    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
414
    success = Users.update_user_api_key_by_id(user.id, None)
liu.vaayne's avatar
liu.vaayne committed
415
416
417
418
419
420
    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
421
    api_key = Users.get_user_api_key_by_id(user.id)
liu.vaayne's avatar
liu.vaayne committed
422
423
424
425
426
427
    if api_key:
        return {
            "api_key": api_key,
        }
    else:
        raise HTTPException(404, detail=ERROR_MESSAGES.API_KEY_NOT_FOUND)