Skip to content

Commit e21c62d

Browse files
Dashboard page reworks (#677)
* ensure application rating uniqueness per rater * hardcode share link app url * logout endpoint * fix logout endpoint * add "logout" button to dashboard sidebar * dev mode for inviting members * improved invite page * fix usage of `getCurrentUser`
1 parent e9078ab commit e21c62d

17 files changed

Lines changed: 121 additions & 92 deletions

File tree

backend/server/src/handler/auth.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,32 @@ pub async fn google_callback(
153153
Ok((jar.add(cookie), Redirect::to(redirect_url.as_str())))
154154
}
155155

156+
pub async fn logout(
157+
State(state): State<AppState>,
158+
jar: CookieJar
159+
) -> Result<impl IntoResponse, ChaosError> {
160+
let domain = if state.is_dev_env {
161+
"localhost"
162+
} else {
163+
"devsoc.app"
164+
};
165+
166+
let empty_cookie= Cookie::build(("auth_token", ""))
167+
.http_only(true) // Prevent JavaScript access
168+
.expires(Expiration::DateTime(OffsetDateTime::now_utc() + time::Duration::days(5))) // Set an expiration time of 5 days, TODO: read from env?
169+
.secure(!state.is_dev_env) // Send only over HTTPS, comment out for testing
170+
.domain(domain)
171+
.path("/");
172+
173+
let redirect = if state.is_dev_env {
174+
"http://localhost:3000"
175+
} else {
176+
"https://chaos.devsoc.app"
177+
};
178+
179+
Ok((jar.remove(empty_cookie), Redirect::to(redirect)))
180+
}
181+
156182
pub struct DevLoginHandler;
157183
impl DevLoginHandler {
158184

backend/server/src/handler/organisation.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -381,6 +381,7 @@ impl OrganisationHandler {
381381
admin.user_id,
382382
request_body.email,
383383
state.email_credentials.clone(),
384+
state.is_dev_env,
384385
&mut state.snowflake_generator,
385386
&mut transaction.tx,
386387
)

backend/server/src/models/app.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use crate::handler::answer::AnswerHandler;
22
use crate::handler::application::ApplicationHandler;
3-
use crate::handler::auth::{google_callback, google_auth_init, DevLoginHandler};
3+
use crate::handler::auth::{DevLoginHandler, google_auth_init, google_callback, logout};
44
use crate::handler::campaign::CampaignHandler;
55
use crate::handler::email_template::EmailTemplateHandler;
66
use crate::handler::offer::OfferHandler;
@@ -201,6 +201,7 @@ pub async fn app() -> Result<(Router, AppState), ChaosError> {
201201
let router = Router::new()
202202
.route("/", get(|| async { "Join DevSoc! https://devsoc.app/" }))
203203
.route("/auth/google", get(google_auth_init))
204+
.route("/auth/logout", get(logout))
204205
.route("/api/auth/callback/google", get(google_callback))
205206
.route("/api/v1/dev/super_admin_login", get(DevLoginHandler::dev_super_admin_login))
206207
.route("/api/v1/dev/org_admin_login", get(DevLoginHandler::dev_org_admin_login))

backend/server/src/models/application.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -668,7 +668,7 @@ impl Application {
668668
669669
coalesce(
670670
to_jsonb(
671-
array_agg(
671+
array_agg(DISTINCT
672672
jsonb_build_object(
673673
'id', ar.id,
674674
'rater_id', reviewer.id,
@@ -690,7 +690,7 @@ impl Application {
690690
WHERE crc.campaign_id = a.campaign_id
691691
),
692692
'updated_at', ar.updated_at
693-
) ORDER BY ar.updated_at DESC
693+
)
694694
) FILTER (WHERE ar.id IS NOT NULL)
695695
),
696696
'[]'::jsonb

backend/server/src/models/organisation.rs

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -745,6 +745,7 @@ impl Organisation {
745745
inviting_user_id: i64,
746746
email: String,
747747
email_credentials: EmailCredentials,
748+
is_dev_env: bool,
748749
snowflake_generator: &mut SnowflakeIdGenerator,
749750
transaction: &mut Transaction<'_, Postgres>,
750751
) -> Result<String, ChaosError> {
@@ -830,14 +831,18 @@ impl Organisation {
830831
.execute(transaction.deref_mut())
831832
.await?;
832833

833-
ChaosEmail::send_message(
834-
None,
835-
email,
836-
"You have been invited to join an organisation on Chaos".to_string(),
837-
format!("You have been invited to join an organisation on Chaos. Please use the following link to accept the invite: https://chaos.devsoc.app/dashboard/invite/{code}").to_string(),
838-
email_credentials
839-
)
840-
.await?;
834+
if is_dev_env {
835+
println!("Invite code for {email}: {code}")
836+
} else {
837+
ChaosEmail::send_message(
838+
None,
839+
email,
840+
"You have been invited to join an organisation on Chaos".to_string(),
841+
format!("You have been invited to join an organisation on Chaos. Please use the following link to accept the invite: https://chaos.devsoc.app/dashboard/invite/{code}").to_string(),
842+
email_credentials
843+
)
844+
.await?;
845+
}
841846

842847
return Ok(code);
843848
}

backend/server/src/models/rating.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -317,6 +317,8 @@ impl Rating {
317317
"
318318
INSERT INTO application_ratings (id, application_id, rater_id, comment)
319319
VALUES ($1, $2, $3, $4)
320+
ON CONFLICT (application_id, rater_id)
321+
DO UPDATE SET comment = $4, updated_at = NOW()
320322
",
321323
rating_id,
322324
application_id,

frontend-nextjs/src/app/[lang]/dashboard/invite/[code]/invite-client.tsx

Lines changed: 44 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,16 @@ import Link from "next/link";
77
import { useEffect, useState } from "react";
88
import { useRouter } from "next/navigation";
99
import { useQuery } from "@tanstack/react-query";
10+
import { User } from "@/models/user";
11+
import { LogOut } from "lucide-react";
1012

1113
type Props = {
1214
code: string;
15+
currentUser?: User;
1316
dict: any;
1417
};
1518

16-
export default function InviteClient({ code, dict }: Props) {
19+
export default function InviteClient({ code, currentUser, dict }: Props) {
1720
const router = useRouter();
1821

1922
const [status, setStatus] = useState<"idle" | "loading" | "success" | "error">("idle");
@@ -46,18 +49,17 @@ export default function InviteClient({ code, dict }: Props) {
4649
const inviteInvalid = !invite || invite.expired || invite.used || status === "success";
4750

4851
return (
49-
<div className="max-w-xl mx-auto p-6 flex flex-col gap-4">
52+
<div className="max-w-3xl mx-auto p-6 flex flex-col gap-4">
5053
<h1 className="text-2xl font-bold">{dict.dashboard.invite.title}</h1>
51-
<p className="text-sm text-muted-foreground">
54+
<p className="text-lg">
5255
{invite?.organisation_name}{" "}
53-
{invite
54-
? dict.dashboard.invite.invited_by
55-
: dict?.common?.loading ?? "Loading..."}
56+
{dict.dashboard.invite.invited_by}
5657
</p>
5758
{/* Show the email the invite was sent to */}
5859
{invite && (
59-
<p className="text-sm text-muted-foreground">
60-
{dict.dashboard.invite.sent_to}: <span className="font-medium">{invite.email}</span>
60+
<p className="text-lg">
61+
{dict.dashboard.invite.sent_to}: <span className="font-medium">{invite.email}</span>.
62+
{(!currentUser || currentUser.email !== invite.email) && dict.dashboard.invite.sent_to_note}
6163
</p>
6264
)}
6365
{/* Show the expired message if the invite has expired */}
@@ -74,25 +76,41 @@ export default function InviteClient({ code, dict }: Props) {
7476
{message}
7577
</p>
7678
)}
77-
<div className="flex flex-col gap-2">
78-
<Link href={`/login?to=/dashboard/invite/${code}`} className="w-full">
79-
<Button variant="outline" className="w-full">
80-
{dict.dashboard.invite.login_cta}
79+
{
80+
currentUser && currentUser.email === invite?.email && (
81+
<Button onClick={handleAccept} disabled={status === "loading" || inviteInvalid} className="w-full">
82+
{status === "loading" ? "Loading..." : dict.dashboard.invite.accept_cta}
8183
</Button>
82-
</Link>
83-
<Button
84-
onClick={handleAccept}
85-
disabled={status === "loading" || inviteInvalid}
86-
className="w-full"
87-
>
88-
{status === "loading" ? "Loading..." : dict.dashboard.invite.accept_cta}
89-
</Button>
90-
{/* Show the wrong account message if the account is not invited */}
91-
<p className="text-xs text-muted-foreground">
92-
{dict.dashboard.invite.wrong_account}
93-
</p>
94-
</div>
95-
</div>
84+
)
85+
}
86+
{/* Show the wrong account message if the account is not invited */}
87+
{
88+
currentUser && currentUser.email !== invite?.email && (
89+
<>
90+
<p className="text-xl">
91+
{dict.dashboard.invite.wrong_account}
92+
</p>
93+
<Link href={`${process.env.NEXT_PUBLIC_API_BASE_URL || "https://chaos-api.devsoc.app"}/auth/logout`} className="w-full">
94+
<Button variant="outline" className="w-full">
95+
<LogOut />
96+
{dict.common.logout}
97+
</Button>
98+
</Link>
99+
</>
100+
)
101+
}
102+
{
103+
!currentUser && (
104+
<div className="flex flex-col gap-2">
105+
<Link href={`/login?to=/dashboard/invite/${code}`} className="w-full">
106+
<Button variant="outline" className="w-full">
107+
{dict.dashboard.invite.login_cta}
108+
</Button>
109+
</Link>
110+
</div>
111+
)
112+
}
113+
</div >
96114
);
97115
}
98116

frontend-nextjs/src/app/[lang]/dashboard/invite/[code]/page.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import { getDictionary } from "@/app/[lang]/dictionaries";
22
import { getInvite } from "@/models/invite";
33
import { HydrationBoundary, QueryClient, dehydrate } from "@tanstack/react-query";
44
import InviteClient from "./invite-client";
5+
import { getCurrentUser } from "@/lib";
6+
import { User } from "@/models/user";
57

68
export default async function Page({ params }: { params: Promise<{ lang: string; code: string }> }) {
79
const { lang, code } = await params;
@@ -13,9 +15,15 @@ export default async function Page({ params }: { params: Promise<{ lang: string;
1315
queryFn: () => getInvite(code),
1416
});
1517

18+
19+
let user: User | undefined = undefined;
20+
try {
21+
user = await getCurrentUser(false);
22+
} catch (_) {}
23+
1624
return (
1725
<HydrationBoundary state={dehydrate(queryClient)}>
18-
<InviteClient code={code} dict={dict} />
26+
<InviteClient code={code} dict={dict} currentUser={user} />
1927
</HydrationBoundary>
2028
);
2129
}

frontend-nextjs/src/app/[lang]/dashboard/organisation/[orgId]/(home)/dashboard.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { useQuery } from "@tanstack/react-query";
66
export default function Dashboard() {
77
const { data } = useQuery({
88
queryKey: ['user'],
9-
queryFn: getCurrentUser,
9+
queryFn: () => getCurrentUser(),
1010
});
1111

1212
return (

frontend-nextjs/src/app/[lang]/dashboard/organisation/[orgId]/campaigns/[campaignId]/campaign-details.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ export default function CampaignDetails({ campaignId, orgId, dict }: { campaignI
124124
</Link>
125125
</ButtonGroup>
126126
<ButtonGroup>
127-
<CopyButton value={`${process.env.NEXT_PUBLIC_APP_URL}/campaign/${campaign?.organisation_slug}/${campaign?.campaign_slug}`}>
127+
<CopyButton value={`https://chaos.devsoc.app/campaign/${campaign?.organisation_slug}/${campaign?.campaign_slug}`}>
128128
<Share className="w-4 h-4" /> {dict.dashboard.campaigns.share_link}
129129
</CopyButton>
130130
<CopyButton value={campaignId}>

0 commit comments

Comments
 (0)