Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: auto_login=off error on login and editing a user + FE tests #3471

Merged
merged 9 commits into from
Aug 21, 2024
Merged
6 changes: 5 additions & 1 deletion src/backend/base/langflow/api/v1/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,17 +90,21 @@ def patch_user(
Update an existing user's data.
"""

update_password = user_update.password is not None and user_update.password != ""

if not user.is_superuser and user_update.is_superuser:
raise HTTPException(status_code=403, detail="Permission denied")

if not user.is_superuser and user.id != user_id:
raise HTTPException(status_code=403, detail="Permission denied")
if user_update.password:
if update_password:
if not user.is_superuser:
raise HTTPException(status_code=400, detail="You can't change your password here")
user_update.password = get_password_hash(user_update.password)

if user_db := get_user_by_id(session, user_id):
if not update_password:
user_update.password = user_db.password
return update_user(user_db, user_update, session)
else:
raise HTTPException(status_code=404, detail="User not found")
Expand Down
1 change: 0 additions & 1 deletion src/frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions src/frontend/src/constants/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -879,6 +879,7 @@ export const TABS_ORDER = [
export const LANGFLOW_ACCESS_TOKEN = "access_token_lf";
export const LANGFLOW_API_TOKEN = "apikey_tkn_lflw";
export const LANGFLOW_AUTO_LOGIN_OPTION = "auto_login_lf";
export const LANGFLOW_REFRESH_TOKEN = "refresh_token_lf";

export const LANGFLOW_ACCESS_TOKEN_EXPIRE_SECONDS = 60 * 60 - 60 * 60 * 0.1;
export const LANGFLOW_ACCESS_TOKEN_EXPIRE_SECONDS_ENV =
Expand Down
10 changes: 9 additions & 1 deletion src/frontend/src/contexts/authContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
LANGFLOW_ACCESS_TOKEN,
LANGFLOW_API_TOKEN,
LANGFLOW_AUTO_LOGIN_OPTION,
LANGFLOW_REFRESH_TOKEN,
} from "@/constants/constants";
import { useGetUserData } from "@/controllers/API/queries/auth";
import useAuthStore from "@/stores/authStore";
Expand Down Expand Up @@ -76,8 +77,15 @@ export function AuthProvider({ children }): React.ReactElement {
);
}

function login(newAccessToken: string, autoLogin: string) {
function login(
newAccessToken: string,
autoLogin: string,
refreshToken?: string,
) {
cookies.set(LANGFLOW_AUTO_LOGIN_OPTION, autoLogin, { path: "/" });
if (refreshToken) {
cookies.set(LANGFLOW_REFRESH_TOKEN, refreshToken, { path: "/" });
}
setAccessToken(newAccessToken);
setIsAuthenticated(true);
getUser();
Expand Down
21 changes: 15 additions & 6 deletions src/frontend/src/modals/userManagementModal/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,21 +42,30 @@ export default function UserManagementModal({
}: inputHandlerEventType): void {
setInputState((prev) => ({ ...prev, [name]: value }));
}
console.log(data);

useEffect(() => {
if (!data) {
resetForm();
} else {
handleInput({ target: { name: "username", value: username } });
handleInput({ target: { name: "is_active", value: isActive } });
handleInput({ target: { name: "is_superuser", value: isSuperUser } });
if (open) {
if (!data) {
resetForm();
} else {
setUserName(data.username);
setIsActive(data.is_active);
setIsSuperUser(data.is_superuser);

handleInput({ target: { name: "username", value: username } });
handleInput({ target: { name: "is_active", value: isActive } });
handleInput({ target: { name: "is_superuser", value: isSuperUser } });
}
}
}, [open]);

function resetForm() {
setPassword("");
setUserName("");
setConfirmPassword("");
setIsActive(false);
setIsSuperUser(false);
}

return (
Expand Down
2 changes: 1 addition & 1 deletion src/frontend/src/pages/AdminPage/LoginPage/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export default function LoginAdminPage() {
setSelectedFolder(null);

setLoading(true);
login(res.access_token, "login");
login(res.access_token, "login", res.refresh_token);
},
onError: (error) => {
setErrorData({
Expand Down
2 changes: 1 addition & 1 deletion src/frontend/src/pages/LoginPage/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export default function LoginPage(): JSX.Element {
onSuccess: (data) => {
setSelectedFolder(null);

login(data.access_token, "login");
login(data.access_token, "login", data.refresh_token);
},
onError: (error) => {
setErrorData({
Expand Down
2 changes: 1 addition & 1 deletion src/frontend/src/types/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -417,7 +417,7 @@ export type signUpInputStateType = {

export type inputHandlerEventType = {
target: {
value: string;
value: string | boolean;
name: string;
};
};
Expand Down
6 changes: 5 additions & 1 deletion src/frontend/src/types/contexts/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@ import { Users } from "../api";

export type AuthContextType = {
accessToken: string | null;
login: (accessToken: string, autoLogin: string) => void;
login: (
accessToken: string,
autoLogin: string,
refreshToken?: string,
) => void;
userData: Users | null;
setUserData: (userData: Users | null) => void;
authenticationErrorCount: number;
Expand Down
249 changes: 249 additions & 0 deletions src/frontend/tests/end-to-end/auto-login-off.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
import { expect, test } from "@playwright/test";
import { before, beforeEach } from "node:test";

test("when auto_login is false, admin can CRUD user's and should see just your own flows", async ({
page,
}) => {
await page.route("**/api/v1/auto_login", (route) => {
route.fulfill({
status: 500,
contentType: "application/json",
body: JSON.stringify({
detail: { auto_login: false },
}),
});
});

const randomName = Math.random().toString(36).substring(5);
const randomPassword = Math.random().toString(36).substring(5);
const secondRandomName = Math.random().toString(36).substring(5);
const randomFlowName = Math.random().toString(36).substring(5);
const secondRandomFlowName = Math.random().toString(36).substring(5);

await page.goto("/");

await page.waitForSelector("text=sign in to langflow", { timeout: 30000 });

await page.getByPlaceholder("Username").fill("langflow");
await page.getByPlaceholder("Password").fill("langflow");

await page.getByRole("button", { name: "Sign In" }).click();

await page.waitForSelector('[data-testid="mainpage_title"]', {
timeout: 30000,
});

await page.waitForSelector('[id="new-project-btn"]', {
timeout: 30000,
});

await page.getByTestId("user-profile-settings").click();

await page.getByText("Admin Page", { exact: true }).click();

//CRUD an user
await page.getByText("New User", { exact: true }).click();

await page.getByPlaceholder("Username").last().fill(randomName);
await page.locator('input[name="password"]').fill(randomPassword);
await page.locator('input[name="confirmpassword"]').fill(randomPassword);

await page.waitForTimeout(1000);

await page.locator("#is_active").click();

await page.getByText("Save", { exact: true }).click();

await page.waitForSelector("text=new user added", { timeout: 30000 });

expect(await page.getByText(randomName, { exact: true }).isVisible()).toBe(
true,
);

await page.getByTestId("icon-Trash2").last().click();
await page.getByText("Delete", { exact: true }).last().click();

await page.waitForSelector("text=user deleted", { timeout: 30000 });

expect(await page.getByText(randomName, { exact: true }).isVisible()).toBe(
false,
);

await page.getByText("New User", { exact: true }).click();

await page.getByPlaceholder("Username").last().fill(randomName);
await page.locator('input[name="password"]').fill(randomPassword);
await page.locator('input[name="confirmpassword"]').fill(randomPassword);

await page.waitForTimeout(1000);

await page.locator("#is_active").click();

await page.getByText("Save", { exact: true }).click();

await page.waitForSelector("text=new user added", { timeout: 30000 });

await page.getByPlaceholder("Username").last().fill(randomName);

await page.getByTestId("icon-Pencil").last().click();

await page.getByPlaceholder("Username").last().fill(secondRandomName);

await page.getByText("Save", { exact: true }).click();

await page.waitForSelector("text=user edited", { timeout: 30000 });

await page.waitForTimeout(1000);

expect(
await page.getByText(secondRandomName, { exact: true }).isVisible(),
).toBe(true);

//user must see just your own flows
await page.getByText("My Collection", { exact: true }).last().click();

await page.waitForSelector('[id="new-project-btn"]', {
timeout: 30000,
});

let modalCount = 0;
try {
const modalTitleElement = await page?.getByTestId("modal-title");
if (modalTitleElement) {
modalCount = await modalTitleElement.count();
}
} catch (error) {
modalCount = 0;
}

while (modalCount === 0) {
await page.getByText("New Project", { exact: true }).click();
await page.waitForTimeout(3000);
modalCount = await page.getByTestId("modal-title")?.count();
}

await page.getByRole("heading", { name: "Basic Prompting" }).click();

await page.waitForSelector('[title="fit view"]', {
timeout: 100000,
});

await page.getByTitle("fit view").click();
await page.getByTitle("zoom out").click();

await page.getByTestId("flow-configuration-button").click();
await page.getByText("Settings", { exact: true }).last().click();

await page.getByPlaceholder("Flow Name").fill(randomFlowName);

await page.getByText("Save", { exact: true }).click();

await page.waitForSelector('[data-testid="icon-ChevronLeft"]', {
timeout: 100000,
});

await page.getByTestId("icon-ChevronLeft").first().click();

await page.waitForSelector('[id="new-project-btn"]', {
timeout: 30000,
});

expect(
await page.getByText(randomFlowName, { exact: true }).last().isVisible(),
).toBe(true);

await page.getByTestId("user-profile-settings").click();

await page.getByText("Log Out", { exact: true }).click();

await page.waitForSelector("text=sign in to langflow", { timeout: 30000 });

await page.getByPlaceholder("Username").fill(secondRandomName);
await page.getByPlaceholder("Password").fill(randomPassword);
await page.waitForTimeout(1000);

await page.getByRole("button", { name: "Sign In" }).click();

await page.waitForSelector('[data-testid="mainpage_title"]', {
timeout: 30000,
});

await page.waitForSelector('[id="new-project-btn"]', {
timeout: 30000,
});

expect(
(
await page.waitForSelector("text=this folder is empty", {
timeout: 30000,
})
).isVisible(),
);

while (modalCount === 0) {
await page.getByText("New Project", { exact: true }).click();
await page.waitForTimeout(3000);
modalCount = await page.getByTestId("modal-title")?.count();
}

await page.waitForSelector('[id="new-project-btn"]', {
timeout: 30000,
});

await page.getByText("New Project", { exact: true }).click();

await page.getByRole("heading", { name: "Basic Prompting" }).click();

await page.waitForSelector('[title="fit view"]', {
timeout: 100000,
});

await page.getByTitle("fit view").click();
await page.getByTitle("zoom out").click();

await page.getByTestId("flow-configuration-button").click();
await page.getByText("Settings", { exact: true }).last().click();

await page.getByPlaceholder("Flow Name").fill(secondRandomFlowName);

await page.getByText("Save", { exact: true }).click();

await page.waitForSelector('[data-testid="icon-ChevronLeft"]', {
timeout: 100000,
});

await page.getByTestId("icon-ChevronLeft").first().click();

expect(
await page.getByText(secondRandomFlowName, { exact: true }).isVisible(),
).toBe(true);
expect(
await page.getByText(randomFlowName, { exact: true }).isVisible(),
).toBe(false);

await page.getByTestId("user-profile-settings").click();

await page.getByText("Log Out", { exact: true }).click();

await page.waitForSelector("text=sign in to langflow", { timeout: 30000 });

await page.getByPlaceholder("Username").fill("langflow");
await page.getByPlaceholder("Password").fill("langflow");

await page.getByRole("button", { name: "Sign In" }).click();

await page.waitForSelector('[data-testid="mainpage_title"]', {
timeout: 30000,
});

await page.waitForSelector('[id="new-project-btn"]', {
timeout: 30000,
});

expect(
await page.getByText(secondRandomFlowName, { exact: true }).isVisible(),
).toBe(false);
expect(
await page.getByText(randomFlowName, { exact: true }).isVisible(),
).toBe(true);
});
Loading