Skip to content

Commit

Permalink
Introduce SafeSession, Session Deauthorizing, and Browser Detection
Browse files Browse the repository at this point in the history
  • Loading branch information
lucemans committed Jul 30, 2024
1 parent f82783a commit dfbef84
Show file tree
Hide file tree
Showing 12 changed files with 234 additions and 38 deletions.
29 changes: 28 additions & 1 deletion engine/src/auth/session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,17 @@ use uuid::Uuid;

use crate::database::Database;

#[derive(sqlx::FromRow, Debug, Clone, Serialize, Deserialize, Object)]
#[derive(Debug, Clone, Serialize, Deserialize, Object)]
pub struct SafeSession {
pub id: String,
pub user_id: i32,
pub user_agent: String,
pub user_ip: IpAddr,
pub last_access: chrono::DateTime<chrono::Utc>,
pub valid: bool,
}

#[derive(sqlx::FromRow, Debug, Clone, Serialize, Deserialize)]
pub struct SessionState {
pub id: Uuid,
pub user_id: i32,
Expand Down Expand Up @@ -82,3 +92,20 @@ impl SessionState {
Ok(sessions)
}
}

impl Into<SafeSession> for SessionState {
fn into(self) -> SafeSession {
let id = self.id.to_string();
let id = id[0..6].to_string() + &id[30..];

SafeSession {
// Strip uuid to be abc...xyz
id,
user_id: self.user_id,
user_agent: self.user_agent,
user_ip: self.user_ip,
last_access: self.last_access,
valid: self.valid,
}
}
}
17 changes: 12 additions & 5 deletions engine/src/routes/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@ use poem_openapi::{param::Query, payload::PlainText, OpenApi, OpenApiService};
use reqwest::StatusCode;

use crate::{
auth::{middleware::AuthToken, session::SessionState},
auth::{
middleware::AuthToken,
session::{SafeSession, SessionState},
},
models::{media::Media, product::Product, property::Property},
state::AppState,
};
Expand Down Expand Up @@ -88,12 +91,16 @@ impl Api {
&self,
auth: AuthToken,
state: Data<&Arc<AppState>>,
) -> poem_openapi::payload::Json<Vec<SessionState>> {
) -> poem_openapi::payload::Json<Vec<SafeSession>> {
match auth {
AuthToken::Active(active_user) => {
let sessions = SessionState::get_by_user_id(active_user.session.user_id, &state.database)
.await
.unwrap();
let sessions =
SessionState::get_by_user_id(active_user.session.user_id, &state.database)
.await
.unwrap()
.into_iter()
.map(|x| x.into())
.collect();

poem_openapi::payload::Json(sessions)
}
Expand Down
4 changes: 3 additions & 1 deletion web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"test": "echo \"Error: no test specified\" && exit 1",
"dev": "vite",
"build": "vite build",
"lint": "eslint -c .eslintrc.json --ext .ts ./src"
"lint": "eslint -c .eslintrc.json"
},
"keywords": [],
"author": "",
Expand All @@ -25,6 +25,7 @@
"react-dom": "^18.3.1",
"swr": "^2.2.5",
"tailwindcss": "^3.4.3",
"ua-parser-js": "^1.0.38",
"vite": "^5.2.10",
"zustand": "^4.5.4"
},
Expand All @@ -33,6 +34,7 @@
"@tanstack/router-plugin": "^1.45.13",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@types/ua-parser-js": "^0.7.39",
"@types/url-search-params": "^1.1.2",
"@typescript-eslint/parser": "^7.17.0",
"eslint": "^8.57.0",
Expand Down
14 changes: 14 additions & 0 deletions web/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 10 additions & 1 deletion web/src/api/sessions.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
import { useHttp } from './core';

export const useSessions = () => useHttp<any>('/api/sessions');
type SessionResponse = {
id: string;
user_id: number;
user_agent: string;
user_ip: string;
last_access: string;
// valid: boolean;
};

export const useSessions = () => useHttp<SessionResponse[]>('/api/sessions');
70 changes: 70 additions & 0 deletions web/src/components/ActiveSessionsTable.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { FC } from 'react';
import { UAParser } from 'ua-parser-js';

import { useSessions } from '../api/sessions';
import { getRelativeTimeString } from '../util/date';

export const ActiveSessionsTable: FC = () => {
const { data: sessions } = useSessions();

return (
<div className="p-2 space-y-2">
<h2 className="font-bold">Active Sessions</h2>
<hr />
<p>
These are the sessions that are currently active for your
account.
</p>
<div className="space-y-2">
{sessions &&
sessions.map((session) => {
const ua = UAParser(session.user_agent);
const time = new Date(session.last_access);
const x = getRelativeTimeString(time);

return (
<div
key={session.id}
className="bg-blue-50 p-2 flex justify-between"
>
<div>
<div className="space-x-2">
<b>
{[
ua.browser.name,
ua.browser.version,
]
.filter(Boolean)
.join(' ')}
</b>
<span>on</span>
<b>
{[
ua.os.name,
ua.cpu.architecture,
ua.os.version,
]
.filter(Boolean)
.join(' ')}
</b>
</div>
<div>{session.user_ip}</div>
<div>{x}</div>
<div>#{session.id}</div>
</div>
<div className="flex items-center">
<button className="btn">Deauthorize</button>
</div>
</div>
);
})}
</div>
<p>
If there is a session in here that you do not recognize, you can
deauthorize it.
</p>
<hr />
<button className="btn">Log out everywhere</button>
</div>
);
};
4 changes: 4 additions & 0 deletions web/src/index.css
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

.btn {
@apply bg-white text-neutral-800 border border-neutral-200 px-2.5 py-0.5 h-fit hover:bg-neutral-50 rounded-md focus:outline focus:outline-2 outline-offset-2 outline-blue-500;
}
9 changes: 5 additions & 4 deletions web/src/routes/__root.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { createRootRoute, Link, Outlet } from '@tanstack/react-router'
import { TanStackRouterDevtools } from '@tanstack/router-devtools'
import { Navbar } from '../components/Navbar'
import { createRootRoute, Outlet } from '@tanstack/react-router';
import { TanStackRouterDevtools } from '@tanstack/router-devtools';

import { Navbar } from '../components/Navbar';

export const Route = createRootRoute({
component: () => (
Expand All @@ -10,4 +11,4 @@ export const Route = createRootRoute({
<TanStackRouterDevtools />
</>
),
})
});
11 changes: 5 additions & 6 deletions web/src/routes/about.lazy.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import { createLazyFileRoute } from '@tanstack/react-router'
import { FC } from 'react'

import { createLazyFileRoute } from '@tanstack/react-router';
import { FC } from 'react';

const component: FC = () => {
return <div className="p-2">Hello from About!</div>
}
return <div className="p-2">Hello from About!</div>;
};

export const Route = createLazyFileRoute('/about')({
component,
})
});
11 changes: 6 additions & 5 deletions web/src/routes/index.lazy.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { createLazyFileRoute } from '@tanstack/react-router'
import { createLazyFileRoute } from '@tanstack/react-router';

const component = () => {
return (
<div className="p-2">
<h3>Welcome Home!</h3>
</div>
)
}
);
};

export const Route = createLazyFileRoute('/')({
component
})
component,
});
40 changes: 25 additions & 15 deletions web/src/routes/sessions.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,35 @@
import { createFileRoute } from '@tanstack/react-router';
import { FC } from 'react';

import { useSessions } from '../api/sessions';
import { useAuth } from '../api/auth';
import { ActiveSessionsTable } from '../components/ActiveSessionsTable';

const component: FC = () => {
const { data: sessions } = useSessions();

return (
<div className="p-2">
Hello from About!
<div>
{sessions &&
sessions.map((session: any) => (
<div key={session.id}>
<span>{session.id}</span>
<button>Disconnect</button>
</div>
))}
</div>
<div className="mx-auto w-full max-w-xl p-2">
<ActiveSessionsTable />
</div>
);
};

export const Route = createFileRoute('/sessions')({ component });
export const Route = createFileRoute('/sessions')({
component,
beforeLoad: async ({ location }) => {
if (useAuth.getState().token === null) {
// throw redirect({
// to: 'https://localhost:3000/login' as any,
// search: {
// // Use the current location to power a redirect after login
// // (Do not use `router.state.resolvedLocation` as it can
// // potentially lag behind the actual current location)
// redirect: location.href,
// },
// });

// eslint-disable-next-line no-undef
window.location.href =
'http://localhost:3000/login?redirect=' +
encodeURIComponent(location.href);
}
},
});
52 changes: 52 additions & 0 deletions web/src/util/date.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/**
* Convert a date to a relative time string, such as
* "a minute ago", "in 2 hours", "yesterday", "3 months ago", etc.
* using Intl.RelativeTimeFormat
*/
export function getRelativeTimeString(
date: Date | number,
// eslint-disable-next-line no-undef
lang = navigator.language
): string {
// Allow dates or times to be passed
const timeMs = typeof date === 'number' ? date : date.getTime();

// Get the amount of seconds between the given date and now
const deltaSeconds = Math.round((timeMs - Date.now()) / 1000);

// Array reprsenting one minute, hour, day, week, month, etc in seconds
const cutoffs = [
60,
3600,
86_400,
86_400 * 7,
86_400 * 30,
86_400 * 365,
Number.POSITIVE_INFINITY,
];

// Array equivalent to the above but in the string representation of the units
const units: Intl.RelativeTimeFormatUnit[] = [
'second',
'minute',
'hour',
'day',
'week',
'month',
'year',
];

// Grab the ideal cutoff unit
const unitIndex = cutoffs.findIndex(
(cutoff) => cutoff > Math.abs(deltaSeconds)
);

// Get the divisor to divide from the seconds. E.g. if our unit is "day" our divisor
// is one day in seconds, so we can divide our seconds by this to get the # of days
const divisor = unitIndex ? cutoffs[unitIndex - 1] : 1;

// Intl.RelativeTimeFormat do its magic
const rtf = new Intl.RelativeTimeFormat(lang, { numeric: 'auto' });

return rtf.format(Math.floor(deltaSeconds / divisor), units[unitIndex]);
}

0 comments on commit dfbef84

Please sign in to comment.