Skip to content

Commit 57d6fc2

Browse files
committed
Introduce basic authentication models
1 parent 8c17466 commit 57d6fc2

File tree

12 files changed

+437
-23
lines changed

12 files changed

+437
-23
lines changed

app/src/server/auth/mod.rs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
use poem::Result;
2+
use poem_openapi::{payload::Json, Object, OpenApi};
3+
use serde::{Deserialize, Serialize};
4+
use sqids::Sqids;
5+
use rand::Rng;
6+
7+
pub struct AuthApi;
8+
9+
#[derive(Deserialize, Serialize, Object)]
10+
pub struct GuestResponse {
11+
pub token: String,
12+
pub user: Option<GuestUser>,
13+
}
14+
15+
#[OpenApi]
16+
impl AuthApi {
17+
#[oai(path = "/auth/guest", method = "post")]
18+
pub async fn guest(&self) -> Result<Json<GuestResponse>> {
19+
let new_id = Sqids::default();
20+
let random_number = rand::thread_rng().gen_range(0..u64::MAX);
21+
let new_id = new_id.encode(&[0, random_number]).unwrap();
22+
23+
Ok(Json(GuestResponse {
24+
token: new_id.clone(),
25+
user: Some(GuestUser {
26+
user_id: new_id,
27+
name: "John Doe".to_string(),
28+
}),
29+
}))
30+
}
31+
}
32+
33+
/**
34+
* This is a guest user
35+
*/
36+
#[derive(Deserialize, Serialize, Object)]
37+
pub struct GuestUser {
38+
pub user_id: String,
39+
pub name: String,
40+
}

app/src/server/mod.rs

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,11 @@ use poem::{
1212
use poem_openapi::{payload::Html, OpenApi, OpenApiService, Tags};
1313

1414
use maps::MapsApi;
15-
// use request::RequestApi;
16-
// use stats::{balance::BalanceApi, StatsApi};
1715
use tracing::info;
1816

1917
use crate::state::AppState;
2018
use tracing_mw::TraceId;
19+
use auth::AuthApi;
2120

2221
// pub mod auth;
2322
// pub mod channel;
@@ -29,6 +28,7 @@ use tracing_mw::TraceId;
2928
// pub mod stats;
3029
pub mod party;
3130
pub mod maps;
31+
pub mod auth;
3232
pub mod tracing_mw;
3333

3434
#[derive(Tags)]
@@ -37,13 +37,12 @@ enum ApiTags {
3737
Party,
3838
/// Maps Related Operations
3939
Maps,
40+
/// Auth Related Operations
41+
Auth,
4042
}
4143

4244
fn get_api() -> impl OpenApi {
43-
(
44-
PartyApi,
45-
MapsApi
46-
)
45+
(PartyApi, MapsApi, AuthApi)
4746
}
4847

4948
pub async fn start_http(state: AppState) {

web/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
"author": "V3X Labs",
1414
"license": "GPL-3.0-only",
1515
"dependencies": {
16+
"@radix-ui/react-avatar": "^1.1.3",
1617
"@radix-ui/react-dialog": "^1.1.6",
1718
"@radix-ui/react-switch": "^1.1.3",
1819
"@tanstack/query-sync-storage-persister": "^5.64.1",
@@ -23,6 +24,7 @@
2324
"@types/react": "^19.0.7",
2425
"@types/react-dom": "^19.0.3",
2526
"@vitejs/plugin-react": "^4.3.4",
27+
"@xstate/store": "^3.3.0",
2628
"autoprefixer": "^10.4.20",
2729
"classnames": "^2.5.1",
2830
"cx": "^25.3.1",

web/pnpm-lock.yaml

Lines changed: 46 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

web/src/api/auth.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { MutationOptions, useMutation } from "@tanstack/react-query";
2+
import { components } from "./schema.gen";
3+
import { useApi } from "./api";
4+
5+
export type GuestResponse = components['schemas']['GuestResponse'];
6+
7+
export const useGuestAuth = (extra?: Partial<MutationOptions<GuestResponse, undefined, undefined>>) => {
8+
return useMutation({
9+
mutationFn: async () => {
10+
const response = await useApi('/auth/guest', 'post', {})
11+
12+
return response.data;
13+
},
14+
...extra,
15+
})
16+
}

web/src/api/schema.gen.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,10 +114,57 @@ export interface paths {
114114
patch?: never;
115115
trace?: never;
116116
};
117+
"/auth/guest": {
118+
parameters: {
119+
query?: never;
120+
header?: never;
121+
path?: never;
122+
cookie?: never;
123+
};
124+
get?: never;
125+
put?: never;
126+
post: {
127+
parameters: {
128+
query?: never;
129+
header?: never;
130+
path?: never;
131+
cookie?: never;
132+
};
133+
requestBody?: never;
134+
responses: {
135+
200: {
136+
headers: {
137+
[name: string]: unknown;
138+
};
139+
content: {
140+
"application/json; charset=utf-8": components["schemas"]["GuestResponse"];
141+
};
142+
};
143+
};
144+
};
145+
delete?: never;
146+
options?: never;
147+
head?: never;
148+
patch?: never;
149+
trace?: never;
150+
};
117151
}
118152
export type webhooks = Record<string, never>;
119153
export interface components {
120154
schemas: {
155+
/** GuestResponse */
156+
GuestResponse: {
157+
token: string;
158+
user?: components["schemas"]["GuestUser"];
159+
};
160+
/**
161+
* GuestUser
162+
* @description * This is a guest user
163+
*/
164+
GuestUser: {
165+
user_id: string;
166+
name: string;
167+
};
121168
/** MapData */
122169
MapData: {
123170
id: string;

web/src/components/Navbar.tsx

Lines changed: 93 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,105 @@
1+
import { useAuth } from "@/hooks/auth";
2+
import { Dialog, DialogClose, DialogContent, DialogTrigger } from "@radix-ui/react-dialog";
13
import { Link } from "@tanstack/react-router";
4+
import { FC } from "react";
5+
import { Modal } from "./modal/Modal";
6+
import { FaSteam } from "react-icons/fa";
7+
import { useGuestAuth } from "@/api/auth";
8+
import { Avatar } from "./auth/Avatar";
9+
import { LuLogOut } from "react-icons/lu";
10+
import { toast } from "sonner";
211

312
export const Navbar = () => {
413
return (
514
<>
6-
<div className="w-full bg-primary fixed sm:relative">
7-
<div className="flex items-stretch gap-2 h-full">
8-
<div className="px-1 max-h-full min-w-2 flex items-center gap-2 bg-secondary">
9-
<div className="size-8 rounded-md">
10-
<img src="/lock.code.png" alt="lock.code" className="w-full h-full object-contain" />
15+
<div className="w-full bg-primary fixed sm:relative flex justify-between items-center">
16+
<div className="flex items-stretch gap-2 h-full">
17+
<div className="px-1 max-h-full min-w-2 flex items-center gap-2 bg-secondary">
18+
<div className="size-8 rounded-md">
19+
<img src="/lock.code.png" alt="lock.code" className="w-full h-full object-contain" />
20+
</div>
1121
</div>
22+
<Link to="/" className="text-accent text-base hover:underline py-2 block">
23+
<span>code</span>
24+
<span className="text-secondary">.</span>
25+
<span>fishing</span>
26+
</Link>
27+
</div>
28+
<div className="flex items-center h-full gap-2 flex-1 justify-end">
29+
<UserProfile />
1230
</div>
13-
<Link to="/" className="text-accent text-base hover:underline py-2 block">
14-
<span>code</span>
15-
<span className="text-secondary">.</span>
16-
<span>fishing</span>
17-
</Link>
18-
</div>
1931
</div>
2032
<div className="h-12 w-full sm:hidden" />
2133
</>
2234
)
2335
};
36+
37+
export const UserProfile: FC<{}> = () => {
38+
const { isAuthenticated, logout, user } = useAuth();
39+
40+
if (!isAuthenticated) {
41+
return (
42+
<LoginModal />
43+
);
44+
}
45+
46+
return (
47+
<div className="flex items-center gap-1">
48+
<div className="flex items-center gap-1">
49+
<Avatar src={user?.avatar} seed={user?.user_id} />
50+
<span>{user?.name}</span>
51+
</div>
52+
<button className="button flex items-center gap-1" onClick={logout}>
53+
<LuLogOut className="size-4" />
54+
<span>Logout</span>
55+
</button>
56+
</div>
57+
);
58+
};
59+
60+
export const LoginModal: FC<{}> = () => {
61+
return (
62+
<Dialog>
63+
<DialogTrigger asChild>
64+
<button className="button">
65+
<span>Login</span>
66+
</button>
67+
</DialogTrigger>
68+
<Modal size="medium">
69+
<LoginModalContent />
70+
</Modal>
71+
</Dialog>
72+
)
73+
}
74+
75+
export const LoginModalContent: FC<{}> = () => {
76+
const { login } = useAuth();
77+
const { mutate: guestAuth } = useGuestAuth({
78+
onSuccess: (data) => {
79+
login(data.token, data.user);
80+
toast.success("Logged in as guest");
81+
}
82+
});
83+
84+
return (
85+
<div className="flex flex-wrap gap-4">
86+
<div className="flex flex-col gap-2 sm:w-[calc(50%-1rem)]">
87+
<h2>Sign in as Guest</h2>
88+
<p className="text-secondary">Click here to continue as an anonymous user.</p>
89+
<DialogClose asChild>
90+
<button className="button" onClick={() => guestAuth(undefined, undefined)}>
91+
<span>Continue as Guest</span>
92+
</button>
93+
</DialogClose>
94+
</div>
95+
<div className="flex flex-col gap-2 sm:w-[calc(50%-1rem)]">
96+
<h2>Sign in with Steam</h2>
97+
<p className="text-secondary">Click here to sign in with your Steam account.</p>
98+
<button className="button flex items-center justify-center gap-2" disabled>
99+
<FaSteam className="size-4" />
100+
<span>Sign in with Steam</span>
101+
</button>
102+
</div>
103+
</div>
104+
)
105+
}

0 commit comments

Comments
 (0)