diff --git a/cutt/main.py b/cutt/main.py index 0431b93..596fbb8 100644 --- a/cutt/main.py +++ b/cutt/main.py @@ -13,6 +13,7 @@ from cutt.analysis import analysis_router from cutt.mail import send_forgotten_password_link from cutt.security import ( get_current_active_user, + join_team_token, login_for_access_token, logout, register, @@ -59,6 +60,9 @@ team_router = APIRouter( ) team_router.add_api_route("/list", endpoint=list_teams, methods=["GET"]) team_router.add_api_route("/add", endpoint=add_team, methods=["POST"]) +team_router.add_api_route( + "/join_link/{team_id}", endpoint=join_team_token, methods=["GET"] +) wrong_user_id_exception = HTTPException( diff --git a/cutt/player.py b/cutt/player.py index 197a1b9..1d42c7d 100644 --- a/cutt/player.py +++ b/cutt/player.py @@ -10,6 +10,7 @@ from cutt.security import ( change_password, get_current_active_user, read_player_me, + verify_one_time_token, verify_team_scope, ) from cutt.demo import demo_players @@ -197,14 +198,46 @@ def add_player_to_team(player_id: int, team_id: int): player = session.exec(select(P).where(P.id == player_id)).one() team = session.exec(select(Team).where(Team.id == team_id)).one() if player and team: - team.players.append(player) - session.add(team) - session.commit() - return PlainTextResponse( - f"added {player.display_name} ({player.username}) to {team.name}" + if player in team.players: + return PlainTextResponse( + f"{player.display_name} ({player.username}) is already part of {team.name}" + ) + else: + team.players.append(player) + session.add(team) + session.commit() + return PlainTextResponse( + f"added {player.display_name} ({player.username}) to {team.name}" + ) + else: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="something went wrong", ) +class JoinRequest(BaseModel): + token: str + team_id: int + + +def join_team(r: JoinRequest, user: Annotated[P, Depends(get_current_active_user)]): + payload = verify_one_time_token(r.token) + action: str = payload.get("sub") + if action != "join": + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="wrong type of token.", + ) + team_id: int = payload.get("team_id") + if team_id != r.team_id: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="wrong team", + ) + return add_player_to_team(user.id, r.team_id) + + def add_players(players: list[P]): with Session(engine) as session: for player in players: @@ -218,7 +251,7 @@ async def list_all_players(): async def list_players( - team_id: int, user: Annotated[Player, Depends(get_current_active_user)] + team_id: int, user: Annotated[P, Depends(get_current_active_user)] ): if team_id == 42: return [ @@ -313,6 +346,11 @@ player_router.add_api_route( endpoint=remove_player_from_team, methods=["DELETE"], ) +player_router.add_api_route( + "/{team_id}/join", + endpoint=join_team, + methods=["POST"], +) player_router.add_api_route( "/{team_id}/list", endpoint=list_players, diff --git a/cutt/security.py b/cutt/security.py index 99da870..17af087 100644 --- a/cutt/security.py +++ b/cutt/security.py @@ -225,16 +225,19 @@ def set_password_token(user: Player): return token -def register_token(team_id: int): +def join_team_token(team_id: int): with Session(engine) as session: team = session.exec(select(Team).where(Team.id == team_id)).one() if team: expire = timedelta(days=30) token = create_access_token( - data={"sub": "register", "team_id": team_id, "name": team.name}, + data={"sub": "join", "team_id": team_id, "name": team.name}, expires_delta=expire, ) - return token + if token: + session.add(TokenDB(token=token)) + session.commit() + return PlainTextResponse(f"https://cutt.0124816.xyz/join?token={token}") def verify_one_time_token(token: str): @@ -344,7 +347,7 @@ class RegisterRequest(BaseModel): async def register(req: RegisterRequest): payload = verify_one_time_token(req.token) action: str = payload.get("sub") - if action != "register": + if action != "join": raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="wrong type of token.", diff --git a/frontend/src/Form.tsx b/frontend/src/Form.tsx index 4e4f0bd..d7ead1e 100644 --- a/frontend/src/Form.tsx +++ b/frontend/src/Form.tsx @@ -104,7 +104,7 @@ function TypeDnD({ user, teams, players }: PlayerInfoProps) { useEffect(() => { handleGet(); - }, [players]); + }, [players, teams]); const [dialog, setDialog] = useState("dialog"); const dialogRef = useRef(null); @@ -245,7 +245,7 @@ function MVPDnD({ user, teams, players }: PlayerInfoProps) { const activeTeam = teams.teams.find((team) => team.id == teams.activeTeam); activeTeam && setMixed(activeTeam.mixed); handleGet(); - }, [players]); + }, [players, teams]); useEffect(() => { handleGet(); @@ -364,7 +364,7 @@ function ChemistryDnDMobile({ user, teams, players }: PlayerInfoProps) { useEffect(() => { handleGet(); - }, [players]); + }, [players, teams]); const [dialog, setDialog] = useState("dialog"); const dialogRef = useRef(null); diff --git a/frontend/src/JoinTeam.tsx b/frontend/src/JoinTeam.tsx index d4bada8..c504019 100644 --- a/frontend/src/JoinTeam.tsx +++ b/frontend/src/JoinTeam.tsx @@ -1,14 +1,17 @@ import { jwtDecode } from "jwt-decode"; -import { PassToken } from "./api"; +import { apiAuth, PassToken } from "./api"; import { useEffect, useState } from "react"; +import { UsersIcon } from "lucide-react"; +import { useNavigate } from "react-router"; import { useSession } from "./Session"; export const Join = () => { const [token, setToken] = useState(""); - const { user } = useSession(); const [teamName, setTeamName] = useState(""); const [teamID, setTeamID] = useState(); const [error, setError] = useState(""); + const { teams, setTeams } = useSession(); + const navigate = useNavigate(); useEffect(() => { const params = new URLSearchParams(window.location.search); @@ -17,7 +20,7 @@ export const Join = () => { setToken(token); try { const payload = jwtDecode(token); - if (payload.sub === "register") { + if (payload.sub === "join") { if (payload.team_id) setTeamID(payload.team_id); } else { setError("not a valid token for joining"); @@ -29,15 +32,32 @@ export const Join = () => { } else setError("no token found"); }, []); + async function handleJoin() { + const r = await apiAuth( + `player/${teamID}/join`, + { token: token, team_id: teamID }, + "POST" + ); + if (r.detail) setError(r.detail); + else { + setTeams({ ...teams, activeTeam: teamID }); + navigate("/", { replace: true }); + } + } + return (
-
+

Join "{teamName}"

-
+ {error}
); diff --git a/frontend/src/Register.tsx b/frontend/src/Register.tsx index 1e56ef5..bedd804 100644 --- a/frontend/src/Register.tsx +++ b/frontend/src/Register.tsx @@ -1,8 +1,7 @@ import { jwtDecode } from "jwt-decode"; import { FormEvent, useEffect, useState } from "react"; import { baseUrl, Gender, PassToken } from "./api"; -import { Link, useLocation, useNavigate } from "react-router"; -import { TriangleAlert } from "lucide-react"; +import { useLocation, useNavigate } from "react-router"; export const Register = () => { const [name, setName] = useState(""); @@ -28,7 +27,7 @@ export const Register = () => { setToken(token); try { const payload = jwtDecode(token); - if (payload.sub === "register") { + if (payload.sub === "join") { if (payload.team_id) setTeamID(payload.team_id); } else { setError("not a valid token for registration"); @@ -107,17 +106,6 @@ export const Register = () => {

Register {teamName && `in team "${teamName}"`}

-
-
- - - - - If you already have an account,{" "} - login first. - -
-
diff --git a/frontend/src/Session.tsx b/frontend/src/Session.tsx index d83399a..a751ba7 100644 --- a/frontend/src/Session.tsx +++ b/frontend/src/Session.tsx @@ -7,15 +7,10 @@ import { } from "react"; import { apiAuth, currentUser, loadPlayers, logout, User } from "./api"; import { Login } from "./Login"; -import Header from "./Header"; import { Team } from "./types"; import Loading from "./Loading"; -import { - useLocation, - useNavigate, - useParams, - useSearchParams, -} from "react-router"; +import { Link, useLocation } from "react-router"; +import { TriangleAlert } from "lucide-react"; export interface SessionProviderProps { children: ReactNode; @@ -53,8 +48,6 @@ export function SessionProvider(props: SessionProviderProps) { const [error, setError] = useState(null); const [loading, setLoading] = useState(false); const location = useLocation(); - let [searchParams] = useSearchParams(); - const navigate = useNavigate(); function loadUser() { setLoading(true); @@ -71,8 +64,12 @@ export function SessionProvider(props: SessionProviderProps) { } async function loadTeam() { - const teams: Team[] = await apiAuth("player/me/teams", null, "GET"); - if (teams) setTeams({ teams: teams, activeTeam: teams[0].id }); + const loaded_teams: Team[] = await apiAuth("player/me/teams", null, "GET"); + if (loaded_teams) + setTeams({ + teams: loaded_teams, + activeTeam: teams?.activeTeam || loaded_teams[0].id, + }); } async function reloadPlayers() { @@ -110,8 +107,6 @@ export function SessionProvider(props: SessionProviderProps) { if (loading || (!error && !user)) content =
{Loading}
; else if (error) { - if (location.pathname === "/join" && !searchParams.get("login")) - navigate(`/register${location.search}`); content = (
@@ -125,6 +120,19 @@ export function SessionProvider(props: SessionProviderProps) { />

+ {location.pathname === "/join" && ( +
+
+ + + + + If you don't already have an account,{" "} + register here. + +
+
+ )}