diff --git a/service/src/auth/model.rs b/service/src/auth/model.rs index 54dafae..c6cd5ea 100644 --- a/service/src/auth/model.rs +++ b/service/src/auth/model.rs @@ -79,9 +79,30 @@ impl InsertUser { } } +#[derive(Debug, Serialize, Deserialize)] +pub struct ResponseUser { + pub email: String, + pub role: String, + pub first_name: String, + pub last_name: String, +} + +impl From for ResponseUser { + fn from(user: QueryUser) -> Self { + ResponseUser { + email: user.email, + role: user.role, + first_name: user.first_name, + last_name: user.last_name, + } + } +} + #[derive(Debug, Serialize, Deserialize)] pub struct JwtAuth { - pub access_token_uuid: uuid::Uuid + pub access_token_uuid: uuid::Uuid, + pub email: String, + pub role: String, } impl FromRequest for JwtAuth { @@ -140,8 +161,8 @@ impl FromRequest for JwtAuth { }; match QueryUser::get_by_email(&user_email) { - Ok(_) => { - ready(Ok(JwtAuth { access_token_uuid })) + Ok(user) => { + ready(Ok(JwtAuth { access_token_uuid, email: user.email, role: user.role })) } Err(err) => return ready(Err(ActixError::from(ServiceError { status: 500, diff --git a/service/src/auth/routes.rs b/service/src/auth/routes.rs index 284ef4e..54cba8e 100644 --- a/service/src/auth/routes.rs +++ b/service/src/auth/routes.rs @@ -5,7 +5,7 @@ use log::error; use redis::AsyncCommands; use siren::ServiceError; -use crate::{auth::{LoginRequest, RegisterUser, InsertUser, QueryUser, verify_password, generate_token, JwtAuth}, db}; +use crate::{auth::{LoginRequest, RegisterUser, InsertUser, QueryUser, verify_password, generate_token, JwtAuth, ResponseUser}, db}; #[post("/register")] async fn register(user: web::Json) -> HttpResponse { @@ -96,17 +96,17 @@ async fn login(request: web::Json) -> HttpResponse { let access_cookie = Cookie::build("access_token", access_token_details.token.clone().unwrap()) .path("/") - .max_age(Duration::new(access_token_max_age, 0)) + .max_age(Duration::new(access_token_max_age * 60, 0)) .http_only(true) .finish(); let refresh_cookie = Cookie::build("refresh_token", refresh_token_details.token.clone().unwrap()) .path("/") - .max_age(Duration::new(refresh_token_max_age, 0)) + .max_age(Duration::new(refresh_token_max_age * 60, 0)) .http_only(true) .finish(); let logged_in_cookie = Cookie::build("logged_in", "true") .path("/") - .max_age(Duration::new(access_token_max_age, 0)) + .max_age(Duration::new(access_token_max_age * 60, 0)) .http_only(false) .finish(); @@ -135,10 +135,24 @@ async fn logout(req: HttpRequest, auth: JwtAuth) -> HttpResponse { #[get("/me")] async fn me(auth: JwtAuth) -> HttpResponse { - HttpResponse::Ok().json(auth) + let query_user = match QueryUser::get_by_email(&auth.email) { + Ok(user) => user, + Err(err) => return ResponseError::error_response(&err) + }; + let user: ResponseUser = query_user.into(); + HttpResponse::Ok().json(user) } pub fn init_routes(config: &mut web::ServiceConfig) { + let r = RegisterUser { + email: "admin".to_string(), + password: "admin".to_string(), + first_name: "Admin".to_string(), + last_name: "Admin".to_string(), + }; + let mut u = r.convert_to_insert().unwrap(); + u.role = "admin".to_string(); + let _ = InsertUser::insert(u); config.service(web::scope("auth") .service(register) .service(login) diff --git a/service/src/bot/api/routes.rs b/service/src/bot/api/routes.rs index 4579e40..8159446 100644 --- a/service/src/bot/api/routes.rs +++ b/service/src/bot/api/routes.rs @@ -6,10 +6,10 @@ use serde::{Serialize, Deserialize}; use serenity::model::prelude::{GuildChannel, ChannelType}; use siren::ServiceError; -use crate::{AppState, bot::commands::audio::{play::play_track, join}, db::guilds::{InsertGuild, QueryGuild}}; +use crate::{AppState, bot::commands::audio::{play::play_track, join}, db::guilds::{InsertGuild, QueryGuild}, auth::JwtAuth}; #[get("/guilds")] -async fn get_guilds(data: web::Data>) -> HttpResponse { +async fn get_guilds(data: web::Data>, auth: JwtAuth) -> HttpResponse { let guild_results = &data.http.get_guilds(None, None).await; let guilds = match guild_results { Ok(guilds) => guilds, @@ -22,7 +22,7 @@ async fn get_guilds(data: web::Data>) -> HttpResponse { } #[get("/{id}/text")] -async fn get_text_channels(id: web::Path, data: web::Data>) -> HttpResponse { +async fn get_text_channels(id: web::Path, data: web::Data>, auth: JwtAuth) -> HttpResponse { let channel_results = &data.http.get_channels(id.parse::().unwrap()).await; let channels = match channel_results { Ok(channels) => channels.iter().filter(|c| c.kind == ChannelType::Text).collect::>(), @@ -35,7 +35,7 @@ async fn get_text_channels(id: web::Path, data: web::Data> } #[get("/{id}/voice")] -async fn get_voice_channels(id: web::Path, data: web::Data>) -> HttpResponse { +async fn get_voice_channels(id: web::Path, data: web::Data>, auth: JwtAuth) -> HttpResponse { let channel_results = &data.http.get_channels(id.parse::().unwrap()).await; let channels = match channel_results { Ok(channels) => channels.iter().filter(|c| c.kind == ChannelType::Voice).collect::>(), @@ -53,7 +53,7 @@ struct ChannelMessage { } #[post("/{guild_id}/text/{channel_id}/message")] -async fn send_message(path: web::Path<(String, String)>, text: web::Json, data: web::Data>) -> HttpResponse { +async fn send_message(path: web::Path<(String, String)>, text: web::Json, data: web::Data>, auth: JwtAuth) -> HttpResponse { let (guild_id, channel_id) = path.into_inner(); let guild_id = match guild_id.parse::() { Ok(id) => id, @@ -115,7 +115,7 @@ struct PlayRequest { } #[post("/{guild_id}/voice/{channel_id}/play")] -async fn play(path: web::Path<(String, String)>, play_request: web::Json, data: web::Data>) -> HttpResponse { +async fn play(path: web::Path<(String, String)>, play_request: web::Json, data: web::Data>, auth: JwtAuth) -> HttpResponse { let (guild_id, channel_id) = path.into_inner(); let guild_id = match guild_id.parse::() { Ok(id) => id, @@ -167,7 +167,7 @@ async fn play(path: web::Path<(String, String)>, play_request: web::Json, data: web::Data>) -> HttpResponse { +async fn stop(path: web::Path, data: web::Data>, auth: JwtAuth) -> HttpResponse { let guild_id = path.into_inner(); let guild_id = match guild_id.parse::() { Ok(id) => id, @@ -189,7 +189,7 @@ async fn stop(path: web::Path, data: web::Data>) -> HttpRe } #[post("/{guild_id}/voice/resume")] -async fn resume(path: web::Path, data: web::Data>) -> HttpResponse { +async fn resume(path: web::Path, data: web::Data>, auth: JwtAuth) -> HttpResponse { let guild_id = path.into_inner(); let guild_id = match guild_id.parse::() { Ok(id) => id, @@ -217,7 +217,7 @@ async fn resume(path: web::Path, data: web::Data>) -> Http } #[post("/{guild_id}/voice/pause")] -async fn pause(path: web::Path, data: web::Data>) -> HttpResponse { +async fn pause(path: web::Path, data: web::Data>, auth: JwtAuth) -> HttpResponse { let guild_id = path.into_inner(); let guild_id = match guild_id.parse::() { Ok(id) => id, @@ -250,7 +250,7 @@ struct SetVolume { } #[get("/{guild_id}/voice/volume")] -async fn get_volume(path: web::Path) -> HttpResponse { +async fn get_volume(path: web::Path, auth: JwtAuth) -> HttpResponse { let guild_id = path.into_inner(); let guild_id = match guild_id.parse::() { Ok(id) => id, @@ -278,7 +278,7 @@ async fn get_volume(path: web::Path) -> HttpResponse { } #[post("/{guild_id}/voice/volume")] -async fn set_volume(path: web::Path, volume: web::Json::, data: web::Data>) -> HttpResponse { +async fn set_volume(path: web::Path, volume: web::Json::, data: web::Data>, auth: JwtAuth) -> HttpResponse { let guild_id = path.into_inner(); let guild_id = match guild_id.parse::() { Ok(id) => id, @@ -307,7 +307,7 @@ async fn set_volume(path: web::Path, volume: web::Json::, dat } #[post("/{guild_id}/voice/skip")] -async fn skip(path: web::Path, data: web::Data>) -> HttpResponse { +async fn skip(path: web::Path, data: web::Data>, auth: JwtAuth) -> HttpResponse { let guild_id = path.into_inner(); let guild_id = match guild_id.parse::() { Ok(id) => id, diff --git a/service/src/db/messages/routes.rs b/service/src/db/messages/routes.rs index 6f1ea21..cb58f36 100644 --- a/service/src/db/messages/routes.rs +++ b/service/src/db/messages/routes.rs @@ -3,7 +3,7 @@ use log::error; use serde::{Serialize, Deserialize}; use siren::{GetResponse, Metadata, ServiceError}; -use crate::db::messages::{QueryMessage, QueryFilters, InsertMessage}; +use crate::{db::messages::{QueryMessage, QueryFilters, InsertMessage}, auth::JwtAuth}; #[derive(Serialize, Deserialize)] struct GetAllParams { @@ -21,7 +21,7 @@ struct GetAllParams { } #[get("/messages")] -async fn get_all(req: HttpRequest) -> HttpResponse { +async fn get_all(req: HttpRequest, auth: JwtAuth) -> HttpResponse { let params = match web::Query::::from_query(req.query_string()) { Ok(params) => params, Err(err) => return ResponseError::error_response(&ServiceError { @@ -64,7 +64,7 @@ async fn get_all(req: HttpRequest) -> HttpResponse { } #[post("/messages")] -async fn create(message: web::Json) -> HttpResponse { +async fn create(message: web::Json, auth: JwtAuth) -> HttpResponse { match InsertMessage::insert(message.into_inner()) { Ok(message) => HttpResponse::Created().json(message), Err(err) => { diff --git a/service/src/db/spells/routes.rs b/service/src/db/spells/routes.rs index 5069476..501a6bf 100644 --- a/service/src/db/spells/routes.rs +++ b/service/src/db/spells/routes.rs @@ -3,7 +3,7 @@ use log::error; use serde::{Serialize, Deserialize}; use siren::{GetResponse, Metadata, ServiceError}; -use crate::db::spells::{QuerySpell, QueryFilters}; +use crate::{db::spells::{QuerySpell, QueryFilters}, auth::JwtAuth}; use super::{Spell, InsertSpell}; @@ -134,7 +134,7 @@ async fn get_by_id(id: web::Path) -> HttpResponse { } #[post("/spells")] -async fn create(spell: web::Json) -> HttpResponse { +async fn create(spell: web::Json, auth: JwtAuth) -> HttpResponse { match InsertSpell::insert(spell.into_inner().into()) { Ok(spell) => HttpResponse::Created().json(Spell::from(spell)), Err(err) => { @@ -145,7 +145,7 @@ async fn create(spell: web::Json) -> HttpResponse { } #[put("/spells/{id}")] -async fn update(id: web::Path, spell: web::Json) -> HttpResponse { +async fn update(id: web::Path, spell: web::Json, auth: JwtAuth) -> HttpResponse { let id = match id.parse::() { Ok(id) => id, Err(err) => return ResponseError::error_response(&ServiceError { @@ -163,7 +163,7 @@ async fn update(id: web::Path, spell: web::Json) -> HttpResponse } #[delete("/spells/{id}")] -async fn delete(id: web::Path) -> HttpResponse { +async fn delete(id: web::Path, auth: JwtAuth) -> HttpResponse { let id = match id.parse::() { Ok(id) => id, Err(err) => return ResponseError::error_response(&ServiceError { diff --git a/ui/src/api/auth.ts b/ui/src/api/auth.ts new file mode 100644 index 0000000..359f603 --- /dev/null +++ b/ui/src/api/auth.ts @@ -0,0 +1,13 @@ +import { getRequest, postRequest } from '.'; + +export async function login(email: string, password: string) { + return await postRequest('auth/login', { email, password }, { withCredentials: true }); +} + +export async function logout() { + return await postRequest('auth/logout', {}, { withCredentials: true }); +} + +export async function me() { + return await getRequest('auth/me', { withCredentials: true }); +} diff --git a/ui/src/api/auth.types.ts b/ui/src/api/auth.types.ts new file mode 100644 index 0000000..7635eea --- /dev/null +++ b/ui/src/api/auth.types.ts @@ -0,0 +1,6 @@ +export interface User { + email: string; + role: string; + first_name: string; + last_name: string; +} diff --git a/ui/src/api/guilds.ts b/ui/src/api/guilds.ts index 3b21ea8..a3f662e 100644 --- a/ui/src/api/guilds.ts +++ b/ui/src/api/guilds.ts @@ -2,12 +2,12 @@ import { getRequest, postRequest } from '.'; import { GuildChannel, GuildInfo } from './guilds.types'; export async function getGuilds(): Promise { - const response = await getRequest('guilds', {}); + const response = await getRequest('guilds'); return response?.data || { data: [] }; } export async function getTextChannels(guildId: number): Promise { - const response = await getRequest(`guilds/${guildId}/text`, {}); + const response = await getRequest(`guilds/${guildId}/text`); return response?.data || { data: [] }; } @@ -16,7 +16,7 @@ export async function sendMessage(guildId: number, channelId: number, message: s } export async function getVoiceChannels(guildId: number): Promise { - const response = await getRequest(`guilds/${guildId}/voice`, {}); + const response = await getRequest(`guilds/${guildId}/voice`); return response?.data || { data: [] }; } @@ -45,6 +45,6 @@ export async function skipTrack(guildId: number): Promise { } export async function getVolume(guildId: number): Promise { - const response = await getRequest(`guilds/${guildId}/voice/volume`, {}); + const response = await getRequest(`guilds/${guildId}/voice/volume`); return response?.data?.volume || 0; } diff --git a/ui/src/api/index.ts b/ui/src/api/index.ts index d587e68..bce1031 100644 --- a/ui/src/api/index.ts +++ b/ui/src/api/index.ts @@ -5,10 +5,10 @@ const servicePort = process.env.SERVICE_PORT || 5000; export async function getRequest( url: string, - params: AxiosRequestConfig + config?: AxiosRequestConfig ): Promise | undefined> { const response = await axios - .get(`${serviceHost}:${servicePort}/${url}`, { params }) + .get(`${serviceHost}:${servicePort}/${url}`, config) .catch((error) => console.error(error)); return response || undefined; } diff --git a/ui/src/api/spells.ts b/ui/src/api/spells.ts index de28c1a..426c5d3 100644 --- a/ui/src/api/spells.ts +++ b/ui/src/api/spells.ts @@ -20,20 +20,22 @@ interface GetSpellsParams { export async function getSpells(params?: GetSpellsParams): Promise { const response = await getRequest('dnd/spells', { - name: params?.name, - like_name: params?.like_name, - schools: params?.schools?.join(','), - levels: params?.levels?.join(','), - ritual: params?.ritual, - concentration: params?.concentration, - classes: params?.classes?.join(','), - damage_inflict: params?.damage_inflict?.join(','), - damage_resist: params?.damage_resist?.join(','), - conditions: params?.conditions?.join(','), - saving_throw: params?.saving_throw?.join(','), - attack_type: params?.attack_type?.join(','), - limit: params?.limit, - page: params?.page + params: { + name: params?.name, + like_name: params?.like_name, + schools: params?.schools?.join(','), + levels: params?.levels?.join(','), + ritual: params?.ritual, + concentration: params?.concentration, + classes: params?.classes?.join(','), + damage_inflict: params?.damage_inflict?.join(','), + damage_resist: params?.damage_resist?.join(','), + conditions: params?.conditions?.join(','), + saving_throw: params?.saving_throw?.join(','), + attack_type: params?.attack_type?.join(','), + limit: params?.limit, + page: params?.page + } }); return response?.data || { data: [] }; } diff --git a/ui/src/api/users.ts b/ui/src/api/users.ts deleted file mode 100644 index eb2c6f5..0000000 --- a/ui/src/api/users.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { getRequest, postRequest } from '.'; - -export async function login(email: string, password: string) { - return await postRequest('users/login', { email, password }, { withCredentials: true }); -} - -export async function logout() { - return await postRequest('users/logout', {}, { withCredentials: true }); -} - -export async function ping() { - return await getRequest('users/ping', { withCredentials: true }); -} diff --git a/ui/src/components/Topbar/index.tsx b/ui/src/components/Topbar/index.tsx index 7ddcb3e..1cb89fe 100644 --- a/ui/src/components/Topbar/index.tsx +++ b/ui/src/components/Topbar/index.tsx @@ -21,9 +21,16 @@ import { import Cookies from 'js-cookie'; import { useEffect, useState } from 'react'; import { useForm } from '@mantine/form'; -import { login, logout, ping } from '@/api/users'; +import { login, logout, me } from '@/api/auth'; +import { User } from '@/api/auth.types'; -const headerItems = [ +interface HeaderItem { + name: string; + link: string; + role?: string; +} + +const headerItems: HeaderItem[] = [ { name: 'Races', link: '/races' @@ -54,24 +61,38 @@ const headerItems = [ }, { name: 'Management', - link: '/management' + link: '/management', + role: 'admin' } ]; export default function Topbar() { const pathName = usePathname(); const [showLogin, setShowLogin] = useState(false); - const [authenticated, setAuthenticated] = useState(false); - // Check if the auth cookie is set - // If it is, show the user avatar - // If not, show the login button + const [headers, setHeaders] = useState([]); + const [user, setUser] = useState(undefined); useEffect(() => { - console.log('cookies', Cookies.get()); - if (Cookies.get('auth')) { - setAuthenticated(true); + if (Cookies.get('logged_in')) { + me().then((response) => { + if (response?.status == 200) { + setUser(response.data); + } + }); + } else { + setUser(undefined); } }, []); + useEffect(() => { + const h: HeaderItem[] = []; + headerItems.forEach((item) => { + if (item.role == undefined || user?.role == item.role) { + h.push(item); + } + setHeaders(h); + }); + }, [user]); + return ( <> - + ); } @@ -120,20 +140,29 @@ export default function Topbar() { function LoginModal({ showLogin, setShowLogin, - setAuthenticated + setUser }: { showLogin: boolean; setShowLogin: (show: boolean) => void; - setAuthenticated: (authenticated: boolean) => void; + setUser: (user: User) => void; }) { const form = useForm({ initialValues: { email: '', - password: '' + password: '', + remember: false } }); + + function onClose() { + setShowLogin(false); + if (!form.values.remember) { + form.reset(); + } + } + return ( - setShowLogin(false)} withCloseButton={false}> + Welcome back! @@ -148,8 +177,12 @@ function LoginModal({ onSubmit={form.onSubmit(async (values) => { const response = await login(values.email, values.password); if (response?.status == 200) { - setShowLogin(false); - setAuthenticated(true); + me().then((response) => { + if (response?.status == 200) { + setUser(response.data); + } + onClose(); + }); } })} > @@ -162,7 +195,7 @@ function LoginModal({ {...form.getInputProps('password')} /> - + Forgot password?