mirror of
https://github.com/Significant-Gravitas/Auto-GPT.git
synced 2025-01-07 03:17:23 +08:00
Merge branch 'dev' into ntindle/open-2032-re-enable-getredditpostblock-sendemailblock
This commit is contained in:
commit
c0a5a01311
@ -2,6 +2,7 @@ import autogpt_libs.auth.depends
|
||||
import autogpt_libs.auth.middleware
|
||||
import fastapi
|
||||
import fastapi.testclient
|
||||
import pytest
|
||||
import pytest_mock
|
||||
|
||||
import backend.server.v2.library.db
|
||||
@ -80,6 +81,7 @@ def test_get_library_agents_error(mocker: pytest_mock.MockFixture):
|
||||
mock_db_call.assert_called_once_with("test-user-id")
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="Mocker Not implemented")
|
||||
def test_add_agent_to_library_success(mocker: pytest_mock.MockFixture):
|
||||
mock_db_call = mocker.patch("backend.server.v2.library.db.add_agent_to_library")
|
||||
mock_db_call.return_value = None
|
||||
@ -91,6 +93,7 @@ def test_add_agent_to_library_success(mocker: pytest_mock.MockFixture):
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="Mocker Not implemented")
|
||||
def test_add_agent_to_library_error(mocker: pytest_mock.MockFixture):
|
||||
mock_db_call = mocker.patch("backend.server.v2.library.db.add_agent_to_library")
|
||||
mock_db_call.side_effect = Exception("Test error")
|
||||
|
@ -31,7 +31,7 @@ async def get_store_agents(
|
||||
sanitized_query = search_query.strip()
|
||||
if not sanitized_query or len(sanitized_query) > 100: # Reasonable length limit
|
||||
raise backend.server.v2.store.exceptions.DatabaseError(
|
||||
"Invalid search query"
|
||||
f"Invalid search query: len({len(sanitized_query)}) query: {search_query}"
|
||||
)
|
||||
|
||||
# Escape special SQL characters
|
||||
@ -449,6 +449,11 @@ async def create_store_submission(
|
||||
)
|
||||
|
||||
try:
|
||||
# Sanitize slug to only allow letters and hyphens
|
||||
slug = "".join(
|
||||
c if c.isalpha() or c == "-" or c.isnumeric() else "" for c in slug
|
||||
).lower()
|
||||
|
||||
# First verify the agent belongs to this user
|
||||
agent = await prisma.models.AgentGraph.prisma().find_first(
|
||||
where=prisma.types.AgentGraphWhereInput(
|
||||
@ -636,7 +641,12 @@ async def update_or_create_profile(
|
||||
logger.info(f"Updating profile for user {user_id} data: {profile}")
|
||||
|
||||
try:
|
||||
# Check if profile exists for user
|
||||
# Sanitize username to only allow letters and hyphens
|
||||
username = "".join(
|
||||
c if c.isalpha() or c == "-" or c.isnumeric() else ""
|
||||
for c in profile.username
|
||||
).lower()
|
||||
|
||||
existing_profile = await prisma.models.Profile.prisma().find_first(
|
||||
where={"userId": user_id}
|
||||
)
|
||||
@ -651,7 +661,7 @@ async def update_or_create_profile(
|
||||
data={
|
||||
"userId": user_id,
|
||||
"name": profile.name,
|
||||
"username": profile.username.lower(),
|
||||
"username": username,
|
||||
"description": profile.description,
|
||||
"links": profile.links or [],
|
||||
"avatarUrl": profile.avatar_url,
|
||||
@ -676,7 +686,7 @@ async def update_or_create_profile(
|
||||
if profile.name is not None:
|
||||
update_data["name"] = profile.name
|
||||
if profile.username is not None:
|
||||
update_data["username"] = profile.username.lower()
|
||||
update_data["username"] = username
|
||||
if profile.description is not None:
|
||||
update_data["description"] = profile.description
|
||||
if profile.links is not None:
|
||||
|
@ -23,12 +23,16 @@ router = fastapi.APIRouter()
|
||||
##############################################
|
||||
|
||||
|
||||
@router.get("/profile", tags=["store", "private"])
|
||||
@router.get(
|
||||
"/profile",
|
||||
tags=["store", "private"],
|
||||
response_model=backend.server.v2.store.model.ProfileDetails,
|
||||
)
|
||||
async def get_profile(
|
||||
user_id: typing.Annotated[
|
||||
str, fastapi.Depends(autogpt_libs.auth.depends.get_user_id)
|
||||
]
|
||||
) -> backend.server.v2.store.model.ProfileDetails:
|
||||
):
|
||||
"""
|
||||
Get the profile details for the authenticated user.
|
||||
"""
|
||||
@ -37,20 +41,24 @@ async def get_profile(
|
||||
return profile
|
||||
except Exception:
|
||||
logger.exception("Exception occurred whilst getting user profile")
|
||||
raise
|
||||
return fastapi.responses.JSONResponse(
|
||||
status_code=500,
|
||||
content={"detail": "An error occurred while retrieving the user profile"},
|
||||
)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/profile",
|
||||
tags=["store", "private"],
|
||||
dependencies=[fastapi.Depends(autogpt_libs.auth.middleware.auth_middleware)],
|
||||
response_model=backend.server.v2.store.model.CreatorDetails,
|
||||
)
|
||||
async def update_or_create_profile(
|
||||
profile: backend.server.v2.store.model.Profile,
|
||||
user_id: typing.Annotated[
|
||||
str, fastapi.Depends(autogpt_libs.auth.depends.get_user_id)
|
||||
],
|
||||
) -> backend.server.v2.store.model.CreatorDetails:
|
||||
):
|
||||
"""
|
||||
Update the store profile for the authenticated user.
|
||||
|
||||
@ -71,7 +79,10 @@ async def update_or_create_profile(
|
||||
return updated_profile
|
||||
except Exception:
|
||||
logger.exception("Exception occurred whilst updating profile")
|
||||
raise
|
||||
return fastapi.responses.JSONResponse(
|
||||
status_code=500,
|
||||
content={"detail": "An error occurred while updating the user profile"},
|
||||
)
|
||||
|
||||
|
||||
##############################################
|
||||
@ -79,7 +90,11 @@ async def update_or_create_profile(
|
||||
##############################################
|
||||
|
||||
|
||||
@router.get("/agents", tags=["store", "public"])
|
||||
@router.get(
|
||||
"/agents",
|
||||
tags=["store", "public"],
|
||||
response_model=backend.server.v2.store.model.StoreAgentsResponse,
|
||||
)
|
||||
async def get_agents(
|
||||
featured: bool = False,
|
||||
creator: str | None = None,
|
||||
@ -88,7 +103,7 @@ async def get_agents(
|
||||
category: str | None = None,
|
||||
page: int = 1,
|
||||
page_size: int = 20,
|
||||
) -> backend.server.v2.store.model.StoreAgentsResponse:
|
||||
):
|
||||
"""
|
||||
Get a paginated list of agents from the store with optional filtering and sorting.
|
||||
|
||||
@ -138,13 +153,18 @@ async def get_agents(
|
||||
return agents
|
||||
except Exception:
|
||||
logger.exception("Exception occured whilst getting store agents")
|
||||
raise
|
||||
return fastapi.responses.JSONResponse(
|
||||
status_code=500,
|
||||
content={"detail": "An error occurred while retrieving the store agents"},
|
||||
)
|
||||
|
||||
|
||||
@router.get("/agents/{username}/{agent_name}", tags=["store", "public"])
|
||||
async def get_agent(
|
||||
username: str, agent_name: str
|
||||
) -> backend.server.v2.store.model.StoreAgentDetails:
|
||||
@router.get(
|
||||
"/agents/{username}/{agent_name}",
|
||||
tags=["store", "public"],
|
||||
response_model=backend.server.v2.store.model.StoreAgentDetails,
|
||||
)
|
||||
async def get_agent(username: str, agent_name: str):
|
||||
"""
|
||||
This is only used on the AgentDetails Page
|
||||
|
||||
@ -153,20 +173,26 @@ async def get_agent(
|
||||
try:
|
||||
username = urllib.parse.unquote(username).lower()
|
||||
# URL decode the agent name since it comes from the URL path
|
||||
agent_name = urllib.parse.unquote(agent_name)
|
||||
agent_name = urllib.parse.unquote(agent_name).lower()
|
||||
agent = await backend.server.v2.store.db.get_store_agent_details(
|
||||
username=username, agent_name=agent_name
|
||||
)
|
||||
return agent
|
||||
except Exception:
|
||||
logger.exception("Exception occurred whilst getting store agent details")
|
||||
raise
|
||||
return fastapi.responses.JSONResponse(
|
||||
status_code=500,
|
||||
content={
|
||||
"detail": "An error occurred while retrieving the store agent details"
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/agents/{username}/{agent_name}/review",
|
||||
tags=["store"],
|
||||
dependencies=[fastapi.Depends(autogpt_libs.auth.middleware.auth_middleware)],
|
||||
response_model=backend.server.v2.store.model.StoreReview,
|
||||
)
|
||||
async def create_review(
|
||||
username: str,
|
||||
@ -175,7 +201,7 @@ async def create_review(
|
||||
user_id: typing.Annotated[
|
||||
str, fastapi.Depends(autogpt_libs.auth.depends.get_user_id)
|
||||
],
|
||||
) -> backend.server.v2.store.model.StoreReview:
|
||||
):
|
||||
"""
|
||||
Create a review for a store agent.
|
||||
|
||||
@ -202,7 +228,10 @@ async def create_review(
|
||||
return created_review
|
||||
except Exception:
|
||||
logger.exception("Exception occurred whilst creating store review")
|
||||
raise
|
||||
return fastapi.responses.JSONResponse(
|
||||
status_code=500,
|
||||
content={"detail": "An error occurred while creating the store review"},
|
||||
)
|
||||
|
||||
|
||||
##############################################
|
||||
@ -210,14 +239,18 @@ async def create_review(
|
||||
##############################################
|
||||
|
||||
|
||||
@router.get("/creators", tags=["store", "public"])
|
||||
@router.get(
|
||||
"/creators",
|
||||
tags=["store", "public"],
|
||||
response_model=backend.server.v2.store.model.CreatorsResponse,
|
||||
)
|
||||
async def get_creators(
|
||||
featured: bool = False,
|
||||
search_query: str | None = None,
|
||||
sorted_by: str | None = None,
|
||||
page: int = 1,
|
||||
page_size: int = 20,
|
||||
) -> backend.server.v2.store.model.CreatorsResponse:
|
||||
):
|
||||
"""
|
||||
This is needed for:
|
||||
- Home Page Featured Creators
|
||||
@ -251,11 +284,20 @@ async def get_creators(
|
||||
return creators
|
||||
except Exception:
|
||||
logger.exception("Exception occurred whilst getting store creators")
|
||||
raise
|
||||
return fastapi.responses.JSONResponse(
|
||||
status_code=500,
|
||||
content={"detail": "An error occurred while retrieving the store creators"},
|
||||
)
|
||||
|
||||
|
||||
@router.get("/creator/{username}", tags=["store", "public"])
|
||||
async def get_creator(username: str) -> backend.server.v2.store.model.CreatorDetails:
|
||||
@router.get(
|
||||
"/creator/{username}",
|
||||
tags=["store", "public"],
|
||||
response_model=backend.server.v2.store.model.CreatorDetails,
|
||||
)
|
||||
async def get_creator(
|
||||
username: str,
|
||||
):
|
||||
"""
|
||||
Get the details of a creator
|
||||
- Creator Details Page
|
||||
@ -268,7 +310,12 @@ async def get_creator(username: str) -> backend.server.v2.store.model.CreatorDet
|
||||
return creator
|
||||
except Exception:
|
||||
logger.exception("Exception occurred whilst getting creator details")
|
||||
raise
|
||||
return fastapi.responses.JSONResponse(
|
||||
status_code=500,
|
||||
content={
|
||||
"detail": "An error occurred while retrieving the creator details"
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
############################################
|
||||
@ -278,31 +325,36 @@ async def get_creator(username: str) -> backend.server.v2.store.model.CreatorDet
|
||||
"/myagents",
|
||||
tags=["store", "private"],
|
||||
dependencies=[fastapi.Depends(autogpt_libs.auth.middleware.auth_middleware)],
|
||||
response_model=backend.server.v2.store.model.MyAgentsResponse,
|
||||
)
|
||||
async def get_my_agents(
|
||||
user_id: typing.Annotated[
|
||||
str, fastapi.Depends(autogpt_libs.auth.depends.get_user_id)
|
||||
]
|
||||
) -> backend.server.v2.store.model.MyAgentsResponse:
|
||||
):
|
||||
try:
|
||||
agents = await backend.server.v2.store.db.get_my_agents(user_id)
|
||||
return agents
|
||||
except Exception:
|
||||
logger.exception("Exception occurred whilst getting my agents")
|
||||
raise
|
||||
return fastapi.responses.JSONResponse(
|
||||
status_code=500,
|
||||
content={"detail": "An error occurred while retrieving the my agents"},
|
||||
)
|
||||
|
||||
|
||||
@router.delete(
|
||||
"/submissions/{submission_id}",
|
||||
tags=["store", "private"],
|
||||
dependencies=[fastapi.Depends(autogpt_libs.auth.middleware.auth_middleware)],
|
||||
response_model=bool,
|
||||
)
|
||||
async def delete_submission(
|
||||
user_id: typing.Annotated[
|
||||
str, fastapi.Depends(autogpt_libs.auth.depends.get_user_id)
|
||||
],
|
||||
submission_id: str,
|
||||
) -> bool:
|
||||
):
|
||||
"""
|
||||
Delete a store listing submission.
|
||||
|
||||
@ -321,13 +373,17 @@ async def delete_submission(
|
||||
return result
|
||||
except Exception:
|
||||
logger.exception("Exception occurred whilst deleting store submission")
|
||||
raise
|
||||
return fastapi.responses.JSONResponse(
|
||||
status_code=500,
|
||||
content={"detail": "An error occurred while deleting the store submission"},
|
||||
)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/submissions",
|
||||
tags=["store", "private"],
|
||||
dependencies=[fastapi.Depends(autogpt_libs.auth.middleware.auth_middleware)],
|
||||
response_model=backend.server.v2.store.model.StoreSubmissionsResponse,
|
||||
)
|
||||
async def get_submissions(
|
||||
user_id: typing.Annotated[
|
||||
@ -335,7 +391,7 @@ async def get_submissions(
|
||||
],
|
||||
page: int = 1,
|
||||
page_size: int = 20,
|
||||
) -> backend.server.v2.store.model.StoreSubmissionsResponse:
|
||||
):
|
||||
"""
|
||||
Get a paginated list of store submissions for the authenticated user.
|
||||
|
||||
@ -368,20 +424,26 @@ async def get_submissions(
|
||||
return listings
|
||||
except Exception:
|
||||
logger.exception("Exception occurred whilst getting store submissions")
|
||||
raise
|
||||
return fastapi.responses.JSONResponse(
|
||||
status_code=500,
|
||||
content={
|
||||
"detail": "An error occurred while retrieving the store submissions"
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/submissions",
|
||||
tags=["store", "private"],
|
||||
dependencies=[fastapi.Depends(autogpt_libs.auth.middleware.auth_middleware)],
|
||||
response_model=backend.server.v2.store.model.StoreSubmission,
|
||||
)
|
||||
async def create_submission(
|
||||
submission_request: backend.server.v2.store.model.StoreSubmissionRequest,
|
||||
user_id: typing.Annotated[
|
||||
str, fastapi.Depends(autogpt_libs.auth.depends.get_user_id)
|
||||
],
|
||||
) -> backend.server.v2.store.model.StoreSubmission:
|
||||
):
|
||||
"""
|
||||
Create a new store listing submission.
|
||||
|
||||
@ -411,7 +473,10 @@ async def create_submission(
|
||||
return submission
|
||||
except Exception:
|
||||
logger.exception("Exception occurred whilst creating store submission")
|
||||
raise
|
||||
return fastapi.responses.JSONResponse(
|
||||
status_code=500,
|
||||
content={"detail": "An error occurred while creating the store submission"},
|
||||
)
|
||||
|
||||
|
||||
@router.post(
|
||||
@ -424,7 +489,7 @@ async def upload_submission_media(
|
||||
user_id: typing.Annotated[
|
||||
str, fastapi.Depends(autogpt_libs.auth.depends.get_user_id)
|
||||
],
|
||||
) -> str:
|
||||
):
|
||||
"""
|
||||
Upload media (images/videos) for a store listing submission.
|
||||
|
||||
@ -443,10 +508,11 @@ async def upload_submission_media(
|
||||
user_id=user_id, file=file
|
||||
)
|
||||
return media_url
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
logger.exception("Exception occurred whilst uploading submission media")
|
||||
raise fastapi.HTTPException(
|
||||
status_code=500, detail=f"Failed to upload media file: {str(e)}"
|
||||
return fastapi.responses.JSONResponse(
|
||||
status_code=500,
|
||||
content={"detail": "An error occurred while uploading the media file"},
|
||||
)
|
||||
|
||||
|
||||
@ -503,8 +569,9 @@ async def generate_image(
|
||||
)
|
||||
|
||||
return fastapi.responses.JSONResponse(content={"image_url": image_url})
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
logger.exception("Exception occurred whilst generating submission image")
|
||||
raise fastapi.HTTPException(
|
||||
status_code=500, detail=f"Failed to generate image: {str(e)}"
|
||||
return fastapi.responses.JSONResponse(
|
||||
status_code=500,
|
||||
content={"detail": "An error occurred while generating the image"},
|
||||
)
|
||||
|
@ -0,0 +1,50 @@
|
||||
BEGIN;
|
||||
|
||||
DROP VIEW IF EXISTS "StoreAgent";
|
||||
|
||||
CREATE VIEW "StoreAgent" AS
|
||||
WITH ReviewStats AS (
|
||||
SELECT sl."id" AS "storeListingId",
|
||||
COUNT(sr.id) AS review_count,
|
||||
AVG(CAST(sr.score AS DECIMAL)) AS avg_rating
|
||||
FROM "StoreListing" sl
|
||||
JOIN "StoreListingVersion" slv ON slv."storeListingId" = sl."id"
|
||||
JOIN "StoreListingReview" sr ON sr."storeListingVersionId" = slv.id
|
||||
WHERE sl."isDeleted" = FALSE
|
||||
GROUP BY sl."id"
|
||||
),
|
||||
AgentRuns AS (
|
||||
SELECT "agentGraphId", COUNT(*) AS run_count
|
||||
FROM "AgentGraphExecution"
|
||||
GROUP BY "agentGraphId"
|
||||
)
|
||||
SELECT
|
||||
sl.id AS listing_id,
|
||||
slv.id AS "storeListingVersionId",
|
||||
slv."createdAt" AS updated_at,
|
||||
slv.slug,
|
||||
slv.name AS agent_name,
|
||||
slv."videoUrl" AS agent_video,
|
||||
COALESCE(slv."imageUrls", ARRAY[]::TEXT[]) AS agent_image,
|
||||
slv."isFeatured" AS featured,
|
||||
p.username AS creator_username,
|
||||
p."avatarUrl" AS creator_avatar,
|
||||
slv."subHeading" AS sub_heading,
|
||||
slv.description,
|
||||
slv.categories,
|
||||
COALESCE(ar.run_count, 0) AS runs,
|
||||
CAST(COALESCE(rs.avg_rating, 0.0) AS DOUBLE PRECISION) AS rating,
|
||||
ARRAY_AGG(DISTINCT CAST(slv.version AS TEXT)) AS versions
|
||||
FROM "StoreListing" sl
|
||||
JOIN "AgentGraph" a ON sl."agentId" = a.id AND sl."agentVersion" = a."version"
|
||||
LEFT JOIN "Profile" p ON sl."owningUserId" = p."userId"
|
||||
LEFT JOIN "StoreListingVersion" slv ON slv."storeListingId" = sl.id
|
||||
LEFT JOIN ReviewStats rs ON sl.id = rs."storeListingId"
|
||||
LEFT JOIN AgentRuns ar ON a.id = ar."agentGraphId"
|
||||
WHERE sl."isDeleted" = FALSE
|
||||
AND sl."isApproved" = TRUE
|
||||
GROUP BY sl.id, slv.id, slv.slug, slv."createdAt", slv.name, slv."videoUrl", slv."imageUrls", slv."isFeatured",
|
||||
p.username, p."avatarUrl", slv."subHeading", slv.description, slv.categories,
|
||||
ar.run_count, rs.avg_rating;
|
||||
|
||||
COMMIT;
|
@ -5,11 +5,7 @@ import { z } from "zod";
|
||||
import * as Sentry from "@sentry/nextjs";
|
||||
import getServerSupabase from "@/lib/supabase/getServerSupabase";
|
||||
import BackendAPI from "@/lib/autogpt-server-api";
|
||||
|
||||
const loginFormSchema = z.object({
|
||||
email: z.string().email().min(2).max(64),
|
||||
password: z.string().min(6).max(64),
|
||||
});
|
||||
import { loginFormSchema, LoginProvider } from "@/types/auth";
|
||||
|
||||
export async function logout() {
|
||||
return await Sentry.withServerActionInstrumentation(
|
||||
@ -25,7 +21,7 @@ export async function logout() {
|
||||
const { error } = await supabase.auth.signOut();
|
||||
|
||||
if (error) {
|
||||
console.log("Error logging out", error);
|
||||
console.error("Error logging out", error);
|
||||
return error.message;
|
||||
}
|
||||
|
||||
@ -47,18 +43,13 @@ export async function login(values: z.infer<typeof loginFormSchema>) {
|
||||
// We are sure that the values are of the correct type because zod validates the form
|
||||
const { data, error } = await supabase.auth.signInWithPassword(values);
|
||||
|
||||
await api.createUser();
|
||||
|
||||
if (error) {
|
||||
console.log("Error logging in", error);
|
||||
if (error.status == 400) {
|
||||
// Hence User is not present
|
||||
redirect("/login");
|
||||
}
|
||||
|
||||
console.error("Error logging in", error);
|
||||
return error.message;
|
||||
}
|
||||
|
||||
await api.createUser();
|
||||
|
||||
if (data.session) {
|
||||
await supabase.auth.setSession(data.session);
|
||||
}
|
||||
@ -68,38 +59,34 @@ export async function login(values: z.infer<typeof loginFormSchema>) {
|
||||
});
|
||||
}
|
||||
|
||||
export async function signup(values: z.infer<typeof loginFormSchema>) {
|
||||
"use server";
|
||||
export async function providerLogin(provider: LoginProvider) {
|
||||
return await Sentry.withServerActionInstrumentation(
|
||||
"signup",
|
||||
"providerLogin",
|
||||
{},
|
||||
async () => {
|
||||
const supabase = getServerSupabase();
|
||||
const api = new BackendAPI();
|
||||
|
||||
if (!supabase) {
|
||||
redirect("/error");
|
||||
}
|
||||
|
||||
// We are sure that the values are of the correct type because zod validates the form
|
||||
const { data, error } = await supabase.auth.signUp(values);
|
||||
const { error } = await supabase!.auth.signInWithOAuth({
|
||||
provider: provider,
|
||||
options: {
|
||||
redirectTo:
|
||||
process.env.AUTH_CALLBACK_URL ??
|
||||
`http://localhost:3000/auth/callback`,
|
||||
},
|
||||
});
|
||||
|
||||
if (error) {
|
||||
console.log("Error signing up", error);
|
||||
if (error.message.includes("P0001")) {
|
||||
return "Please join our waitlist for your turn: https://agpt.co/waitlist";
|
||||
}
|
||||
if (error.code?.includes("user_already_exists")) {
|
||||
redirect("/login");
|
||||
}
|
||||
console.error("Error logging in", error);
|
||||
return error.message;
|
||||
}
|
||||
|
||||
if (data.session) {
|
||||
await supabase.auth.setSession(data.session);
|
||||
}
|
||||
console.log("Signed up");
|
||||
revalidatePath("/", "layout");
|
||||
redirect("/store/profile");
|
||||
await api.createUser();
|
||||
console.log("Logged in");
|
||||
},
|
||||
);
|
||||
}
|
||||
|
@ -1,10 +1,8 @@
|
||||
"use client";
|
||||
import { login, signup } from "./actions";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { login, providerLogin } from "./actions";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
@ -14,40 +12,69 @@ import { useForm } from "react-hook-form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { z } from "zod";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { PasswordInput } from "@/components/PasswordInput";
|
||||
import { FaGoogle, FaGithub, FaDiscord, FaSpinner } from "react-icons/fa";
|
||||
import { useState } from "react";
|
||||
import { useCallback, useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import Link from "next/link";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import useSupabase from "@/hooks/useSupabase";
|
||||
import Spinner from "@/components/Spinner";
|
||||
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
|
||||
|
||||
const loginFormSchema = z.object({
|
||||
email: z.string().email().min(2).max(64),
|
||||
password: z.string().min(6).max(64),
|
||||
agreeToTerms: z.boolean().refine((value) => value === true, {
|
||||
message: "You must agree to the Terms of Use and Privacy Policy",
|
||||
}),
|
||||
});
|
||||
import {
|
||||
AuthCard,
|
||||
AuthHeader,
|
||||
AuthButton,
|
||||
AuthFeedback,
|
||||
AuthBottomText,
|
||||
PasswordInput,
|
||||
} from "@/components/auth";
|
||||
import { loginFormSchema } from "@/types/auth";
|
||||
|
||||
export default function LoginPage() {
|
||||
const { supabase, user, isUserLoading } = useSupabase();
|
||||
const [feedback, setFeedback] = useState<string | null>(null);
|
||||
const router = useRouter();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const api = useBackendAPI();
|
||||
|
||||
const form = useForm<z.infer<typeof loginFormSchema>>({
|
||||
resolver: zodResolver(loginFormSchema),
|
||||
defaultValues: {
|
||||
email: "",
|
||||
password: "",
|
||||
agreeToTerms: false,
|
||||
},
|
||||
});
|
||||
|
||||
// TODO: uncomment when we enable social login
|
||||
// const onProviderLogin = useCallback(async (
|
||||
// provider: LoginProvider,
|
||||
// ) => {
|
||||
// setIsLoading(true);
|
||||
// const error = await providerLogin(provider);
|
||||
// setIsLoading(false);
|
||||
// if (error) {
|
||||
// setFeedback(error);
|
||||
// return;
|
||||
// }
|
||||
// setFeedback(null);
|
||||
// }, [supabase]);
|
||||
|
||||
const onLogin = useCallback(
|
||||
async (data: z.infer<typeof loginFormSchema>) => {
|
||||
setIsLoading(true);
|
||||
|
||||
if (!(await form.trigger())) {
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const error = await login(data);
|
||||
setIsLoading(false);
|
||||
if (error) {
|
||||
setFeedback(error);
|
||||
return;
|
||||
}
|
||||
setFeedback(null);
|
||||
},
|
||||
[form],
|
||||
);
|
||||
|
||||
if (user) {
|
||||
console.debug("User exists, redirecting to /");
|
||||
router.push("/");
|
||||
@ -65,179 +92,60 @@ export default function LoginPage() {
|
||||
);
|
||||
}
|
||||
|
||||
async function handleSignInWithProvider(
|
||||
provider: "google" | "github" | "discord",
|
||||
) {
|
||||
const { data, error } = await supabase!.auth.signInWithOAuth({
|
||||
provider: provider,
|
||||
options: {
|
||||
redirectTo:
|
||||
process.env.AUTH_CALLBACK_URL ??
|
||||
`http://localhost:3000/auth/callback`,
|
||||
},
|
||||
});
|
||||
|
||||
await api.createUser();
|
||||
|
||||
if (!error) {
|
||||
setFeedback(null);
|
||||
return;
|
||||
}
|
||||
setFeedback(error.message);
|
||||
}
|
||||
|
||||
const onLogin = async (data: z.infer<typeof loginFormSchema>) => {
|
||||
setIsLoading(true);
|
||||
const error = await login(data);
|
||||
setIsLoading(false);
|
||||
if (error) {
|
||||
setFeedback(error);
|
||||
return;
|
||||
}
|
||||
setFeedback(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-[80vh] items-center justify-center">
|
||||
<div className="w-full max-w-md space-y-6 rounded-lg p-8 shadow-md">
|
||||
<h1 className="text-lg font-medium">Log in to your Account </h1>
|
||||
{/* <div className="mb-6 space-y-2">
|
||||
<Button
|
||||
className="w-full"
|
||||
onClick={() => handleSignInWithProvider("google")}
|
||||
variant="outline"
|
||||
type="button"
|
||||
disabled={isLoading}
|
||||
<AuthCard>
|
||||
<AuthHeader>Login to your account</AuthHeader>
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onLogin)}>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<FormItem className="mb-6">
|
||||
<FormLabel>Email</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="m@example.com" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="password"
|
||||
render={({ field }) => (
|
||||
<FormItem className="mb-6">
|
||||
<FormLabel className="flex w-full items-center justify-between">
|
||||
<span>Password</span>
|
||||
<Link
|
||||
href="/reset_password"
|
||||
className="text-sm font-normal leading-normal text-black underline"
|
||||
>
|
||||
Forgot your password?
|
||||
</Link>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<PasswordInput {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<AuthButton
|
||||
onClick={() => onLogin(form.getValues())}
|
||||
isLoading={isLoading}
|
||||
type="submit"
|
||||
>
|
||||
<FaGoogle className="mr-2 h-4 w-4" />
|
||||
Sign in with Google
|
||||
</Button>
|
||||
<Button
|
||||
className="w-full"
|
||||
onClick={() => handleSignInWithProvider("github")}
|
||||
variant="outline"
|
||||
type="button"
|
||||
disabled={isLoading}
|
||||
>
|
||||
<FaGithub className="mr-2 h-4 w-4" />
|
||||
Sign in with GitHub
|
||||
</Button>
|
||||
<Button
|
||||
className="w-full"
|
||||
onClick={() => handleSignInWithProvider("discord")}
|
||||
variant="outline"
|
||||
type="button"
|
||||
disabled={isLoading}
|
||||
>
|
||||
<FaDiscord className="mr-2 h-4 w-4" />
|
||||
Sign in with Discord
|
||||
</Button>
|
||||
</div> */}
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onLogin)}>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<FormItem className="mb-4">
|
||||
<FormLabel>Email</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="user@email.com" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="password"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Password</FormLabel>
|
||||
<FormControl>
|
||||
<PasswordInput placeholder="password" {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Password needs to be at least 6 characters long
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="agreeToTerms"
|
||||
render={({ field }) => (
|
||||
<FormItem className="mt-4 flex flex-row items-start space-x-3 space-y-0">
|
||||
<FormControl>
|
||||
<Checkbox
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
<div className="space-y-1 leading-none">
|
||||
<FormLabel>
|
||||
I agree to the{" "}
|
||||
<Link
|
||||
href="https://auto-gpt.notion.site/Terms-of-Use-11400ef5bece80d0b087d7831c5fd6bf"
|
||||
className="underline"
|
||||
>
|
||||
Terms of Use
|
||||
</Link>{" "}
|
||||
and{" "}
|
||||
<Link
|
||||
href="https://www.notion.so/auto-gpt/Privacy-Policy-ab11c9c20dbd4de1a15dcffe84d77984"
|
||||
className="underline"
|
||||
>
|
||||
Privacy Policy
|
||||
</Link>
|
||||
</FormLabel>
|
||||
<FormMessage />
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<div className="mb-6 mt-8 flex w-full space-x-4">
|
||||
<Button
|
||||
className="flex w-full justify-center"
|
||||
type="submit"
|
||||
disabled={isLoading}
|
||||
onClick={async () => {
|
||||
setIsLoading(true);
|
||||
const values = form.getValues();
|
||||
const result = await login(values);
|
||||
if (result) {
|
||||
setFeedback(result);
|
||||
}
|
||||
setIsLoading(false);
|
||||
}}
|
||||
>
|
||||
{isLoading ? <FaSpinner className="animate-spin" /> : "Log in"}
|
||||
</Button>
|
||||
<Button
|
||||
className="flex w-full justify-center"
|
||||
type="button"
|
||||
disabled={isLoading}
|
||||
onClick={async () => {
|
||||
setIsLoading(true);
|
||||
const values = form.getValues();
|
||||
const result = await signup(values);
|
||||
if (result) {
|
||||
setFeedback(result);
|
||||
}
|
||||
setIsLoading(false);
|
||||
}}
|
||||
>
|
||||
{isLoading ? <FaSpinner className="animate-spin" /> : "Sign up"}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
<p className="text-sm text-red-500">{feedback}</p>
|
||||
</Form>
|
||||
<Link href="/reset_password" className="text-sm">
|
||||
Forgot your password?
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
Login
|
||||
</AuthButton>
|
||||
</form>
|
||||
<AuthFeedback message={feedback} isError={true} />
|
||||
</Form>
|
||||
<AuthBottomText
|
||||
text="Don't have an account?"
|
||||
linkText="Sign up"
|
||||
href="/signup"
|
||||
/>
|
||||
</AuthCard>
|
||||
);
|
||||
}
|
||||
|
7
autogpt_platform/frontend/src/app/marketplace/page.tsx
Normal file
7
autogpt_platform/frontend/src/app/marketplace/page.tsx
Normal file
@ -0,0 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
export default function Page() {
|
||||
redirect("/store");
|
||||
}
|
60
autogpt_platform/frontend/src/app/reset_password/actions.ts
Normal file
60
autogpt_platform/frontend/src/app/reset_password/actions.ts
Normal file
@ -0,0 +1,60 @@
|
||||
"use server";
|
||||
import getServerSupabase from "@/lib/supabase/getServerSupabase";
|
||||
import { redirect } from "next/navigation";
|
||||
import * as Sentry from "@sentry/nextjs";
|
||||
import { headers } from "next/headers";
|
||||
|
||||
export async function sendResetEmail(email: string) {
|
||||
return await Sentry.withServerActionInstrumentation(
|
||||
"sendResetEmail",
|
||||
{},
|
||||
async () => {
|
||||
const supabase = getServerSupabase();
|
||||
const headersList = headers();
|
||||
const host = headersList.get("host");
|
||||
const protocol =
|
||||
process.env.NODE_ENV === "development" ? "http" : "https";
|
||||
const origin = `${protocol}://${host}`;
|
||||
|
||||
if (!supabase) {
|
||||
redirect("/error");
|
||||
}
|
||||
|
||||
const { error } = await supabase.auth.resetPasswordForEmail(email, {
|
||||
redirectTo: `${origin}/reset_password`,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
console.error("Error sending reset email", error);
|
||||
return error.message;
|
||||
}
|
||||
|
||||
console.log("Reset email sent");
|
||||
redirect("/reset_password");
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export async function changePassword(password: string) {
|
||||
return await Sentry.withServerActionInstrumentation(
|
||||
"changePassword",
|
||||
{},
|
||||
async () => {
|
||||
const supabase = getServerSupabase();
|
||||
|
||||
if (!supabase) {
|
||||
redirect("/error");
|
||||
}
|
||||
|
||||
const { error } = await supabase.auth.updateUser({ password });
|
||||
|
||||
if (error) {
|
||||
console.error("Error changing password", error);
|
||||
return error.message;
|
||||
}
|
||||
|
||||
await supabase.auth.signOut();
|
||||
redirect("/login");
|
||||
},
|
||||
);
|
||||
}
|
@ -1,8 +1,15 @@
|
||||
"use client";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
AuthCard,
|
||||
AuthHeader,
|
||||
AuthButton,
|
||||
AuthFeedback,
|
||||
PasswordInput,
|
||||
} from "@/components/auth";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
@ -10,54 +17,87 @@ import {
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import useSupabase from "@/hooks/useSupabase";
|
||||
import { sendEmailFormSchema, changePasswordFormSchema } from "@/types/auth";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
import { useCallback, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { FaSpinner } from "react-icons/fa";
|
||||
import { z } from "zod";
|
||||
|
||||
const emailFormSchema = z.object({
|
||||
email: z.string().email().min(2).max(64),
|
||||
});
|
||||
|
||||
const resetPasswordFormSchema = z
|
||||
.object({
|
||||
password: z.string().min(6).max(64),
|
||||
confirmPassword: z.string().min(6).max(64),
|
||||
})
|
||||
.refine((data) => data.password === data.confirmPassword, {
|
||||
message: "Passwords don't match",
|
||||
path: ["confirmPassword"],
|
||||
});
|
||||
import { changePassword, sendResetEmail } from "./actions";
|
||||
import Spinner from "@/components/Spinner";
|
||||
|
||||
export default function ResetPasswordPage() {
|
||||
const { supabase, user, isUserLoading } = useSupabase();
|
||||
const router = useRouter();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [feedback, setFeedback] = useState<string | null>(null);
|
||||
const [isError, setIsError] = useState(false);
|
||||
const [disabled, setDisabled] = useState(false);
|
||||
|
||||
const emailForm = useForm<z.infer<typeof emailFormSchema>>({
|
||||
resolver: zodResolver(emailFormSchema),
|
||||
const sendEmailForm = useForm<z.infer<typeof sendEmailFormSchema>>({
|
||||
resolver: zodResolver(sendEmailFormSchema),
|
||||
defaultValues: {
|
||||
email: "",
|
||||
},
|
||||
});
|
||||
|
||||
const resetPasswordForm = useForm<z.infer<typeof resetPasswordFormSchema>>({
|
||||
resolver: zodResolver(resetPasswordFormSchema),
|
||||
const changePasswordForm = useForm<z.infer<typeof changePasswordFormSchema>>({
|
||||
resolver: zodResolver(changePasswordFormSchema),
|
||||
defaultValues: {
|
||||
password: "",
|
||||
confirmPassword: "",
|
||||
},
|
||||
});
|
||||
|
||||
const onSendEmail = useCallback(
|
||||
async (data: z.infer<typeof sendEmailFormSchema>) => {
|
||||
setIsLoading(true);
|
||||
setFeedback(null);
|
||||
|
||||
if (!(await sendEmailForm.trigger())) {
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const error = await sendResetEmail(data.email);
|
||||
setIsLoading(false);
|
||||
if (error) {
|
||||
setFeedback(error);
|
||||
setIsError(true);
|
||||
return;
|
||||
}
|
||||
setDisabled(true);
|
||||
setFeedback(
|
||||
"Password reset email sent if user exists. Please check your email.",
|
||||
);
|
||||
setIsError(false);
|
||||
},
|
||||
[sendEmailForm],
|
||||
);
|
||||
|
||||
const onChangePassword = useCallback(
|
||||
async (data: z.infer<typeof changePasswordFormSchema>) => {
|
||||
setIsLoading(true);
|
||||
setFeedback(null);
|
||||
|
||||
if (!(await changePasswordForm.trigger())) {
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const error = await changePassword(data.password);
|
||||
setIsLoading(false);
|
||||
if (error) {
|
||||
setFeedback(error);
|
||||
setIsError(true);
|
||||
return;
|
||||
}
|
||||
setFeedback("Password changed successfully. Redirecting to login.");
|
||||
setIsError(false);
|
||||
},
|
||||
[changePasswordForm],
|
||||
);
|
||||
|
||||
if (isUserLoading) {
|
||||
return (
|
||||
<div className="flex h-[80vh] items-center justify-center">
|
||||
<FaSpinner className="mr-2 h-16 w-16 animate-spin" />
|
||||
</div>
|
||||
);
|
||||
return <Spinner />;
|
||||
}
|
||||
|
||||
if (!supabase) {
|
||||
@ -68,147 +108,79 @@ export default function ResetPasswordPage() {
|
||||
);
|
||||
}
|
||||
|
||||
async function onSendEmail(d: z.infer<typeof emailFormSchema>) {
|
||||
setIsLoading(true);
|
||||
setFeedback(null);
|
||||
|
||||
if (!(await emailForm.trigger())) {
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const { data, error } = await supabase!.auth.resetPasswordForEmail(
|
||||
d.email,
|
||||
{
|
||||
redirectTo: `${window.location.origin}/reset_password`,
|
||||
},
|
||||
);
|
||||
|
||||
if (error) {
|
||||
setFeedback(error.message);
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setFeedback("Password reset email sent. Please check your email.");
|
||||
setIsLoading(false);
|
||||
}
|
||||
|
||||
async function onResetPassword(d: z.infer<typeof resetPasswordFormSchema>) {
|
||||
setIsLoading(true);
|
||||
setFeedback(null);
|
||||
|
||||
if (!(await resetPasswordForm.trigger())) {
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const { data, error } = await supabase!.auth.updateUser({
|
||||
password: d.password,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
setFeedback(error.message);
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
await supabase!.auth.signOut();
|
||||
router.push("/login");
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col items-center justify-center">
|
||||
<div className="w-full max-w-md">
|
||||
<h1 className="text-center text-3xl font-bold">Reset Password</h1>
|
||||
{user ? (
|
||||
<form
|
||||
onSubmit={resetPasswordForm.handleSubmit(onResetPassword)}
|
||||
className="mt-6 space-y-6"
|
||||
>
|
||||
<Form {...resetPasswordForm}>
|
||||
<FormField
|
||||
control={resetPasswordForm.control}
|
||||
name="password"
|
||||
render={({ field }) => (
|
||||
<FormItem className="mb-4">
|
||||
<FormLabel>Password</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="password"
|
||||
placeholder="password"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={resetPasswordForm.control}
|
||||
name="confirmPassword"
|
||||
render={({ field }) => (
|
||||
<FormItem className="mb">
|
||||
<FormLabel>Confirm Password</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="password"
|
||||
placeholder="password"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-full"
|
||||
disabled={isLoading}
|
||||
onClick={() => onResetPassword(resetPasswordForm.getValues())}
|
||||
>
|
||||
{isLoading ? <FaSpinner className="mr-2 animate-spin" /> : null}
|
||||
Reset Password
|
||||
</Button>
|
||||
</Form>
|
||||
</form>
|
||||
) : (
|
||||
<form
|
||||
onSubmit={emailForm.handleSubmit(onSendEmail)}
|
||||
className="mt-6 space-y-6"
|
||||
>
|
||||
<Form {...emailForm}>
|
||||
<FormField
|
||||
control={emailForm.control}
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<FormItem className="mb-4">
|
||||
<FormLabel>Email</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="user@email.com" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-full"
|
||||
disabled={isLoading}
|
||||
onClick={() => onSendEmail(emailForm.getValues())}
|
||||
>
|
||||
{isLoading ? <FaSpinner className="mr-2 animate-spin" /> : null}
|
||||
Send Reset Email
|
||||
</Button>
|
||||
{feedback ? (
|
||||
<div className="text-center text-sm text-red-500">
|
||||
{feedback}
|
||||
</div>
|
||||
) : null}
|
||||
</Form>
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<AuthCard>
|
||||
<AuthHeader>Reset Password</AuthHeader>
|
||||
{user ? (
|
||||
<form onSubmit={changePasswordForm.handleSubmit(onChangePassword)}>
|
||||
<Form {...changePasswordForm}>
|
||||
<FormField
|
||||
control={changePasswordForm.control}
|
||||
name="password"
|
||||
render={({ field }) => (
|
||||
<FormItem className="mb-6">
|
||||
<FormLabel>Password</FormLabel>
|
||||
<FormControl>
|
||||
<PasswordInput {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={changePasswordForm.control}
|
||||
name="confirmPassword"
|
||||
render={({ field }) => (
|
||||
<FormItem className="mb-6">
|
||||
<FormLabel>Confirm Password</FormLabel>
|
||||
<FormControl>
|
||||
<PasswordInput {...field} />
|
||||
</FormControl>
|
||||
<FormDescription className="text-sm font-normal leading-tight text-slate-500">
|
||||
Password needs to be at least 6 characters long
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<AuthButton
|
||||
onClick={() => onChangePassword(changePasswordForm.getValues())}
|
||||
isLoading={isLoading}
|
||||
type="submit"
|
||||
>
|
||||
Update password
|
||||
</AuthButton>
|
||||
<AuthFeedback message={feedback} isError={isError} />
|
||||
</Form>
|
||||
</form>
|
||||
) : (
|
||||
<form onSubmit={sendEmailForm.handleSubmit(onSendEmail)}>
|
||||
<Form {...sendEmailForm}>
|
||||
<FormField
|
||||
control={sendEmailForm.control}
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<FormItem className="mb-6">
|
||||
<FormLabel>Email</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="m@example.com" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<AuthButton
|
||||
onClick={() => onSendEmail(sendEmailForm.getValues())}
|
||||
isLoading={isLoading}
|
||||
disabled={disabled}
|
||||
type="submit"
|
||||
>
|
||||
Send reset email
|
||||
</AuthButton>
|
||||
<AuthFeedback message={feedback} isError={isError} />
|
||||
</Form>
|
||||
</form>
|
||||
)}
|
||||
</AuthCard>
|
||||
);
|
||||
}
|
||||
|
44
autogpt_platform/frontend/src/app/signup/actions.ts
Normal file
44
autogpt_platform/frontend/src/app/signup/actions.ts
Normal file
@ -0,0 +1,44 @@
|
||||
"use server";
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { redirect } from "next/navigation";
|
||||
import { z } from "zod";
|
||||
import * as Sentry from "@sentry/nextjs";
|
||||
import getServerSupabase from "@/lib/supabase/getServerSupabase";
|
||||
import { signupFormSchema } from "@/types/auth";
|
||||
|
||||
export async function signup(values: z.infer<typeof signupFormSchema>) {
|
||||
"use server";
|
||||
return await Sentry.withServerActionInstrumentation(
|
||||
"signup",
|
||||
{},
|
||||
async () => {
|
||||
const supabase = getServerSupabase();
|
||||
|
||||
if (!supabase) {
|
||||
redirect("/error");
|
||||
}
|
||||
|
||||
// We are sure that the values are of the correct type because zod validates the form
|
||||
const { data, error } = await supabase.auth.signUp(values);
|
||||
|
||||
if (error) {
|
||||
console.error("Error signing up", error);
|
||||
// FIXME: supabase doesn't return the correct error message for this case
|
||||
if (error.message.includes("P0001")) {
|
||||
return "Please join our waitlist for your turn: https://agpt.co/waitlist";
|
||||
}
|
||||
if (error.code?.includes("user_already_exists")) {
|
||||
redirect("/login");
|
||||
}
|
||||
return error.message;
|
||||
}
|
||||
|
||||
if (data.session) {
|
||||
await supabase.auth.setSession(data.session);
|
||||
}
|
||||
console.log("Signed up");
|
||||
revalidatePath("/", "layout");
|
||||
redirect("/store/profile");
|
||||
},
|
||||
);
|
||||
}
|
219
autogpt_platform/frontend/src/app/signup/page.tsx
Normal file
219
autogpt_platform/frontend/src/app/signup/page.tsx
Normal file
@ -0,0 +1,219 @@
|
||||
"use client";
|
||||
import { signup } from "./actions";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { z } from "zod";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useCallback, useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import Link from "next/link";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import useSupabase from "@/hooks/useSupabase";
|
||||
import Spinner from "@/components/Spinner";
|
||||
import {
|
||||
AuthCard,
|
||||
AuthHeader,
|
||||
AuthButton,
|
||||
AuthFeedback,
|
||||
AuthBottomText,
|
||||
PasswordInput,
|
||||
} from "@/components/auth";
|
||||
import { signupFormSchema } from "@/types/auth";
|
||||
|
||||
export default function SignupPage() {
|
||||
const { supabase, user, isUserLoading } = useSupabase();
|
||||
const [feedback, setFeedback] = useState<string | null>(null);
|
||||
const router = useRouter();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [showWaitlistPrompt, setShowWaitlistPrompt] = useState(false);
|
||||
|
||||
const form = useForm<z.infer<typeof signupFormSchema>>({
|
||||
resolver: zodResolver(signupFormSchema),
|
||||
defaultValues: {
|
||||
email: "",
|
||||
password: "",
|
||||
confirmPassword: "",
|
||||
agreeToTerms: false,
|
||||
},
|
||||
});
|
||||
|
||||
const onSignup = useCallback(
|
||||
async (data: z.infer<typeof signupFormSchema>) => {
|
||||
setIsLoading(true);
|
||||
|
||||
if (!(await form.trigger())) {
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const error = await signup(data);
|
||||
setIsLoading(false);
|
||||
if (error) {
|
||||
setShowWaitlistPrompt(true);
|
||||
return;
|
||||
}
|
||||
setFeedback(null);
|
||||
},
|
||||
[form],
|
||||
);
|
||||
|
||||
if (user) {
|
||||
console.debug("User exists, redirecting to /");
|
||||
router.push("/");
|
||||
}
|
||||
|
||||
if (isUserLoading || user) {
|
||||
return <Spinner />;
|
||||
}
|
||||
|
||||
if (!supabase) {
|
||||
return (
|
||||
<div>
|
||||
User accounts are disabled because Supabase client is unavailable
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<AuthCard>
|
||||
<AuthHeader>Create a new account</AuthHeader>
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSignup)}>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<FormItem className="mb-6">
|
||||
<FormLabel>Email</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="m@example.com" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="password"
|
||||
render={({ field }) => (
|
||||
<FormItem className="mb-6">
|
||||
<FormLabel>Password</FormLabel>
|
||||
<FormControl>
|
||||
<PasswordInput {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="confirmPassword"
|
||||
render={({ field }) => (
|
||||
<FormItem className="mb-4">
|
||||
<FormLabel>Confirm Password</FormLabel>
|
||||
<FormControl>
|
||||
<PasswordInput {...field} />
|
||||
</FormControl>
|
||||
<FormDescription className="text-sm font-normal leading-tight text-slate-500">
|
||||
Password needs to be at least 6 characters long
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<AuthButton
|
||||
onClick={() => onSignup(form.getValues())}
|
||||
isLoading={isLoading}
|
||||
type="submit"
|
||||
>
|
||||
Sign up
|
||||
</AuthButton>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="agreeToTerms"
|
||||
render={({ field }) => (
|
||||
<FormItem className="mt-6 flex flex-row items-start -space-y-1 space-x-2">
|
||||
<FormControl>
|
||||
<Checkbox
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
<div className="">
|
||||
<FormLabel>
|
||||
<span className="mr-1 text-sm font-normal leading-normal text-slate-950">
|
||||
I agree to the
|
||||
</span>
|
||||
<Link
|
||||
href="https://auto-gpt.notion.site/Terms-of-Use-11400ef5bece80d0b087d7831c5fd6bf"
|
||||
className="text-sm font-normal leading-normal text-slate-950 underline"
|
||||
>
|
||||
Terms of Use
|
||||
</Link>
|
||||
<span className="mx-1 text-sm font-normal leading-normal text-slate-950">
|
||||
and
|
||||
</span>
|
||||
<Link
|
||||
href="https://www.notion.so/auto-gpt/Privacy-Policy-ab11c9c20dbd4de1a15dcffe84d77984"
|
||||
className="text-sm font-normal leading-normal text-slate-950 underline"
|
||||
>
|
||||
Privacy Policy
|
||||
</Link>
|
||||
</FormLabel>
|
||||
<FormMessage />
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</form>
|
||||
<AuthFeedback message={feedback} isError={true} />
|
||||
</Form>
|
||||
{showWaitlistPrompt && (
|
||||
<div>
|
||||
<span className="mr-1 text-sm font-normal leading-normal text-red-500">
|
||||
The provided email may not be allowed to sign up.
|
||||
</span>
|
||||
<br />
|
||||
<span className="mx-1 text-sm font-normal leading-normal text-slate-950">
|
||||
- AutoGPT Platform is currently in closed beta. You can join
|
||||
</span>
|
||||
<Link
|
||||
href="https://agpt.co/waitlist"
|
||||
className="text-sm font-normal leading-normal text-slate-950 underline"
|
||||
>
|
||||
the waitlist here.
|
||||
</Link>
|
||||
<br />
|
||||
<span className="mx-1 text-sm font-normal leading-normal text-slate-950">
|
||||
- Make sure you use the same email address you used to sign up for
|
||||
the waitlist.
|
||||
</span>
|
||||
<br />
|
||||
<span className="mx-1 text-sm font-normal leading-normal text-slate-950">
|
||||
- You can self host the platform, visit our
|
||||
</span>
|
||||
<Link
|
||||
href="https://agpt.co/waitlist"
|
||||
className="text-sm font-normal leading-normal text-slate-950 underline"
|
||||
>
|
||||
GitHub repository.
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
<AuthBottomText
|
||||
text="Already a member?"
|
||||
linkText="Log in"
|
||||
href="/login"
|
||||
/>
|
||||
</AuthCard>
|
||||
);
|
||||
}
|
@ -40,7 +40,8 @@ export default async function Page({
|
||||
const agent = await api.getStoreAgent(creator_lower, params.slug);
|
||||
const otherAgents = await api.getStoreAgents({ creator: creator_lower });
|
||||
const similarAgents = await api.getStoreAgents({
|
||||
search_query: agent.categories[0],
|
||||
// We are using slug as we know its has been sanitized and is not null
|
||||
search_query: agent.slug.replace(/-/g, " "),
|
||||
});
|
||||
|
||||
const breadcrumbs = [
|
||||
|
@ -67,9 +67,13 @@ export default async function Page({
|
||||
<p className="font-geist text-underline-position-from-font text-decoration-skip-none text-left text-base font-medium leading-6">
|
||||
About
|
||||
</p>
|
||||
<div className="font-poppins text-[48px] font-normal leading-[59px] text-neutral-900 dark:text-zinc-50">
|
||||
<div
|
||||
className="font-poppins text-[48px] font-normal leading-[59px] text-neutral-900 dark:text-zinc-50"
|
||||
style={{ whiteSpace: "pre-line" }}
|
||||
>
|
||||
{creator.description}
|
||||
</div>
|
||||
|
||||
<CreatorLinks links={creator.links} />
|
||||
</div>
|
||||
</div>
|
||||
|
@ -34,8 +34,8 @@ export const AgentsSection: React.FC<AgentsSectionProps> = ({
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
|
||||
// Take only the first 9 agents
|
||||
const displayedAgents = allAgents.slice(0, 9);
|
||||
// TODO: Update this when we have pagination
|
||||
const displayedAgents = allAgents;
|
||||
|
||||
const handleCardClick = (creator: string, slug: string) => {
|
||||
router.push(
|
||||
|
@ -0,0 +1,37 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
import Link from "next/link";
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
text: string;
|
||||
linkText?: string;
|
||||
href?: string;
|
||||
}
|
||||
|
||||
export default function AuthBottomText({
|
||||
className = "",
|
||||
text,
|
||||
linkText,
|
||||
href = "",
|
||||
}: Props) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
className,
|
||||
"mt-8 inline-flex w-full items-center justify-center",
|
||||
)}
|
||||
>
|
||||
<span className="text-sm font-medium leading-normal text-slate-950">
|
||||
{text}
|
||||
</span>
|
||||
{linkText && (
|
||||
<Link
|
||||
href={href}
|
||||
className="ml-1 text-sm font-medium leading-normal text-slate-950 underline"
|
||||
>
|
||||
{linkText}
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
36
autogpt_platform/frontend/src/components/auth/AuthButton.tsx
Normal file
36
autogpt_platform/frontend/src/components/auth/AuthButton.tsx
Normal file
@ -0,0 +1,36 @@
|
||||
import { ReactNode } from "react";
|
||||
import { Button } from "../ui/button";
|
||||
import { FaSpinner } from "react-icons/fa";
|
||||
|
||||
interface Props {
|
||||
children?: ReactNode;
|
||||
onClick: () => void;
|
||||
isLoading?: boolean;
|
||||
disabled?: boolean;
|
||||
type?: "button" | "submit" | "reset";
|
||||
}
|
||||
|
||||
export default function AuthButton({
|
||||
children,
|
||||
onClick,
|
||||
isLoading = false,
|
||||
disabled = false,
|
||||
type = "button",
|
||||
}: Props) {
|
||||
return (
|
||||
<Button
|
||||
className="mt-2 w-full self-stretch rounded-md bg-slate-900 px-4 py-2"
|
||||
type={type}
|
||||
disabled={isLoading || disabled}
|
||||
onClick={onClick}
|
||||
>
|
||||
{isLoading ? (
|
||||
<FaSpinner className="animate-spin" />
|
||||
) : (
|
||||
<div className="text-sm font-medium leading-normal text-slate-50">
|
||||
{children}
|
||||
</div>
|
||||
)}
|
||||
</Button>
|
||||
);
|
||||
}
|
15
autogpt_platform/frontend/src/components/auth/AuthCard.tsx
Normal file
15
autogpt_platform/frontend/src/components/auth/AuthCard.tsx
Normal file
@ -0,0 +1,15 @@
|
||||
import { ReactNode } from "react";
|
||||
|
||||
interface Props {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export default function AuthCard({ children }: Props) {
|
||||
return (
|
||||
<div className="flex h-[80vh] w-[32rem] items-center justify-center">
|
||||
<div className="w-full max-w-md rounded-lg bg-white p-6 shadow-md">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
interface Props {
|
||||
message?: string | null;
|
||||
isError?: boolean;
|
||||
}
|
||||
|
||||
export default function AuthFeedback({ message = "", isError = false }: Props) {
|
||||
return (
|
||||
<div className="mt-4 text-center text-sm font-medium leading-normal">
|
||||
{isError ? (
|
||||
<div className="text-red-500">{message}</div>
|
||||
) : (
|
||||
<div className="text-slate-950">{message}</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
13
autogpt_platform/frontend/src/components/auth/AuthHeader.tsx
Normal file
13
autogpt_platform/frontend/src/components/auth/AuthHeader.tsx
Normal file
@ -0,0 +1,13 @@
|
||||
import { ReactNode } from "react";
|
||||
|
||||
interface Props {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export default function AuthHeader({ children }: Props) {
|
||||
return (
|
||||
<div className="mb-8 text-2xl font-semibold leading-normal text-slate-950">
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
@ -16,6 +16,7 @@ const PasswordInput = forwardRef<HTMLInputElement, InputProps>(
|
||||
type={showPassword ? "text" : "password"}
|
||||
className={cn("hide-password-toggle pr-10", className)}
|
||||
ref={ref}
|
||||
title="password"
|
||||
{...props}
|
||||
/>
|
||||
<Button
|
||||
@ -23,8 +24,11 @@ const PasswordInput = forwardRef<HTMLInputElement, InputProps>(
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="absolute right-0 top-0 h-full px-3 py-2 hover:bg-transparent"
|
||||
onClick={() => setShowPassword((prev) => !prev)}
|
||||
onMouseDown={() => setShowPassword(true)}
|
||||
onMouseUp={() => setShowPassword(false)}
|
||||
onMouseLeave={() => setShowPassword(false)}
|
||||
disabled={disabled}
|
||||
tabIndex={-1}
|
||||
>
|
||||
{showPassword && !disabled ? (
|
||||
<EyeIcon className="h-4 w-4" aria-hidden="true" />
|
15
autogpt_platform/frontend/src/components/auth/index.ts
Normal file
15
autogpt_platform/frontend/src/components/auth/index.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import AuthBottomText from "./AuthBottomText";
|
||||
import AuthButton from "./AuthButton";
|
||||
import AuthCard from "./AuthCard";
|
||||
import AuthFeedback from "./AuthFeedback";
|
||||
import AuthHeader from "./AuthHeader";
|
||||
import { PasswordInput } from "./PasswordInput";
|
||||
|
||||
export {
|
||||
AuthBottomText,
|
||||
AuthButton,
|
||||
AuthCard,
|
||||
AuthFeedback,
|
||||
AuthHeader,
|
||||
PasswordInput,
|
||||
};
|
@ -28,7 +28,11 @@ export default function useSupabase() {
|
||||
const response = await supabase.auth.getUser();
|
||||
|
||||
if (response.error) {
|
||||
console.error("Error fetching user", response.error);
|
||||
// Display error only if it's not about missing auth session (user is not logged in)
|
||||
if (response.error.message !== "Auth session missing!") {
|
||||
console.error("Error fetching user", response.error);
|
||||
}
|
||||
setUser(null);
|
||||
} else {
|
||||
setUser(response.data.user);
|
||||
}
|
||||
|
@ -1,7 +1,8 @@
|
||||
import { Graph, Block, Node } from "./types";
|
||||
import { Graph, Block, Node, BlockUIType } from "./types";
|
||||
|
||||
/** Creates a copy of the graph with all secrets removed */
|
||||
export function safeCopyGraph(graph: Graph, block_defs: Block[]): Graph {
|
||||
graph = removeAgentInputBlockValues(graph, block_defs);
|
||||
return {
|
||||
...graph,
|
||||
nodes: graph.nodes.map((node) => {
|
||||
@ -18,3 +19,28 @@ export function safeCopyGraph(graph: Graph, block_defs: Block[]): Graph {
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
export function removeAgentInputBlockValues(graph: Graph, blocks: Block[]) {
|
||||
const inputBlocks = graph.nodes.filter(
|
||||
(node) =>
|
||||
blocks.find((b) => b.id === node.block_id)?.uiType === BlockUIType.INPUT,
|
||||
);
|
||||
|
||||
const modifiedNodes = graph.nodes.map((node) => {
|
||||
if (inputBlocks.find((inputNode) => inputNode.id === node.id)) {
|
||||
return {
|
||||
...node,
|
||||
input_default: {
|
||||
...node.input_default,
|
||||
value: "",
|
||||
},
|
||||
};
|
||||
}
|
||||
return node;
|
||||
});
|
||||
|
||||
return {
|
||||
...graph,
|
||||
nodes: modifiedNodes,
|
||||
};
|
||||
}
|
||||
|
@ -7,33 +7,28 @@ export class LoginPage {
|
||||
console.log("Attempting login with:", { email, password }); // Debug log
|
||||
|
||||
// Fill email
|
||||
const emailInput = this.page.getByPlaceholder("user@email.com");
|
||||
const emailInput = this.page.getByPlaceholder("m@example.com");
|
||||
await emailInput.waitFor({ state: "visible" });
|
||||
await emailInput.fill(email);
|
||||
|
||||
// Fill password
|
||||
const passwordInput = this.page.getByPlaceholder("password");
|
||||
const passwordInput = this.page.getByTitle("Password");
|
||||
await passwordInput.waitFor({ state: "visible" });
|
||||
await passwordInput.fill(password);
|
||||
|
||||
// Check terms
|
||||
const termsCheckbox = this.page.getByLabel("I agree to the Terms of Use");
|
||||
await termsCheckbox.waitFor({ state: "visible" });
|
||||
await termsCheckbox.click();
|
||||
|
||||
// TODO: This is a workaround to wait for the page to load after filling the email and password
|
||||
const emailInput2 = this.page.getByPlaceholder("user@email.com");
|
||||
const emailInput2 = this.page.getByPlaceholder("m@example.com");
|
||||
await emailInput2.waitFor({ state: "visible" });
|
||||
await emailInput2.fill(email);
|
||||
|
||||
// Fill password
|
||||
const passwordInput2 = this.page.getByPlaceholder("password");
|
||||
const passwordInput2 = this.page.getByTitle("Password");
|
||||
await passwordInput2.waitFor({ state: "visible" });
|
||||
await passwordInput2.fill(password);
|
||||
|
||||
// Wait for the button to be ready
|
||||
const loginButton = this.page.getByRole("button", {
|
||||
name: "Log in",
|
||||
name: "Login",
|
||||
exact: true,
|
||||
});
|
||||
await loginButton.waitFor({ state: "visible" });
|
||||
|
63
autogpt_platform/frontend/src/types/auth.ts
Normal file
63
autogpt_platform/frontend/src/types/auth.ts
Normal file
@ -0,0 +1,63 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export type LoginProvider = "google" | "github" | "discord";
|
||||
|
||||
export const loginFormSchema = z.object({
|
||||
email: z
|
||||
.string()
|
||||
.email()
|
||||
.max(128, "Email must contain at most 128 characters")
|
||||
.trim(),
|
||||
password: z
|
||||
.string()
|
||||
.min(6, "Password must contain at least 6 characters")
|
||||
.max(64, "Password must contain at most 64 characters"),
|
||||
});
|
||||
|
||||
export const signupFormSchema = z
|
||||
.object({
|
||||
email: z
|
||||
.string()
|
||||
.email()
|
||||
.max(128, "Email must contain at most 128 characters")
|
||||
.trim(),
|
||||
password: z
|
||||
.string()
|
||||
.min(6, "Password must contain at least 6 characters")
|
||||
.max(64, "Password must contain at most 64 characters"),
|
||||
confirmPassword: z
|
||||
.string()
|
||||
.min(6, "Password must contain at least 6 characters")
|
||||
.max(64, "Password must contain at most 64 characters"),
|
||||
agreeToTerms: z.boolean().refine((value) => value === true, {
|
||||
message: "You must agree to the Terms of Use and Privacy Policy",
|
||||
}),
|
||||
})
|
||||
.refine((data) => data.password === data.confirmPassword, {
|
||||
message: "Passwords don't match",
|
||||
path: ["confirmPassword"],
|
||||
});
|
||||
|
||||
export const sendEmailFormSchema = z.object({
|
||||
email: z
|
||||
.string()
|
||||
.email()
|
||||
.max(128, "Email must contain at most 128 characters")
|
||||
.trim(),
|
||||
});
|
||||
|
||||
export const changePasswordFormSchema = z
|
||||
.object({
|
||||
password: z
|
||||
.string()
|
||||
.min(6, "Password must contain at least 6 characters")
|
||||
.max(64, "Password must contain at most 64 characters"),
|
||||
confirmPassword: z
|
||||
.string()
|
||||
.min(6, "Password must contain at least 6 characters")
|
||||
.max(64, "Password must contain at most 64 characters"),
|
||||
})
|
||||
.refine((data) => data.password === data.confirmPassword, {
|
||||
message: "Passwords don't match",
|
||||
path: ["confirmPassword"],
|
||||
});
|
@ -3257,17 +3257,17 @@
|
||||
resolved "https://registry.yarnpkg.com/@storybook/theming/-/theming-8.4.7.tgz#c308f6a883999bd35e87826738ab8a76515932b5"
|
||||
integrity sha512-99rgLEjf7iwfSEmdqlHkSG3AyLcK0sfExcr0jnc6rLiAkBhzuIsvcHjjUwkR210SOCgXqBPW0ZA6uhnuyppHLw==
|
||||
|
||||
"@supabase/auth-js@2.67.1":
|
||||
version "2.67.1"
|
||||
resolved "https://registry.yarnpkg.com/@supabase/auth-js/-/auth-js-2.67.1.tgz#b72217136df61d645dcfb7b12c7db8cbb7875a4c"
|
||||
integrity sha512-1SRZG9VkLFz4rtiyEc1l49tMq9jTYu4wJt3pMQEWi7yshZFIBdVH1o5sshk1plQd5LY6GcrPIpCydM2gGDxchA==
|
||||
"@supabase/auth-js@2.67.3":
|
||||
version "2.67.3"
|
||||
resolved "https://registry.yarnpkg.com/@supabase/auth-js/-/auth-js-2.67.3.tgz#a1f5eb22440b0cdbf87fe2ecae662a8dd8bb2028"
|
||||
integrity sha512-NJDaW8yXs49xMvWVOkSIr8j46jf+tYHV0wHhrwOaLLMZSFO4g6kKAf+MfzQ2RaD06OCUkUHIzctLAxjTgEVpzw==
|
||||
dependencies:
|
||||
"@supabase/node-fetch" "^2.6.14"
|
||||
|
||||
"@supabase/functions-js@2.4.3":
|
||||
version "2.4.3"
|
||||
resolved "https://registry.yarnpkg.com/@supabase/functions-js/-/functions-js-2.4.3.tgz#ac1c696d3a1ebe00f60d5cea69b208078678ef8b"
|
||||
integrity sha512-sOLXy+mWRyu4LLv1onYydq+10mNRQ4rzqQxNhbrKLTLTcdcmS9hbWif0bGz/NavmiQfPs4ZcmQJp4WqOXlR4AQ==
|
||||
"@supabase/functions-js@2.4.4":
|
||||
version "2.4.4"
|
||||
resolved "https://registry.yarnpkg.com/@supabase/functions-js/-/functions-js-2.4.4.tgz#45fcd94d546bdfa66d01f93a796ca0304ec154b8"
|
||||
integrity sha512-WL2p6r4AXNGwop7iwvul2BvOtuJ1YQy8EbOd0dhG1oN1q8el/BIRSFCFnWAMM/vJJlHWLi4ad22sKbKr9mvjoA==
|
||||
dependencies:
|
||||
"@supabase/node-fetch" "^2.6.14"
|
||||
|
||||
@ -3311,12 +3311,12 @@
|
||||
"@supabase/node-fetch" "^2.6.14"
|
||||
|
||||
"@supabase/supabase-js@^2.47.8":
|
||||
version "2.47.8"
|
||||
resolved "https://registry.yarnpkg.com/@supabase/supabase-js/-/supabase-js-2.47.8.tgz#6471a356b694e14170a00e6582bdbd0126944ec6"
|
||||
integrity sha512-2GjK8/PrGJYDVBcjqGyM2irBLMQXvvkJLbS8VFPlym2uuNz+pPMnwLbNf5njkknUTy3PamjgIRoADpuPPPA6oA==
|
||||
version "2.47.10"
|
||||
resolved "https://registry.yarnpkg.com/@supabase/supabase-js/-/supabase-js-2.47.10.tgz#310ce81dc734116f9445dbce7f9341ae1c24d834"
|
||||
integrity sha512-vJfPF820Ho5WILYHfKiBykDQ1SB9odTHrRZ0JxHfuLMC8GRvv21YLkUZQK7/rSVCkLvD6/ZwMWaOAfdUd//guw==
|
||||
dependencies:
|
||||
"@supabase/auth-js" "2.67.1"
|
||||
"@supabase/functions-js" "2.4.3"
|
||||
"@supabase/auth-js" "2.67.3"
|
||||
"@supabase/functions-js" "2.4.4"
|
||||
"@supabase/node-fetch" "2.6.15"
|
||||
"@supabase/postgrest-js" "1.17.7"
|
||||
"@supabase/realtime-js" "2.11.2"
|
||||
|
Loading…
Reference in New Issue
Block a user