Merge branch 'login' of personal:logspace-ai/langflow into login

This commit is contained in:
anovazzi1 2023-08-15 16:01:08 -03:00
commit 4a7ccda51c
11 changed files with 294 additions and 172 deletions

View file

@ -6,18 +6,19 @@ from fastapi.security import OAuth2PasswordBearer
from fastapi import Depends, HTTPException, status from fastapi import Depends, HTTPException, status
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
from langflow.database.models.token import TokenData
from langflow.database.models.user import get_user, User
from langflow.services.utils import get_session from langflow.services.utils import get_session
from langflow.database.models.token import TokenData
from langflow.database.models.user import User, get_user_by_username
# TODO: Move to env - Test propose!!!!! # TODO: Move to env - JUST FOR TEST!!!!!
SECRET_KEY = "698619adad2d916f1f32d264540976964b3c0d3828e0870a65add5800a8cc6b9" SECRET_KEY = "698619adad2d916f1f32d264540976964b3c0d3828e0870a65add5800a8cc6b9"
ALGORITHM = "HS256" ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30 ACCESS_TOKEN_EXPIRE_MINUTES = 60
REFRESH_TOKEN_EXPIRE_MINUTES = 70
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") oauth2_scheme = OAuth2PasswordBearer(tokenUrl="login")
def verify_password(plain_password, hashed_password): def verify_password(plain_password, hashed_password):
@ -28,18 +29,57 @@ def get_password_hash(password):
return pwd_context.hash(password) return pwd_context.hash(password)
def create_access_token(data: dict, expires_delta: timedelta = None): # type: ignore def create_token(data: dict, expires_delta: timedelta):
to_encode = data.copy() to_encode = data.copy()
if expires_delta:
expire = datetime.now(timezone.utc) + expires_delta expire = datetime.now(timezone.utc) + expires_delta
else:
expire = datetime.now(timezone.utc) + timedelta(minutes=15)
to_encode["exp"] = expire to_encode["exp"] = expire
return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
def create_user_tokens(username: str) -> dict:
access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
access_token = create_token(
data={"sub": username},
expires_delta=access_token_expires,
)
refresh_token_expires = timedelta(minutes=REFRESH_TOKEN_EXPIRE_MINUTES)
refresh_token = create_token(
data={"sub": username, "type": "rf"},
expires_delta=refresh_token_expires,
)
return {
"access_token": access_token,
"refresh_token": refresh_token,
"token_type": "bearer",
}
def create_refresh_token(refresh_token: str):
try:
payload = jwt.decode(refresh_token, SECRET_KEY, algorithms=[ALGORITHM])
username: str = payload.get("sub") # type: ignore
token_type: str = payload.get("type") # type: ignore
if username is None or token_type is None:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid refresh token"
)
return create_user_tokens(username)
except JWTError as e:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid refresh token",
) from e
def authenticate_user(db: Session, username: str, password: str): def authenticate_user(db: Session, username: str, password: str):
if user := get_user(db, username): if user := get_user_by_username(db, username):
return user if verify_password(password, user.password) else False return user if verify_password(password, user.password) else False
else: else:
return False return False
@ -47,7 +87,7 @@ def authenticate_user(db: Session, username: str, password: str):
async def get_current_user( async def get_current_user(
token: Annotated[str, Depends(oauth2_scheme)], db: Session = Depends(get_session) token: Annotated[str, Depends(oauth2_scheme)], db: Session = Depends(get_session)
): ) -> User:
credentials_exception = HTTPException( credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials", detail="Could not validate credentials",
@ -56,13 +96,15 @@ async def get_current_user(
try: try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
username: str = payload.get("sub") # type: ignore username: str = payload.get("sub") # type: ignore
if username is None: token_type: str = payload.get("type") # type: ignore
if username is None or token_type:
raise credentials_exception raise credentials_exception
token_data = TokenData(username=username) token_data = TokenData(username=username)
except JWTError as e: except JWTError as e:
raise credentials_exception from e raise credentials_exception from e
user = get_user(db, token_data.username) # type: ignore user = get_user_by_username(db, token_data.username) # type: ignore
if user is None: if user is None:
raise credentials_exception raise credentials_exception
return user return user

View file

@ -3,6 +3,7 @@ from pydantic import BaseModel
class Token(BaseModel): class Token(BaseModel):
access_token: str access_token: str
refresh_token: str
token_type: str token_type: str

View file

@ -32,6 +32,17 @@ class UserListModel(SQLModel):
updated_at: datetime = Field() updated_at: datetime = Field()
def get_user(db: Session, username: str) -> User: class UserPatchModel(SQLModel):
username: str = Field()
is_disabled: bool = Field()
is_superuser: bool = Field()
def get_user_by_username(db: Session, username: str) -> User:
db_user = db.query(User).filter(User.username == username).first() db_user = db.query(User).filter(User.username == username).first()
return User.from_orm(db_user) if db_user else None # type: ignore return User.from_orm(db_user) if db_user else None # type: ignore
def get_user_by_id(db: Session, id: UUID) -> User:
db_user = db.query(User).filter(User.id == id).first()
return User.from_orm(db_user) if db_user else None # type: ignore

View file

@ -1,41 +1,39 @@
from datetime import timedelta from sqlalchemy.orm import Session
from fastapi import APIRouter, Depends, HTTPException, status from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordRequestForm from fastapi.security import OAuth2PasswordRequestForm
from langflow.services.utils import get_session
from langflow.database.models.token import Token from langflow.database.models.token import Token
from langflow.auth.auth import ( from langflow.auth.auth import (
ACCESS_TOKEN_EXPIRE_MINUTES,
authenticate_user, authenticate_user,
create_access_token, create_user_tokens,
create_refresh_token,
) )
from sqlalchemy.orm import Session
from langflow.services.utils import get_session
from langflow.database.models.user import User
router = APIRouter() router = APIRouter()
def create_user_token(user: User) -> dict: @router.post("/login", response_model=Token)
access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
access_token = create_access_token(
data={"sub": user.username},
expires_delta=access_token_expires,
)
return {"access_token": access_token, "token_type": "bearer"}
@router.post("/token", response_model=Token)
async def login_to_get_access_token( async def login_to_get_access_token(
form_data: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(get_session) form_data: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(get_session)
): ):
if user := authenticate_user(db, form_data.username, form_data.password): if user := authenticate_user(db, form_data.username, form_data.password):
return create_user_token(user) return create_user_tokens(user.username)
else: else:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password", detail="Incorrect username or password",
headers={"WWW-Authenticate": "Bearer"}, headers={"WWW-Authenticate": "Bearer"},
) )
@router.post("/refresh")
async def refresh_token(token: str):
if token:
return create_refresh_token(token)
else:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid refresh token",
headers={"WWW-Authenticate": "Bearer"},
)

View file

@ -1,24 +1,52 @@
from typing import List from typing import List
from sqlmodel import Session, select from uuid import UUID
from datetime import timezone, datetime
from sqlalchemy.exc import IntegrityError from sqlalchemy.exc import IntegrityError
from fastapi import APIRouter, Depends, HTTPException from fastapi import APIRouter, Depends, HTTPException
from sqlmodel import Session, select
from langflow.services.utils import get_session from langflow.services.utils import get_session
from langflow.auth.auth import get_current_active_user from langflow.auth.auth import get_current_active_user, get_password_hash
from langflow.database.models.user import UserAddModel, UserListModel, User from langflow.database.models.user import (
User,
from passlib.context import CryptContext UserAddModel,
UserListModel,
UserPatchModel,
get_user_by_id,
get_user_by_username,
)
router = APIRouter(tags=["Login"]) router = APIRouter(tags=["Login"])
def get_password_hash(password): @router.post("/user", response_model=UserListModel)
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") def add_user(
return pwd_context.hash(password) user: UserAddModel,
_: Session = Depends(get_current_active_user),
db: Session = Depends(get_session),
) -> User:
"""
Add a new user to the database.
"""
new_user = User(**user.dict())
try:
new_user.password = get_password_hash(user.password)
db.add(new_user)
db.commit()
db.refresh(new_user)
except IntegrityError as e:
db.rollback()
raise HTTPException(status_code=400, detail="User exists") from e
return new_user
@router.get("/user", response_model=UserListModel) @router.get("/user", response_model=UserListModel)
def read_current_user(current_user: User = Depends(get_current_active_user)): def read_current_user(current_user: User = Depends(get_current_active_user)) -> User:
"""
Retrieve the current user's data.
"""
return current_user return current_user
@ -28,52 +56,88 @@ def read_all_users(
limit: int = 10, limit: int = 10,
_: Session = Depends(get_current_active_user), _: Session = Depends(get_current_active_user),
db: Session = Depends(get_session), db: Session = Depends(get_session),
): ) -> List[UserListModel]:
query = select(User) """
query = query.offset(skip).limit(limit) Retrieve a list of users from the database with pagination.
"""
query = select(User).offset(skip).limit(limit)
users = db.execute(query).fetchall()
return db.execute(query).fetchall() return [UserListModel(**dict(user.User)) for user in users]
@router.post("/user", response_model=User) @router.patch("/user/{user_id}", response_model=UserListModel)
def add_user( def update_user(
user: UserAddModel, user_id: UUID,
user: UserPatchModel,
_: Session = Depends(get_current_active_user), _: Session = Depends(get_current_active_user),
db: Session = Depends(get_session), db: Session = Depends(get_session),
): ) -> User:
new_user = User(**user.dict()) """
try: Update an existing user's data.
new_user.password = get_password_hash(user.password) """
user_db = get_user_by_username(db, user.username)
if user_db and user_db.id != user_id:
raise HTTPException(status_code=409, detail="Username already exists")
db.add(new_user) user_db = get_user_by_id(db, user_id)
if not user_db:
raise HTTPException(status_code=404, detail="User not found")
try:
user_data = user.dict(exclude_unset=True)
for key, value in user_data.items():
setattr(user_db, key, value)
user_db.updated_at = datetime.now(timezone.utc)
user_db = db.merge(user_db)
db.commit() db.commit()
db.refresh(new_user) if db.identity_key(instance=user_db) is not None:
db.refresh(user_db)
except IntegrityError as e: except IntegrityError as e:
db.rollback() db.rollback()
raise HTTPException( raise HTTPException(status_code=400, detail=str(e)) from e
status_code=400,
detail="User exists",
) from e
return new_user return user_db
# TODO: Remove - Just for testing purposes @router.delete("/user/{user_id}")
def delete_user(
user_id: UUID,
_: Session = Depends(get_current_active_user),
db: Session = Depends(get_session),
) -> dict:
"""
Delete a user from the database.
"""
user_db = db.query(User).filter(User.id == user_id).first()
if not user_db:
raise HTTPException(status_code=404, detail="User not found")
db.delete(user_db)
db.commit()
return {"detail": "User deleted"}
# TODO: REMOVE - Just for testing purposes
@router.post("/super_user", response_model=User) @router.post("/super_user", response_model=User)
def add_super_user_to_testing_purposes(db: Session = Depends(get_session)): def add_super_user_for_testing_purposes_delete_me_before_merge_into_dev(
db: Session = Depends(get_session),
) -> User:
"""
Add a superuser for testing purposes.
(This should be removed in production)
"""
new_user = User(username="superuser", password="12345", is_superuser=True) new_user = User(username="superuser", password="12345", is_superuser=True)
try: try:
new_user.password = get_password_hash(new_user.password) new_user.password = get_password_hash(new_user.password)
db.add(new_user) db.add(new_user)
db.commit() db.commit()
db.refresh(new_user) db.refresh(new_user)
except IntegrityError as e: except IntegrityError as e:
db.rollback() db.rollback()
raise HTTPException( raise HTTPException(status_code=400, detail="User exists") from e
status_code=400,
detail="User exists",
) from e
return new_user return new_user

View file

@ -1,3 +1,4 @@
import * as Form from "@radix-ui/react-form";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { InputComponentType } from "../../types/components"; import { InputComponentType } from "../../types/components";
import { classNames } from "../../utils/utils"; import { classNames } from "../../utils/utils";
@ -7,6 +8,8 @@ export default function InputComponent({
value, value,
onChange, onChange,
disabled, disabled,
required = false,
isForm = false,
password, password,
editNode = false, editNode = false,
placeholder = "Type something...", placeholder = "Type something...",
@ -23,11 +26,16 @@ export default function InputComponent({
return ( return (
<div className="relative w-full"> <div className="relative w-full">
{isForm ? (
<Form.Control asChild>
<Input <Input
value={value} value={value}
disabled={disabled} disabled={disabled}
required={required}
className={classNames( className={classNames(
password && !pwdVisible && value !== "" ? " text-clip password " : "", password && !pwdVisible && value !== ""
? " text-clip password "
: "",
editNode ? " input-edit-node " : "", editNode ? " input-edit-node " : "",
password && editNode ? "pr-8" : "", password && editNode ? "pr-8" : "",
password && !editNode ? "pr-10" : "", password && !editNode ? "pr-10" : "",
@ -38,6 +46,27 @@ export default function InputComponent({
onChange(e.target.value); onChange(e.target.value);
}} }}
/> />
</Form.Control>
) : (
<Input
value={value}
disabled={disabled}
required={required}
className={classNames(
password && !pwdVisible && value !== ""
? " text-clip password "
: "",
editNode ? " input-edit-node " : "",
password && editNode ? "pr-8" : "",
password && !editNode ? "pr-10" : "",
className
)}
placeholder={password && editNode ? "Key" : placeholder}
onChange={(e) => {
onChange(e.target.value);
}}
/>
)}
{password && ( {password && (
<button <button
className={classNames( className={classNames(

View file

@ -4,6 +4,7 @@ import { Button } from "../../components/ui/button";
import { UserManagementType } from "../../types/components"; import { UserManagementType } from "../../types/components";
import { nodeIconsLucide } from "../../utils/styleUtils"; import { nodeIconsLucide } from "../../utils/styleUtils";
import BaseModal from "../baseModal"; import BaseModal from "../baseModal";
import InputComponent from "../../components/inputComponent";
export default function UserManagementModal({ export default function UserManagementModal({
title, title,
@ -84,6 +85,7 @@ export default function UserManagementModal({
value={username} value={username}
className="primary-input" className="primary-input"
required required
placeholder="Username"
/> />
</Form.Control> </Form.Control>
<Form.Message match="valueMissing" className="field-invalid"> <Form.Message match="valueMissing" className="field-invalid">
@ -109,16 +111,17 @@ export default function UserManagementModal({
<span className="font-medium text-destructive">*</span> <span className="font-medium text-destructive">*</span>
</Form.Label> </Form.Label>
</div> </div>
<Form.Control asChild> <InputComponent
<input
onChange={(input) => { onChange={(input) => {
setPassword(input.target.value); setPassword(input);
}} }}
value={password} value={password}
password={true}
isForm
className="primary-input" className="primary-input"
required required
placeholder="Password"
/> />
</Form.Control>
<Form.Message className="field-invalid" match="valueMissing"> <Form.Message className="field-invalid" match="valueMissing">
Please enter a password Please enter a password
</Form.Message> </Form.Message>
@ -148,16 +151,17 @@ export default function UserManagementModal({
<span className="font-medium text-destructive">*</span> <span className="font-medium text-destructive">*</span>
</Form.Label> </Form.Label>
</div> </div>
<Form.Control asChild> <InputComponent
<input
onChange={(input) => { onChange={(input) => {
setConfirmPassword(input.target.value); setConfirmPassword(input);
}} }}
value={confirmPassword} value={confirmPassword}
password={true}
isForm
className="primary-input" className="primary-input"
required required
placeholder="Confirm your password"
/> />
</Form.Control>
<Form.Message className="field-invalid" match="valueMissing"> <Form.Message className="field-invalid" match="valueMissing">
Please confirm your password Please confirm your password
</Form.Message> </Form.Message>

View file

@ -39,20 +39,8 @@ export default function LoginPage(): JSX.Element {
<div className="flex w-72 flex-col items-center justify-center gap-2"> <div className="flex w-72 flex-col items-center justify-center gap-2">
<span className="mb-4 text-5xl">⛓️</span> <span className="mb-4 text-5xl">⛓️</span>
<span className="mb-6 text-2xl font-semibold text-primary"> <span className="mb-6 text-2xl font-semibold text-primary">
Sign in to LangFlow Sign in to Langflow
</span> </span>
<div className="flex w-full items-center justify-center gap-2">
<Button variant="primary" className="w-full py-6">
<IconComponent name="FaApple" className="h-6 w-6" />
</Button>
<Button variant="primary" className="w-full py-6">
<IconComponent name="FaGithub" className="h-6 w-6" />
</Button>
<Button variant="primary" className="w-full py-6">
<IconComponent name="GoogleSearchRun" className="h-6 w-6" />
</Button>
</div>
<span className="text-sm text-muted-foreground">or</span>
<div className="mb-3 w-full"> <div className="mb-3 w-full">
<Form.Field name="username"> <Form.Field name="username">
<Form.Label className="data-[invalid]:label-invalid"> <Form.Label className="data-[invalid]:label-invalid">
@ -77,32 +65,26 @@ export default function LoginPage(): JSX.Element {
</Form.Field> </Form.Field>
</div> </div>
<div className="mb-3 w-full"> <div className="mb-3 w-full">
<Form.Field name="password" serverInvalid={password === ""}> <Form.Field name="password">
<Form.Label className="data-[invalid]:label-invalid"> <Form.Label className="data-[invalid]:label-invalid">
Password <span className="font-medium text-destructive">*</span> Password <span className="font-medium text-destructive">*</span>
</Form.Label> </Form.Label>
<Form.Control asChild>
<InputComponent <InputComponent
onChange={(value) => { onChange={(value) => {
handleInput({ target: { name: "password", value } }); handleInput({ target: { name: "password", value } });
}} }}
value={password} value={password}
isForm
password={true} password={true}
required
placeholder="Password" placeholder="Password"
className="w-full" className="w-full"
/> />
</Form.Control>
<Form.Message className="field-invalid" match="valueMissing"> <Form.Message className="field-invalid" match="valueMissing">
Please enter your password Please enter your password
</Form.Message> </Form.Message>
{password === "" && (
<Form.Message className="field-invalid">
Please enter a valid password
</Form.Message>
)}
</Form.Field> </Form.Field>
</div> </div>
<div className="w-full"> <div className="w-full">
@ -111,7 +93,7 @@ export default function LoginPage(): JSX.Element {
</Form.Submit> </Form.Submit>
</div> </div>
<div className="w-full"> <div className="w-full">
<Link to="/signup"> <Link to="">
<Button className="w-full" variant="outline"> <Button className="w-full" variant="outline">
Don't have an account?&nbsp;<b>Sign Up</b> Don't have an account?&nbsp;<b>Sign Up</b>
</Button> </Button>

View file

@ -39,20 +39,8 @@ export default function SignUp(): JSX.Element {
<div className="flex w-72 flex-col items-center justify-center gap-2"> <div className="flex w-72 flex-col items-center justify-center gap-2">
<span className="mb-4 text-5xl">⛓️</span> <span className="mb-4 text-5xl">⛓️</span>
<span className="mb-6 text-2xl font-semibold text-primary"> <span className="mb-6 text-2xl font-semibold text-primary">
Sign up to LangFlow Sign up to Langflow
</span> </span>
<div className="flex w-full items-center justify-center gap-2">
<Button variant="primary" className="w-full py-6">
<IconComponent name="FaApple" className="h-6 w-6" />
</Button>
<Button variant="primary" className="w-full py-6">
<IconComponent name="FaGithub" className="h-6 w-6" />
</Button>
<Button variant="primary" className="w-full py-6">
<IconComponent name="GoogleSearchRun" className="h-6 w-6" />
</Button>
</div>
<span className="text-sm text-muted-foreground">or</span>
<div className="mb-3 w-full"> <div className="mb-3 w-full">
<Form.Field name="username"> <Form.Field name="username">
<Form.Label className="data-[invalid]:label-invalid"> <Form.Label className="data-[invalid]:label-invalid">
@ -81,18 +69,17 @@ export default function SignUp(): JSX.Element {
<Form.Label className="data-[invalid]:label-invalid"> <Form.Label className="data-[invalid]:label-invalid">
Password <span className="font-medium text-destructive">*</span> Password <span className="font-medium text-destructive">*</span>
</Form.Label> </Form.Label>
<Form.Control asChild>
<InputComponent <InputComponent
onChange={(value) => { onChange={(value) => {
handleInput({ target: { name: "password", value } }); handleInput({ target: { name: "password", value } });
}} }}
value={password} value={password}
isForm
password={true} password={true}
required
placeholder="Password" placeholder="Password"
className="w-full" className="w-full"
/> />
</Form.Control>
<Form.Message className="field-invalid" match="valueMissing"> <Form.Message className="field-invalid" match="valueMissing">
Please enter a password Please enter a password
@ -115,15 +102,17 @@ export default function SignUp(): JSX.Element {
<span className="font-medium text-destructive">*</span> <span className="font-medium text-destructive">*</span>
</Form.Label> </Form.Label>
<Form.Control asChild>
<InputComponent <InputComponent
onChange={(value) => { onChange={(value) => {
handleInput({ target: { name: "cnfPassword", value } }); handleInput({ target: { name: "cnfPassword", value } });
}} }}
value={cnfPassword} value={cnfPassword}
isForm
password={true} password={true}
required
placeholder="Confirm your password"
className="w-full"
/> />
</Form.Control>
<Form.Message className="field-invalid" match="valueMissing"> <Form.Message className="field-invalid" match="valueMissing">
Please confirm your password Please confirm your password

View file

@ -19,7 +19,7 @@ const Router = () => {
<Route path="*" element={<HomePage />} /> <Route path="*" element={<HomePage />} />
<Route path="/login" element={<LoginPage />} /> <Route path="/login" element={<LoginPage />} />
<Route path="/signup" element={<SignUp />} /> {/* <Route path="/signup" element={<SignUp />} /> */}
<Route path="/login/admin" element={<LoginAdminPage />} /> <Route path="/login/admin" element={<LoginAdminPage />} />
<Route path="/admin" element={<AdminPage />} /> <Route path="/admin" element={<AdminPage />} />

View file

@ -7,6 +7,8 @@ export type InputComponentType = {
disabled?: boolean; disabled?: boolean;
onChange: (value: string) => void; onChange: (value: string) => void;
password: boolean; password: boolean;
required?: boolean;
isForm?: boolean;
editNode?: boolean; editNode?: boolean;
onChangePass?: (value: boolean | boolean) => void; onChangePass?: (value: boolean | boolean) => void;
showPass?: boolean; showPass?: boolean;