Enable foreign profile view

This commit is contained in:
Chord 2019-12-30 13:20:50 -05:00
parent 20946fdb43
commit cb918033a6
14 changed files with 248 additions and 125 deletions

View file

@ -47,7 +47,7 @@ api.post('/user/:user/add_gm', async (req, res, next) => {
const account = req.user; const account = req.user;
try { try {
await db.update_account(account.id, {"gm" : true}) await db.update_account(account.id, {[db.ACCOUNT.ADMIN] : true})
res.status(200).json({}); res.status(200).json({});
} catch(e) { } catch(e) {
console.log(e); console.log(e);
@ -59,7 +59,7 @@ api.post('/user/:user/remove_gm', async (req, res, next) => {
const account = req.user; const account = req.user;
try { try {
await db.update_account(account.id, {"gm" : false}) await db.update_account(account.id, {[db.ACCOUNT.ADMIN] : false})
res.status(200).json({}); res.status(200).json({});
} catch(e) { } catch(e) {
console.log(e); console.log(e);
@ -72,7 +72,7 @@ api.post('/user/:user/ban', async (req, res, next) => {
try { try {
// also drop GM if they had it... // also drop GM if they had it...
await db.update_account(account.id, {"inactive" : true, "gm" : false}) await db.update_account(account.id, {[db.ACCOUNT.BANNED] : true, [db.ACCOUNT.ADMIN] : false})
res.status(200).json({}); res.status(200).json({});
} catch(e) { } catch(e) {
console.log(e); console.log(e);
@ -84,10 +84,9 @@ api.post('/user/:user/unban', async (req, res, next) => {
const account = req.user; const account = req.user;
try { try {
await db.update_account(account.id, {"inactive" : false}) await db.update_account(account.id, {[db.ACCOUNT.BANNED] : false})
res.status(200).json({}); res.status(200).json({});
} catch(e) { } catch(e) {
console.log(e);
res.status(500).json({ message: 'error' }); res.status(500).json({ message: 'error' });
} }

View file

@ -56,13 +56,59 @@ export const CHARACTER = Object.freeze({
DELETED: Symbol("deleted"), DELETED: Symbol("deleted"),
}); });
export const LOGIN = Object.freeze({
THIS: Symbol("logins"),
ID: Symbol("id"),
ACCOUNT_ID: Symbol("account_id"),
});
function to_sql(symbol) { function to_sql(symbol) {
assert(typeof symbol == 'symbol') assert(typeof symbol == 'symbol')
return String(symbol).slice(7,-1); return String(symbol).slice(7,-1);
} }
async function get_row_count(table) { function to_sql_kv(fields, idx=1) {
const resp = await pool.query(`SELECT COUNT(*) FROM ${to_sql(table)}`); let SQL = [];
let values = [];
// This will ONLY get Symbols in the field dict
assert(Object.getOwnPropertySymbols(fields).length > 0, "to_sql_kv must have at least one field")
Object.getOwnPropertySymbols(fields).forEach(key => {
assert(typeof key == 'symbol')
SQL.push(to_sql(key)+"=$"+idx++);
values.push(fields[key]);
});
return {
sql: SQL,
next_idx: idx,
values: values,
}
}
function build_SET(fields, idx=1) {
const kv = to_sql_kv(fields, idx);
kv.sql = Symbol(kv.sql.join(", "));
return kv;
}
function build_WHERE(fields, idx=1) {
const kv = to_sql_kv(fields, idx);
kv.sql = Symbol(kv.sql.join(" AND "));
return kv;
}
async function get_row_count(table, filter=undefined) {
let resp;
if (filter) {
const where = build_WHERE(filter);
resp = await pool.query(`SELECT COUNT(*) FROM ${to_sql(table)} WHERE ${to_sql(where.sql)}`,
where.values);
} else {
resp = await pool.query(`SELECT COUNT(*) FROM ${to_sql(table)}`);
}
return parseInt(resp.rows[0].count); return parseInt(resp.rows[0].count);
} }
@ -70,6 +116,13 @@ export async function connect_to_db() {
pool = new pg.Pool() pool = new pg.Pool()
try { try {
const res = await pool.query('SELECT NOW()') const res = await pool.query('SELECT NOW()')
// Quick hack for query debugging (throws exception)
const _query = pool.query;
pool.query_log = (q, v) => {
console.log("QUERY LOG: ", q, v);
return _query(q, v);
}
console.log(`Connected to the psql database at ${process.env.PGHOST}`) console.log(`Connected to the psql database at ${process.env.PGHOST}`)
} catch (e) { } catch (e) {
console.log("Unable to connect to the database: " + e.message); console.log("Unable to connect to the database: " + e.message);
@ -80,9 +133,14 @@ export async function connect_to_db() {
export async function get_account_by_id(id) { export async function get_account_by_id(id) {
try { try {
const account = await pool.query('SELECT * FROM accounts WHERE id=$1', [id]); const account = await pool.query('SELECT * FROM accounts WHERE id=$1', [id]);
const account_obj = account.rows[0];
if (account.rows.length == 0) {
return undefined;
}
const account_obj = account.rows[0];
delete account_obj.passhash; delete account_obj.passhash;
return account_obj; return account_obj;
} catch (e) { } catch (e) {
throw e; throw e;
@ -222,29 +280,16 @@ export async function create_account(username, password) {
} }
} }
function build_set(fields, idx=1) {
let SQL = []
let values = []
// TODO: sort for consistency
Object.keys(fields).forEach(key => {
SQL.push(key+"=$"+idx++)
values.push(fields[key])
});
return [SQL.join(", "), idx, values]
}
export async function update_account(account_id, fields) { export async function update_account(account_id, fields) {
if (fields === {}) { if (fields === {}) {
return return
} }
const set = build_set(fields); const set = build_SET(fields);
set[2].push(account_id) set.values.push(account_id)
try { try {
const update_result = await pool.query('UPDATE accounts SET ' + set[0] + ' WHERE id=$'+set[1],set[2]); const update_result = await pool.query(`UPDATE accounts SET ${to_sql(set.sql)} WHERE id=$${set.next_idx}`, set.values);
return update_result.rowCount; return update_result.rowCount;
} catch (e) { } catch (e) {
if (e.code) if (e.code)
@ -287,9 +332,10 @@ export async function get_account_logins(account_id, pagination) {
const values = [account_id, start_id, pagination.items_per_page]; const values = [account_id, start_id, pagination.items_per_page];
try { try {
const logins = await pool.query('SELECT * FROM logins WHERE account_id=$1 ORDER by login_time DESC ' + const login_count = await get_row_count(LOGIN.THIS, { [LOGIN.ACCOUNT_ID] : account_id });
` OFFSET $2 LIMIT $3`, values); const logins = await pool.query('SELECT * FROM logins WHERE account_id=$1 ORDER by login_time DESC ' + ` OFFSET $2 LIMIT $3`, values);
pagination.item_count = 100;
pagination.item_count = login_count;
pagination.page_count = Math.ceil(pagination.item_count / pagination.items_per_page); pagination.page_count = Math.ceil(pagination.item_count / pagination.items_per_page);
return logins.rows; return logins.rows;

View file

@ -20,13 +20,6 @@ if (process.env.NODE_ENV !== "production") {
async function sessionRequired(req, res, next) { async function sessionRequired(req, res, next) {
if (!req.session || !req.session.account_id) { if (!req.session || !req.session.account_id) {
res.status(403).json({message: 'session required'}) res.status(403).json({message: 'session required'})
} else {
next();
}
}
async function adminRequired(req, res, next) {
if (!req.session || !req.session.account_id) {
res.status(403).json({message: 'admin required'})
} else { } else {
try { try {
const account = await db.get_account_by_id(req.session.account_id); const account = await db.get_account_by_id(req.session.account_id);
@ -35,11 +28,8 @@ async function adminRequired(req, res, next) {
console.log("ERROR: failed to lookup account from session!") console.log("ERROR: failed to lookup account from session!")
res.status(500).json({message: 'error'}); res.status(500).json({message: 'error'});
} else { } else {
if (account.gm === true && account.inactive === false) { req.session_account = account;
next(); next();
} else {
res.status(403).json({message : 'admin required'})
}
} }
} catch (e) { } catch (e) {
console.log(e) console.log(e)
@ -47,6 +37,18 @@ async function adminRequired(req, res, next) {
} }
} }
} }
async function adminRequired(req, res, next) {
if (!req.session_account) {
console.log("ERROR: sessionRequired needs to be called before adminRequired")
res.status(500).json({message: ''})
} else {
if (req.session_account.gm === true && req.session_account.inactive === false) {
next();
} else {
res.status(403).json({message : 'admin required'})
}
}
}
api.use(bodyParser.json()); api.use(bodyParser.json());
api.use(bodyParser.urlencoded({ extended: true })); api.use(bodyParser.urlencoded({ extended: true }));
@ -54,7 +56,7 @@ api.use(bodyParser.urlencoded({ extended: true }));
api.use(api_auth) api.use(api_auth)
api.use(api_info) api.use(api_info)
api.use(sessionRequired, api_user) api.use(sessionRequired, api_user)
api.use(adminRequired, api_admin) api.use(sessionRequired, adminRequired, api_admin)
api.post("/bad_route", async (req, res, next) => { api.post("/bad_route", async (req, res, next) => {
console.log("BAD APP ROUTE:", req.body.route) console.log("BAD APP ROUTE:", req.body.route)

View file

@ -16,10 +16,17 @@ api.get('/user', async (req, res, next) => {
} }
}); });
api.get('/user/profile', async (req, res, next) => { api.get('/user/:user/profile', async (req, res, next) => {
const target_account = req.user;
if (target_account.id !== req.session.account_id && !req.session_account.gm) {
res.status(403).json({ message: 'not allowed to see for other users' });
return;
}
try { try {
const account = await db.get_account_by_id(req.session.account_id); const account = await db.get_account_by_id(target_account.id);
const characters = await db.get_characters_by_account(req.session.account_id); const characters = await db.get_characters_by_account(target_account.id);
res.status(200).json({ res.status(200).json({
id : account.id, id : account.id,
@ -36,19 +43,17 @@ api.get('/user/profile', async (req, res, next) => {
} }
}); });
api.get('/user/:user/logins', async (req, res, next) => { api.get('/user/:user/logins', async (req, res, next) => {
const account = req.user; const account = req.user;
const pagination = get_pagination(req); const pagination = get_pagination(req);
if (account.id !== req.session.account_id) { if (account.id !== req.session.account_id && !req.session_account.gm) {
res.status(403).json({ message: 'not allowed to see for other users' }); res.status(403).json({ message: 'not allowed to see for other users' });
return; return;
} }
try { try {
const logins = await db.get_account_logins(account.id, pagination) const logins = await db.get_account_logins(account.id, pagination)
console.log(logins)
res.status(200).json({ logins : logins, page: pagination}); res.status(200).json({ logins : logins, page: pagination});
} catch (e) { } catch (e) {
console.log(e) console.log(e)

View file

@ -16,6 +16,13 @@ export function get_pagination(req) {
} }
export async function fetch_user_middleware(req, res, next, id) { export async function fetch_user_middleware(req, res, next, id) {
id = parseInt(id);
if (id <= 0) {
res.status(500).json({message: 'error'});
return;
}
try { try {
const account = await db.get_account_by_id(id); const account = await db.get_account_by_id(id);

View file

@ -14,11 +14,10 @@
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import page from 'page'; import page from 'page';
import { fade } from 'svelte/transition';
import { get_initial_state } from './UserState.js'; import { get_initial_state } from './UserState.js';
import Nav from './components/Nav.svelte' import Nav from './components/Nav.svelte'
import Alert from './components/Alert.svelte' import Alert from './components/Alert.svelte'
import Home from './views/Home.svelte'; import Home from './views/Home.svelte';
import Login from './views/Login.svelte'; import Login from './views/Login.svelte';
import Register from './views/Register.svelte'; import Register from './views/Register.svelte';
@ -28,12 +27,12 @@ import UserList from './views/UserList.svelte';
import AdminPanel from './views/AdminPanel.svelte'; import AdminPanel from './views/AdminPanel.svelte';
import CharacterList from './views/CharacterList.svelte'; import CharacterList from './views/CharacterList.svelte';
// prevent pop-in // prevent view pop-in
let initialized = false; let initialized = false;
onMount(async () => { onMount(async () => {
await get_initial_state() await get_initial_state()
initialized = true; page() // start the router
}); });
let route; let route;
@ -53,9 +52,18 @@ function setRoute(r, initialState) {
if (!first) if (!first)
previousCtx = currentCtx previousCtx = currentCtx
if (!first && currentCtx.route == r) if (!first && ctx.pathname == previousCtx.pageCtx.pathname)
return return
if (process.env.NODE_ENV !== "production" && !first) {
console.log("-------------------NEW ROUTE--------------\n",
previousCtx.pageCtx.pathname, " -> ", ctx.pathname)
if (previousCtx && r == previousCtx.route)
console.log("/!\\ Re-rendering same view with different params")
}
// We are changing views, clear the global app message
if (appAlert) if (appAlert)
appAlert.message(""); appAlert.message("");
@ -64,27 +72,39 @@ function setRoute(r, initialState) {
else else
initialized = true; initialized = true;
// Change the component context
currentCtx = { currentCtx = {
route : r, route : r,
routeParams : ctx.params, routeParams : ctx.params,
pageCtx : ctx, pageCtx : ctx,
} }
/* If the previous view compoent was the same, we need to
force svelte to rerender it if some of the parameters have changed.
For example, clicking from another User's view to ourself wont lead
to a render due to the component not changing. We force this change
by scheduling the two changes on separate ticks.
*/
if (previousCtx && r == previousCtx.route) {
setImmediate(() => currentCtx.route = null)
setImmediate(() => currentCtx.route = r)
}
}; };
} }
page("/", setRoute(Home, true)); page("/", setRoute(Home, true));
page("/login", setRoute(Login, true)); page("/login", setRoute(Login, true));
page("/register", setRoute(Register)); page("/register", setRoute(Register));
page("/register", setRoute(Register)); page("/admin", setRoute(AdminPanel));
page("/profile", setRoute(Profile, true));
page("/user/:id", setRoute(Profile, true));
//page("/users", setRoute(UserList)); //page("/users", setRoute(UserList));
//page("/characters", setRoute(CharacterList)); //page("/characters", setRoute(CharacterList));
page("/admin", setRoute(AdminPanel));
//page("/recovery", setRoute(Recovery)); //page("/recovery", setRoute(Recovery));
page("/profile", setRoute(Profile, true));
page("*", setRoute(BadRoute)); page("*", setRoute(BadRoute));
page()
</script> </script>
{#if currentCtx}
<Nav bind:route={currentCtx.pageCtx.pathname}/> <Nav bind:route={currentCtx.pageCtx.pathname}/>
<main role="main" class="container"> <main role="main" class="container">
@ -104,3 +124,4 @@ All other trademarks or tradenames are properties of their respective owners.
</span> </span>
</div> </div>
</footer> </footer>
{/if}

View file

@ -24,9 +24,11 @@ export async function logout() {
page("/") page("/")
} }
loggedIn.subscribe((v) => { if (process.env.NODE_ENV !== "production") {
console.log(loggedIn, v) loggedIn.subscribe((v) => {
}) console.log("Login state: ", v)
})
}
export async function get_initial_state() { export async function get_initial_state() {
try { try {
@ -42,7 +44,6 @@ export async function get_initial_state() {
if (e.response.status === 403) { if (e.response.status === 403) {
console.log("User not logged in / not admin!") console.log("User not logged in / not admin!")
clear_user_state(); clear_user_state();
return false;
} else { } else {
console.log("Unknown login error", e) console.log("Unknown login error", e)
} }

View file

@ -13,7 +13,7 @@
{/if} {/if}
{#if $isAdmin} {#if $isAdmin}
<a href="/character/{character.id}">{character.name}</a> <a href="/user/{character.account_id}">{character.name}</a>
{:else} {:else}
<span>{character.name}</span> <span>{character.name}</span>
{/if} {/if}

View file

@ -4,6 +4,7 @@
import { cubicOut } from 'svelte/easing'; import { cubicOut } from 'svelte/easing';
const progress = tweened(0, { const progress = tweened(0, {
delay: 100,
duration: 1000, duration: 1000,
easing: cubicOut easing: cubicOut
}); });
@ -16,21 +17,22 @@
let tr, nc, vs; let tr, nc, vs;
onMount(() => { onMount(() => {
setTimeout(() => progress.set(1.0), 100);
tr.style.height = "1px"; tr.style.height = "1px";
nc.style.height = "1px"; nc.style.height = "1px";
vs.style.height = "1px"; vs.style.height = "1px";
progress.subscribe((v) => {
if (!tr || !nc || !vs)
return;
tr.style.height = v*percentages.TR*200 + "px";
nc.style.height = v*percentages.NC*200 + "px";
vs.style.height = v*percentages.VS*200 + "px";
})
setTimeout(() => progress.set(1.0), 100);
}) })
progress.subscribe((v) => {
if (tr === undefined || !tr.style)
return
tr.style.height = v*percentages.TR*200 + "px";
nc.style.height = v*percentages.NC*200 + "px";
vs.style.height = v*percentages.VS*200 + "px";
})
</script> </script>
<style> <style>

View file

@ -21,15 +21,26 @@
</script> </script>
<PaginatedList {fetch} let:data={logins} let:pagination={pagination}> <PaginatedList {fetch} let:data={logins} let:pagination={pagination}>
<p slot="header">
{#if pagination.item_count}
Login data
{:else}
No logins yet
{/if}
</p>
<table slot="body" class="table table-dark table-responsive"> <table slot="body" class="table table-dark table-responsive">
<thead> <thead>
<td>Login Time</td> <td>From</td>
<td>Login Date</td>
</thead> </thead>
<tbody> <tbody>
{#each logins as login, i} {#each logins as login, i}
<tr> <tr>
<td>{moment(login.login_time).fromNow()}</td> <td>
<code>{login.hostname} - {login.ip_address}</code>
</td>
<td>{moment(login.login_time).format('MMMM Do YYYY, h:mm:ss a')} ({moment(login.login_time).fromNow()})</td>
</tr> </tr>
{/each} {/each}
</tbody> </tbody>

View file

@ -2,8 +2,6 @@
import { get_initial_state, logout, isAdmin, loggedIn, username } from '../UserState.js'; import { get_initial_state, logout, isAdmin, loggedIn, username } from '../UserState.js';
import axios from 'axios' import axios from 'axios'
export let route; export let route;
export let pageCtx;
console.log(pageCtx)
</script> </script>
<nav class="navbar navbar-expand-lg navbar-dark bg-dark justify-content-between"> <nav class="navbar navbar-expand-lg navbar-dark bg-dark justify-content-between">

View file

@ -3,6 +3,7 @@
import axios from 'axios' import axios from 'axios'
import Pagination from '../components/Pagination' import Pagination from '../components/Pagination'
export let setURLParam = false;
export let fetch; export let fetch;
let data; let data;
@ -15,12 +16,16 @@
onMount(async () => { onMount(async () => {
const url = new URL(window.location.href) const url = new URL(window.location.href)
let page = url.searchParams.get('page') let initialPage = 1;
if (page == undefined) if (setURLParam) {
page = 1; let param = parseInt(url.searchParams.get('page'))
await list_fetch(page); if (param != NaN)
initialPage = param;
}
await list_fetch(initialPage);
}) })
async function pageChange(page) { async function pageChange(page) {
@ -50,8 +55,10 @@
{#if data} {#if data}
<slot name="header" data={data} pagination={pagination}></slot> <slot name="header" data={data} pagination={pagination}></slot>
<Pagination {pagination} {pageChange} /> {#if pagination.item_count > 0}
<Pagination {pagination} {pageChange} {setURLParam} />
<slot name="body" data={data} pagination={pagination}></slot> <slot name="body" data={data} pagination={pagination}></slot>
<Pagination {pagination} {pageChange} /> <Pagination {pagination} {pageChange} {setURLParam} />
{/if}
<slot name="footer" data={data} pagination={pagination}></slot> <slot name="footer" data={data} pagination={pagination}></slot>
{/if} {/if}

View file

@ -1,21 +1,31 @@
<script> <script>
export let pagination; export let pagination;
export let pageChange; export let pageChange;
let numPages = 10; export let setURLParam = false;
export let displayPages = 10;
let pages = [] let pages = []
function pageClick(event) {
const page = event.target.getAttribute('data-page');
pageChange(parseInt(page))
if (!setURLParam)
event.preventDefault()
}
$ : { $ : {
const new_pages = []; const new_pages = [];
let pi = 0, i; let pi = 0, i;
let pg = pagination; let pg = pagination;
const pageChunk = Math.max(Math.ceil(numPages/3), 1); const pageChunk = Math.max(Math.ceil(displayPages/3), 1);
const middleChunk = Math.max(Math.ceil(pageChunk/2), 1); const middleChunk = Math.max(Math.ceil(pageChunk/2), 1);
const leftBound = Math.min(pageChunk+1, pagination.page_count); const leftBound = Math.min(pageChunk+1, pagination.page_count);
const rightBound = Math.max(pagination.page_count-pageChunk, 1); const rightBound = Math.max(pagination.page_count-pageChunk, 1);
// fast path: draw all pages // fast path: draw all pages
if (pg.page_count <= numPages || rightBound <= leftBound) { if (pg.page_count <= displayPages || rightBound <= leftBound) {
for (i = 1; i <= pg.page_count; i++) for (i = 1; i <= pg.page_count; i++)
new_pages[pi++] = i; new_pages[pi++] = i;
} else { } else {
@ -54,27 +64,31 @@
<p>Displaying {(pagination.page-1)*pagination.items_per_page+1} &mdash; {Math.min(pagination.page*pagination.items_per_page, pagination.item_count)}</p> <p>Displaying {(pagination.page-1)*pagination.items_per_page+1} &mdash; {Math.min(pagination.page*pagination.items_per_page, pagination.item_count)}</p>
<nav aria-label="Page navigation"> <nav aria-label="Page navigation">
<ul class="pagination pagination-sm"> <ul class="pagination pagination-sm">
<li class="page-item" class:disabled={pagination.page<=1}> <li class="page-item" class:disabled={pagination.page<=1}>
<a class="page-link" href={"?page="+(pagination.page-1)} <a class="page-link" href={"?page="+(pagination.page-1)}
on:click={(e) => pageChange(pagination.page-1)} on:click={pageClick}
aria-label="Previous"> data-page={pagination.page-1}
<span aria-hidden="true">&laquo;</span> aria-label="Previous">
</a> &laquo;
</li> </a>
{#each pages as page,i} </li>
{#each pages as page,i}
{#if page == -1} {#if page == -1}
<li class="page-item page-last-separator disabled" ><a class="page-link">...<a></li> <li class="page-item page-last-separator disabled"><span class="page-link">...</span></li>
{:else} {:else}
<li class="page-item" class:active={page==pagination.page}><a on:click={(e) => pageChange(page)} href={"?page="+page} class="page-link">{page}</a></li> <li class="page-item" class:active={page==pagination.page}>
<a on:click={pageClick} href={"?page="+page} data-page={page} class="page-link">{page}</a>
</li>
{/if} {/if}
{/each} {/each}
<li class="page-item" class:disabled={pagination.page>=pagination.page_count}> <li class="page-item" class:disabled={pagination.page>=pagination.page_count}>
<a class="page-link" href={"?page="+(pagination.page+1)} <a class="page-link" href={"?page="+(pagination.page+1)}
on:click={(e) => pageChange(pagination.page+1)} data-page={pagination.page+1}
aria-label="Next"> on:click={pageClick}
<span aria-hidden="true">&raquo;</span> aria-label="Next">
</a> &raquo;
</li> </a>
</ul> </li>
</ul>
</nav> </nav>

View file

@ -4,9 +4,15 @@
import page from 'page' import page from 'page'
import moment from 'moment' import moment from 'moment'
import CharacterLink from '../components/CharacterLink' import CharacterLink from '../components/CharacterLink'
import { userId } from '../UserState'
import LoginList from '../components/LoginList' import LoginList from '../components/LoginList'
import AccountLink from '../components/AccountLink'
export let pageCtx;
export let appAlert;
export let params;
export let ready; export let ready;
ready = false; ready = false;
let username; let username;
@ -17,8 +23,10 @@
let account; let account;
onMount(async () => { onMount(async () => {
let loadID = params.id || $userId;
try { try {
const resp = await axios.get("/api/user/profile") const resp = await axios.get("/api/user/"+loadID+"/profile")
account = resp.data; account = resp.data;
username = resp.data.name; username = resp.data.name;
characters = resp.data.characters; characters = resp.data.characters;
@ -30,7 +38,13 @@
ready = true ready = true
} catch (e) { } catch (e) {
if (e.response && e.response.status == 403) { if (e.response && e.response.status == 403) {
page("/login?redirect=/profile") if (e.response.data.message == "session required") {
page("/login?redirect="+pageCtx.pathname)
} else {
appAlert.message(e.response.data.message)
}
} else {
appAlert.message(e.message)
} }
} }
}); });
@ -40,17 +54,9 @@
<title>PSForever - Profile</title> <title>PSForever - Profile</title>
</svelte:head> </svelte:head>
<h1>Your Account</h1> {#if account}
<h1>Account: <AccountLink account={account}/></h1>
<form> <form>
{#if isAdmin}
<strong class="color-red">You are a GM.</strong>
{/if}
<div class="form-group row">
<label for="staticUsername" class="col-sm-2 col-form-label">Username</label>
<div class="col-sm-10">
<input type="text" readonly class="form-control-plaintext" id="staticUsername" bind:value={username}>
</div>
</div>
<div class="form-group row"> <div class="form-group row">
<label for="staticEmail" class="col-sm-2 col-form-label">Email</label> <label for="staticEmail" class="col-sm-2 col-form-label">Email</label>
<div class="col-sm-10"> <div class="col-sm-10">
@ -66,17 +72,21 @@
</form> </form>
<h2>Characters</h2> <h2>Characters</h2>
{#if characters.length > 1} <p>
<div class="row"> {#if characters.length > 1}
{#each characters as char, i} <div class="row">
<div class="col-md-4 col-12"><CharacterLink character={char} /></div> {#each characters as char, i}
{/each} <div class="col-md-4 col-12"><CharacterLink character={char} /></div>
</div> {/each}
{:else} </div>
<p>You have no characters</p> {:else}
{/if} You have no characters
{/if}
</p>
<h2>Logins</h2> <h2>Logins</h2>
{#if account} <p>
<LoginList account_id={account.id} /> <LoginList account_id={account.id} />
</p>
{/if} {/if}