Skip to content

Commit

Permalink
implemented profile page, plugin token management
Browse files Browse the repository at this point in the history
  • Loading branch information
dotFionn committed Mar 24, 2024
1 parent 7be1a66 commit 2becf03
Show file tree
Hide file tree
Showing 15 changed files with 310 additions and 29 deletions.
33 changes: 32 additions & 1 deletion src/backend/plugin-token/plugin-token.controller.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import { BadRequestException, Body, Controller, Get, HttpCode, NotFoundException, Param, Post, Res, UnauthorizedException } from '@nestjs/common';
import { BadRequestException, Body, Controller, Delete, Get, HttpCode, NotFoundException, Param, Post, Query, Res, UnauthorizedException, UseGuards } from '@nestjs/common';
import { ApiExcludeController } from '@nestjs/swagger';
import { Response } from 'express';
import { FilterQuery } from 'mongoose';

import PluginToken from '../../shared/interfaces/plugin-token.interface';
import { User } from '../auth/auth.decorator';
import { AuthGuard } from '../auth/auth.guard';
import getAppConfig from '../config';
import logger from '../logger';
import { UserDocument } from '../user/user.model';
Expand Down Expand Up @@ -96,4 +99,32 @@ export class PluginTokenController {
token,
};
}

@Get('')
@UseGuards(AuthGuard)
async getAllTokens(@User() user: UserDocument, @Query('scope') scope = 'own') {
let filter: FilterQuery<PluginToken> = { user: user._id };

if (scope == 'all' && user.admin) {
filter = {};
}

const tokens = await this.pluginTokenService.findTokens(filter);

return tokens;
}

@Delete('/:id')
@UseGuards(AuthGuard)
async revokeToken(@User() user: UserDocument, @Param('id') id: string) {
const token = await this.pluginTokenService.findToken({ user: user._id, _id: id });

if (!token) {
throw new NotFoundException();
}

this.pluginTokenService.deleteTokenById(token._id);

return token;
}
}
4 changes: 2 additions & 2 deletions src/backend/plugin-token/plugin-token.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ export type PluginTokenDocument = HydratedDocument<PluginToken>;
const PluginTokenSchema = new mongoose.Schema<PluginToken>({
user: { type: String },
label: { type: String, default: 'New token' },
pollingSecret: { type: String, required: true },
token: { type: String, required: true },
pollingSecret: { type: String, select: false },
token: { type: String, required: true, select: false },
lastUsed: { type: Date, default: Date.now },
}, { timestamps: true });

Expand Down
21 changes: 20 additions & 1 deletion src/backend/plugin-token/plugin-token.service.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Inject, Injectable } from '@nestjs/common';
import mongoose from 'mongoose';
import mongoose, { FilterQuery } from 'mongoose';

import PluginToken from '../../shared/interfaces/plugin-token.interface';
import { UtilsService } from '../utils/utils.service';

import { PLUGINTOKEN_MODEL, PluginTokenDocument, PluginTokenModel } from './plugin-token.model';
Expand Down Expand Up @@ -80,4 +81,22 @@ export class PluginTokenService {

return count > 0;
}

async findTokens(filter: FilterQuery<PluginToken> = {}): Promise<PluginTokenDocument[]> {
const tokens = await this.pluginTokenModel.find(filter);

return tokens;
}

async findToken(filter: FilterQuery<PluginToken> = {}): Promise<PluginTokenDocument | null> {
const token = await this.pluginTokenModel.findOne(filter);

return token;
}

async deleteTokenById(id: string | mongoose.Types.ObjectId): Promise<PluginTokenDocument | null> {
const token = await this.pluginTokenModel.findOneAndDelete({ _id: id });

return token;
}
}
8 changes: 7 additions & 1 deletion src/frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,18 @@ import Debug from './pages/Debug';
import Delivery from './pages/Delivery';
import FlowManagement from './pages/FlowManagement';
import Landingpage from './pages/Landingpage';
import ProfilePage from './pages/Profile';
import Vdgs from './pages/VdgsNew';
import { button } from './utils/ui/customDesign/button';
import { card } from './utils/ui/customDesign/card';
import { datatable } from './utils/ui/customDesign/datatable';
import { dataview } from './utils/ui/customDesign/dataview';
import { dialog } from './utils/ui/customDesign/dialog';
import { dropdown } from './utils/ui/customDesign/dropdown';
import { global } from './utils/ui/customDesign/global';
import { inputnumber } from './utils/ui/customDesign/inputnumber';
import { inputtext } from './utils/ui/customDesign/inputtext';
import { menu } from './utils/ui/customDesign/menu';
import { selectbutton } from './utils/ui/customDesign/selectbutton';
import { toast } from './utils/ui/customDesign/toast';
import { toolbar } from './utils/ui/customDesign/toolbar';
Expand All @@ -55,6 +58,8 @@ function App() {
inputnumber: inputnumber,
inputtext: inputtext,
selectbutton: selectbutton,
menu: menu,
dataview: dataview,

},
{ mergeSections: true, mergeProps: false },
Expand All @@ -65,7 +70,7 @@ function App() {
<>
<Router>
<DarkModeProvider>
<PrimeReactProvider value={{ unstyled: true, pt: CustomTailwind, ripple: true }}>
<PrimeReactProvider value={{ unstyled: true, pt: CustomTailwind, ripple: false }}>
<AuthProvider>
<Navbar />
<div className="mt-2">
Expand All @@ -85,6 +90,7 @@ function App() {
<Route path="/delivery" element={<Delivery />} />
<Route path="/auth-failure" element={<AuthFailurePage />} />
<Route path='/authorize-plugin/:id' element={<AuthorizePluginPage />} />
<Route path='/profile' element={<ProfilePage />} />
<Route path="/" element={<Landingpage />} />
<Route path="*" element={<Navigate to="/" />} />
</Routes>
Expand Down
17 changes: 3 additions & 14 deletions src/frontend/src/components/Navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import { Link, useNavigate } from 'react-router-dom';
import logo from '../assets/cdm_logo.png';
import AuthContext from '../contexts/AuthProvider';
import DarkModeContext from '../contexts/DarkModeProvider';
import AuthService from '../services/AuthService';

// import { FrontendSettings } from '@/shared/interfaces/config.interface';
import ProfilePicture from './ProfilePicture';
Expand All @@ -29,15 +28,9 @@ export default function NavbarWithDropdown() {
// const [config, setConfig] = useState<FrontendSettings>();
const navigate = useNavigate();
const [items, setItems] = useState<NavItemDefinition[]>([]);
const { auth } = useContext(AuthContext);
const { auth, authenticate, logout } = useContext(AuthContext);
const { darkMode, changeDarkMode } = useContext(DarkModeContext);

async function logout() {
await AuthService.logout();

window.location.reload();
}

const navItems: NavItemDefinition[] = [
{
label: 'Delivery',
Expand Down Expand Up @@ -65,10 +58,6 @@ export default function NavbarWithDropdown() {
},
];

const redirectToVatsimAuth = () => {
window.location.replace('/api/auth');
};

useEffect(() => {
setItems(
navItems.filter((item) =>
Expand Down Expand Up @@ -179,7 +168,7 @@ export default function NavbarWithDropdown() {
</button>

<div className={`${auth.user ? 'hidden' : ''} ml-2`}>
<Button size='small' onClick={() => redirectToVatsimAuth()}>Login</Button>
<Button size='small' onClick={authenticate}>Login</Button>
</div>

{/* Profile dropdown */}
Expand Down Expand Up @@ -217,7 +206,7 @@ export default function NavbarWithDropdown() {
<Menu.Item>
{({ active }) => (
<span
onClick={() => logout()}
onClick={logout}
className={classNames(
active ? 'bg-gray-100' : '',
'block px-4 py-2 text-sm text-gray-700 cursor-pointer',
Expand Down
12 changes: 6 additions & 6 deletions src/frontend/src/components/ProfilePicture.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export interface IProfilePictureProps {
size?: number | void;
}

export default function ProfilePicture({ user, className = 'rounded-full', size = 64 }: IProfilePictureProps) {
export default function ProfilePicture({ user, className = 'rounded-full border-2', size = 64 }: IProfilePictureProps) {
return <svg
xmlns="http://www.w3.org/2000/svg"
xmlnsXlink="http://www.w3.org/1999/xlink"
Expand All @@ -27,12 +27,12 @@ export default function ProfilePicture({ user, className = 'rounded-full', size
x="50%"
y="50%"
style={{ color: '#FFF', lineHeight: 1, fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif" }}
alignment-baseline="middle"
text-anchor="middle"
font-size={size / 2.3}
font-weight="400"
alignmentBaseline="middle"
textAnchor="middle"
fontSize={size / 2.3}
fontWeight="400"
dy=".1em"
dominant-baseline="middle"
dominantBaseline="middle"
fill="#FFFFFF"
>{user ? `${user.firstName.charAt(0)}${user.lastName.charAt(0)}` : '?'}</text>
</svg>;
Expand Down
19 changes: 16 additions & 3 deletions src/frontend/src/contexts/AuthProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,28 +17,41 @@ const AuthContext = createContext<{
user: User | undefined
};
setAuth: Dispatch<SetStateAction<{ user: User | undefined }>>;
}>({ auth: { user: undefined }, setAuth: () => {} });
authenticate: () => void;
logout: () => void;
}>({ auth: { user: undefined }, setAuth() {}, authenticate() {}, logout() {} });

export const AuthProvider = ({ children }: PropsWithChildren) => {
const [auth, setAuth] = useState<{ user: User | undefined }>({ user: undefined });

const navigate = useNavigate();

function authenticate() {
window.location.replace('/api/auth');
}

async function logout() {
await authService.logout();

window.location.reload();
}

useEffect(() => {
authService
.getProfile()
.then((user) => {
setAuth({ user });
})
.catch(() => {
.catch((error) => {
console.error(error);
setAuth({ user: undefined });
navigate('/auth-failure');
});
}, []);

return (
<>
<AuthContext.Provider value={{ auth, setAuth }}>
<AuthContext.Provider value={{ auth, setAuth, authenticate, logout }}>
{children}
</AuthContext.Provider>
</>
Expand Down
129 changes: 129 additions & 0 deletions src/frontend/src/pages/Profile.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import { MinusIcon } from '@heroicons/react/24/outline';
import { Badge } from 'primereact/badge';
import { Button } from 'primereact/button';
import { Card } from 'primereact/card';
import { Menu } from 'primereact/menu';
import { useContext, useEffect, useState } from 'react';

import PluginToken from '../../../shared/interfaces/plugin-token.interface';
import ProfilePicture from '../components/ProfilePicture';
import AuthContext from '../contexts/AuthProvider';
import pluginTokenService from '../services/pluginToken.service';
import time from '../utils/time';

export default function ProfilePage() {
const { auth, logout } = useContext(AuthContext);

const tabs = {
details: {
label: 'Details',
},
pluginTokens: {
label: 'Plugin Tokens',
},
raw: {
label: 'Raw data',
},
};
type TTabId = keyof typeof tabs;

const [activeTab, setActiveTab] = useState<TTabId>(Object.keys(tabs)[0] as TTabId);
const [tokens, setTokens] = useState<PluginToken[]>([]);

const { user } = auth;

function loadTokens() {
pluginTokenService.getOwnTokens().then(setTokens).catch(console.error);
}

useEffect(() => {
const int = setInterval(loadTokens, 60000);
loadTokens();

return () => {
clearInterval(int);
};
}, []);

async function revoke(token: PluginToken) {
const confirmed = confirm(`Confirm you want to revoke the token "${token.label}" (${token._id})`);

if (!confirmed) {
return;
}

await pluginTokenService.revokeToken(token._id);

loadTokens();
}

if (!user) {
return <></>;
}

return (<>
<div className="px-5 w-full">
<div className='flex flex-row items-center my-5'>
<div>
<ProfilePicture user={user} />
</div>
<div className='ms-6'>
<div className="font-bold text-lg">
{user.firstName} {user.lastName}
</div>
<div className="mt-1">{user.cid}</div>
<div className="mt-1">
{user.admin && <Badge value='Admin' className='bg-yellow-300 text-black dark:text-black px-3 me-2' />}
{user.hasAtcRating && <Badge value='ATC' severity='success' className='text-black dark:text-black px-3 me-2' />}
{user.banned && <Badge value='Banned' severity='danger' className='px-3 me-2' />}
</div>
</div>
<div className="ms-auto">
<Button type='button' onClick={logout} severity="danger" outlined>Sign out</Button>
</div>
</div>

<div className="flex flex-row w-full">
<Menu className='me-2 min-w-fit w-fit' model={Object.entries(tabs).map(([k, { ...data }]) => ({ ...data, command: () => setActiveTab(k as TTabId), className: activeTab == k ? 'bg-zinc-700' : undefined }))} />
<Card title={tabs[activeTab].label} pt={{ root: { className: 'w-full' } }}>
{activeTab == 'details' && <div>
<table>
<tbody>
{Object.entries({
'First name': user.firstName,
'Last name': user.lastName,
'VATSIM CID': user.cid,
'vACDM User ID': user._id,
'First Seen': user.createdAt,
'Last updated': user.updatedAt,
}).map(([label, value]) => <tr key={label}><th className='text-left pe-4' scope='row'>{label}</th><td>{value}</td></tr>)}
</tbody>
</table>
</div>}
{activeTab == 'pluginTokens' && <div>
{!tokens.length ? <>
You do not have any plugin tokens. Start using the vACDM plugin to have the plugin generate one!
</> : tokens.map(token => (
<div key={token._id} className="flex flex-row justify-between pt-1 pb-2 px-2 mx-0 rounded-lg hover:bg-zinc-600">
<div className="flex flex-col">
<div className="text-xl font-bold">{token.label}</div>
<table>
<tbody>
{Object.entries({
'ID': token._id,
'Last seen': time.formatDateTime(token.lastUsed),
'Created at': time.formatDateTime(token.createdAt),
}).map(([label, value]) => <tr key={label}><th className='text-left pe-4 text-sm' scope='row'>{label}</th><td className='text-sm'>{value}</td></tr>)}
</tbody>
</table>
</div>
<div><Button severity='danger' outlined onClick={() => revoke(token)}><MinusIcon className='w-6' /></Button></div>
</div>
))}
</div>}
{activeTab == 'raw' && <pre>{JSON.stringify({ user }, undefined, 2)}</pre>}
</Card>
</div>
</div>
</>);
}
2 changes: 1 addition & 1 deletion src/frontend/src/services/AuthService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export async function getConfig() {

export async function logout() {
try {
await axios.get('/api/auth/logout', {
await axios.post('/api/auth/logout', {
withCredentials: true,
});

Expand Down
Loading

0 comments on commit 2becf03

Please sign in to comment.