Compare commits
2 Commits
fcf8f7e3ce
...
e98bd1f647
Author | SHA1 | Date |
---|---|---|
|
e98bd1f647 | |
|
cb9b6535c1 |
|
@ -1400,12 +1400,6 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/web-bluetooth": {
|
||||
"version": "0.0.20",
|
||||
"resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz",
|
||||
"integrity": "sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/yauzl": {
|
||||
"version": "2.10.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz",
|
||||
|
@ -2035,56 +2029,6 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@vueuse/core": {
|
||||
"version": "10.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@vueuse/core/-/core-10.11.1.tgz",
|
||||
"integrity": "sha512-guoy26JQktXPcz+0n3GukWIy/JDNKti9v6VEMu6kV2sYBsWuGiTU8OWdg+ADfUbHg3/3DlqySDe7JmdHrktiww==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/web-bluetooth": "^0.0.20",
|
||||
"@vueuse/metadata": "10.11.1",
|
||||
"@vueuse/shared": "10.11.1",
|
||||
"vue-demi": ">=0.14.8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
}
|
||||
},
|
||||
"node_modules/@vueuse/core/node_modules/vue-demi": {
|
||||
"version": "0.14.10",
|
||||
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz",
|
||||
"integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"vue-demi-fix": "bin/vue-demi-fix.js",
|
||||
"vue-demi-switch": "bin/vue-demi-switch.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@vue/composition-api": "^1.0.0-rc.1",
|
||||
"vue": "^3.0.0-0 || ^2.6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@vue/composition-api": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@vueuse/metadata": {
|
||||
"version": "10.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-10.11.1.tgz",
|
||||
"integrity": "sha512-IGa5FXd003Ug1qAZmyE8wF3sJ81xGLSqTqtQ6jaVfkeZ4i5kS2mwQF61yhVqojRnenVew5PldLyRgvdl4YYuSw==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
}
|
||||
},
|
||||
"node_modules/@vueuse/shared": {
|
||||
"version": "10.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-10.11.1.tgz",
|
||||
|
@ -6399,6 +6343,62 @@
|
|||
"vue": ">= 3.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/radix-vue/node_modules/@types/web-bluetooth": {
|
||||
"version": "0.0.20",
|
||||
"resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz",
|
||||
"integrity": "sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/radix-vue/node_modules/@vueuse/core": {
|
||||
"version": "10.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@vueuse/core/-/core-10.11.1.tgz",
|
||||
"integrity": "sha512-guoy26JQktXPcz+0n3GukWIy/JDNKti9v6VEMu6kV2sYBsWuGiTU8OWdg+ADfUbHg3/3DlqySDe7JmdHrktiww==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/web-bluetooth": "^0.0.20",
|
||||
"@vueuse/metadata": "10.11.1",
|
||||
"@vueuse/shared": "10.11.1",
|
||||
"vue-demi": ">=0.14.8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
}
|
||||
},
|
||||
"node_modules/radix-vue/node_modules/@vueuse/core/node_modules/vue-demi": {
|
||||
"version": "0.14.10",
|
||||
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz",
|
||||
"integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"vue-demi-fix": "bin/vue-demi-fix.js",
|
||||
"vue-demi-switch": "bin/vue-demi-switch.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@vue/composition-api": "^1.0.0-rc.1",
|
||||
"vue": "^3.0.0-0 || ^2.6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@vue/composition-api": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/radix-vue/node_modules/@vueuse/metadata": {
|
||||
"version": "10.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-10.11.1.tgz",
|
||||
"integrity": "sha512-IGa5FXd003Ug1qAZmyE8wF3sJ81xGLSqTqtQ6jaVfkeZ4i5kS2mwQF61yhVqojRnenVew5PldLyRgvdl4YYuSw==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
}
|
||||
},
|
||||
"node_modules/radix-vue/node_modules/nanoid": {
|
||||
"version": "5.0.9",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.0.9.tgz",
|
||||
|
|
|
@ -421,3 +421,69 @@ tr:last-child > td:first-child {
|
|||
tr:last-child > td:last-child {
|
||||
border-bottom-right-radius: 4px;
|
||||
}
|
||||
|
||||
.sidebar-container {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.sidebar-container nav.sidebar {
|
||||
display: flex;
|
||||
justify-content: end;
|
||||
}
|
||||
|
||||
.sidebar-container .view {
|
||||
width: 60%;
|
||||
}
|
||||
|
||||
|
||||
nav.sidebar h3 {
|
||||
text-transform: uppercase;
|
||||
color: var(--overlay-0);
|
||||
padding: 0 8px;
|
||||
font-size: 8pt;
|
||||
}
|
||||
|
||||
nav.sidebar > .categories {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 192px;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
nav.sidebar a.tab {
|
||||
font-size: 11pt;
|
||||
color: var(--overlay-0);
|
||||
padding: 6px 10px;
|
||||
font-weight: 500;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
nav.sidebar a.tab:hover {
|
||||
background-color: var(--crust);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
nav.sidebar a.tab.router-link-exact-active {
|
||||
background-color: var(--crust);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
nav.sidebar button {
|
||||
font-size: 11pt;
|
||||
font-weight: 500;
|
||||
padding: 6px 10px;
|
||||
background-color: transparent;
|
||||
color: var(--overlay-0);
|
||||
}
|
||||
|
||||
nav.sidebar button:hover {
|
||||
background-color: var(--crust);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
nav.sidebar button.destructive-on-hover:hover {
|
||||
background-color: var(--destructive);
|
||||
color: var(--base);
|
||||
}
|
||||
|
|
|
@ -22,10 +22,12 @@ export type { EventWithPlayerSchema } from './models/EventWithPlayerSchema';
|
|||
export type { EventWithPlayerSchemaList } from './models/EventWithPlayerSchemaList';
|
||||
export type { GetEventPlayersResponse } from './models/GetEventPlayersResponse';
|
||||
export type { GetMatchQuery } from './models/GetMatchQuery';
|
||||
export type { GetUserResponse } from './models/GetUserResponse';
|
||||
export type { MatchSchema } from './models/MatchSchema';
|
||||
export type { PlayerEventRolesSchema } from './models/PlayerEventRolesSchema';
|
||||
export type { PlayerRoleSchema } from './models/PlayerRoleSchema';
|
||||
export type { PlayerSchema } from './models/PlayerSchema';
|
||||
export type { PlayerSchemaList } from './models/PlayerSchemaList';
|
||||
export type { PlayerTeamAvailabilityRoleSchema } from './models/PlayerTeamAvailabilityRoleSchema';
|
||||
export type { PutScheduleForm } from './models/PutScheduleForm';
|
||||
export type { RoleSchema } from './models/RoleSchema';
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
import type { PlayerSchema } from './PlayerSchema';
|
||||
export type GetUserResponse = {
|
||||
isAdmin?: boolean;
|
||||
realUser: (PlayerSchema | null);
|
||||
steamId: string;
|
||||
username: string;
|
||||
};
|
||||
|
|
@ -3,6 +3,7 @@
|
|||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export type PlayerSchema = {
|
||||
isAdmin?: boolean;
|
||||
steamId: string;
|
||||
username: string;
|
||||
};
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
import type { PlayerSchema } from './PlayerSchema';
|
||||
export type PlayerSchemaList = Array<PlayerSchema>;
|
|
@ -6,6 +6,7 @@ import type { RoleSchema } from './RoleSchema';
|
|||
export type ViewTeamMembersResponse = {
|
||||
availability: Array<number>;
|
||||
createdAt: string;
|
||||
isAdmin?: boolean;
|
||||
isTeamLeader?: boolean;
|
||||
playtime: number;
|
||||
roles: Array<RoleSchema>;
|
||||
|
|
|
@ -12,8 +12,10 @@ import type { EventSchema } from '../models/EventSchema';
|
|||
import type { EventWithPlayerSchema } from '../models/EventWithPlayerSchema';
|
||||
import type { EventWithPlayerSchemaList } from '../models/EventWithPlayerSchemaList';
|
||||
import type { GetEventPlayersResponse } from '../models/GetEventPlayersResponse';
|
||||
import type { GetUserResponse } from '../models/GetUserResponse';
|
||||
import type { MatchSchema } from '../models/MatchSchema';
|
||||
import type { PlayerSchema } from '../models/PlayerSchema';
|
||||
import type { PlayerSchemaList } from '../models/PlayerSchemaList';
|
||||
import type { PutScheduleForm } from '../models/PutScheduleForm';
|
||||
import type { SetUsernameJson } from '../models/SetUsernameJson';
|
||||
import type { SubmitMatchJson } from '../models/SubmitMatchJson';
|
||||
|
@ -279,10 +281,10 @@ export class DefaultService {
|
|||
}
|
||||
/**
|
||||
* get_user <GET>
|
||||
* @returns PlayerSchema OK
|
||||
* @returns GetUserResponse OK
|
||||
* @throws ApiError
|
||||
*/
|
||||
public getUser(): CancelablePromise<PlayerSchema> {
|
||||
public getUser(): CancelablePromise<GetUserResponse> {
|
||||
return this.httpRequest.request({
|
||||
method: 'GET',
|
||||
url: '/api/login/get-user',
|
||||
|
@ -518,28 +520,6 @@ export class DefaultService {
|
|||
},
|
||||
});
|
||||
}
|
||||
/**
|
||||
* delete_team <DELETE>
|
||||
* @param teamId
|
||||
* @returns any OK
|
||||
* @throws ApiError
|
||||
*/
|
||||
public deleteTeam(
|
||||
teamId: number,
|
||||
): CancelablePromise<any> {
|
||||
return this.httpRequest.request({
|
||||
method: 'DELETE',
|
||||
url: '/api/team/id/{team_id}/',
|
||||
path: {
|
||||
'team_id': teamId,
|
||||
},
|
||||
errors: {
|
||||
403: `Forbidden`,
|
||||
404: `Not Found`,
|
||||
422: `Unprocessable Content`,
|
||||
},
|
||||
});
|
||||
}
|
||||
/**
|
||||
* view_team <GET>
|
||||
* @param teamId
|
||||
|
@ -801,6 +781,54 @@ export class DefaultService {
|
|||
},
|
||||
});
|
||||
}
|
||||
/**
|
||||
* get_all_users <GET>
|
||||
* @returns PlayerSchemaList OK
|
||||
* @throws ApiError
|
||||
*/
|
||||
public getAllUsers(): CancelablePromise<PlayerSchemaList> {
|
||||
return this.httpRequest.request({
|
||||
method: 'GET',
|
||||
url: '/api/user/all',
|
||||
errors: {
|
||||
422: `Unprocessable Content`,
|
||||
},
|
||||
});
|
||||
}
|
||||
/**
|
||||
* unset_doas <DELETE>
|
||||
* @returns void
|
||||
* @throws ApiError
|
||||
*/
|
||||
public unsetDoas(): CancelablePromise<void> {
|
||||
return this.httpRequest.request({
|
||||
method: 'DELETE',
|
||||
url: '/api/user/doas',
|
||||
errors: {
|
||||
422: `Unprocessable Content`,
|
||||
},
|
||||
});
|
||||
}
|
||||
/**
|
||||
* set_doas <PUT>
|
||||
* @param steamId
|
||||
* @returns PlayerSchema OK
|
||||
* @throws ApiError
|
||||
*/
|
||||
public setDoas(
|
||||
steamId: string,
|
||||
): CancelablePromise<PlayerSchema> {
|
||||
return this.httpRequest.request({
|
||||
method: 'PUT',
|
||||
url: '/api/user/doas/{steam_id}',
|
||||
path: {
|
||||
'steam_id': steamId,
|
||||
},
|
||||
errors: {
|
||||
422: `Unprocessable Content`,
|
||||
},
|
||||
});
|
||||
}
|
||||
/**
|
||||
* set_username <POST>
|
||||
* @param requestBody
|
||||
|
|
|
@ -19,6 +19,9 @@ function logout() {
|
|||
<template>
|
||||
<DropdownMenuRoot>
|
||||
<DropdownMenuTrigger className="profile-button no-border">
|
||||
<span class="aside" v-if="authStore.realUser">
|
||||
{{ authStore.realUser?.username }}, disguised as
|
||||
</span>
|
||||
{{ authStore.username }}
|
||||
<i class="bi bi-chevron-down" />
|
||||
</DropdownMenuTrigger>
|
||||
|
@ -40,6 +43,14 @@ function logout() {
|
|||
</button>
|
||||
</RouterLink>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem v-if="authStore.isAdmin">
|
||||
<RouterLink class="button" :to="{ 'name': 'admin' }">
|
||||
<button>
|
||||
<i class="bi bi-person-check margin" />
|
||||
Super secret admin stuff!
|
||||
</button>
|
||||
</RouterLink>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<button class="destructive" @click="logout">
|
||||
<i class="bi bi-box-arrow-right margin" />
|
||||
|
|
|
@ -13,6 +13,9 @@ import TeamSettingsIntegrationsView from "@/views/TeamSettings/IntegrationsView.
|
|||
import TeamSettingsInvitesView from "@/views/TeamSettings/InvitesView.vue";
|
||||
import TeamSettingsMatchesView from "@/views/TeamSettings/MatchesView.vue";
|
||||
import UserSettingsView from "@/views/UserSettingsView.vue";
|
||||
import AdminView from "@/views/AdminView.vue";
|
||||
import AdminGeneralView from "@/views/Admin/GeneralView.vue";
|
||||
import AdminDoasView from "@/views/Admin/DoasView.vue";
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
|
@ -84,6 +87,23 @@ const router = createRouter({
|
|||
name: "user-settings",
|
||||
component: UserSettingsView,
|
||||
},
|
||||
{
|
||||
path: "/admin",
|
||||
name: "admin",
|
||||
component: AdminView,
|
||||
children: [
|
||||
{
|
||||
path: "",
|
||||
name: "admin/",
|
||||
component: AdminGeneralView,
|
||||
},
|
||||
{
|
||||
path: "doas",
|
||||
name: "admin/doas",
|
||||
component: AdminDoasView,
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
});
|
||||
|
||||
|
|
|
@ -2,18 +2,20 @@ import { defineStore } from "pinia";
|
|||
import { ref } from "vue";
|
||||
import { useClientStore } from "./client";
|
||||
import { useRouter, type LocationQuery } from "vue-router";
|
||||
import { type PlayerSchema } from "@/client";
|
||||
import { type GetUserResponse, type PlayerSchema } from "@/client";
|
||||
|
||||
export const useAuthStore = defineStore("auth", () => {
|
||||
const clientStore = useClientStore();
|
||||
const client = clientStore.client;
|
||||
|
||||
const user = ref<PlayerSchema | null>(null);
|
||||
const user = ref<GetUserResponse | null>(null);
|
||||
const steamId = ref("");
|
||||
const username = ref("");
|
||||
const isLoggedIn = ref(false);
|
||||
const isRegistering = ref(false);
|
||||
const hasCheckedAuth = ref(false);
|
||||
const isAdmin = ref(false);
|
||||
const realUser = ref<PlayerSchema | null>(null);
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
|
@ -35,6 +37,9 @@ export const useAuthStore = defineStore("auth", () => {
|
|||
steamId.value = response.steamId;
|
||||
username.value = response.username;
|
||||
user.value = response;
|
||||
isAdmin.value = response.isAdmin || (response.realUser?.isAdmin ?? false);
|
||||
realUser.value = response.realUser ?? null;
|
||||
|
||||
return response;
|
||||
},
|
||||
undefined,
|
||||
|
@ -76,13 +81,48 @@ export const useAuthStore = defineStore("auth", () => {
|
|||
});
|
||||
}
|
||||
|
||||
async function getAllUsers() {
|
||||
return client.default.getAllUsers();
|
||||
}
|
||||
|
||||
async function setDoas(doasSteamId: string) {
|
||||
return client.default.setDoas(doasSteamId)
|
||||
.then((response) => {
|
||||
if (user.value) {
|
||||
realUser.value = {
|
||||
steamId: user.value.steamId,
|
||||
username: user.value.username,
|
||||
isAdmin: user.value.isAdmin,
|
||||
};
|
||||
}
|
||||
steamId.value = response.steamId;
|
||||
username.value = response.username;
|
||||
});
|
||||
}
|
||||
|
||||
async function unsetDoas() {
|
||||
return client.default.unsetDoas()
|
||||
.then((_) => {
|
||||
if (realUser.value) {
|
||||
steamId.value = realUser.value.steamId;
|
||||
username.value = realUser.value.username;
|
||||
}
|
||||
realUser.value = null;
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
steamId,
|
||||
username,
|
||||
isAdmin,
|
||||
realUser,
|
||||
isLoggedIn,
|
||||
hasCheckedAuth,
|
||||
isRegistering,
|
||||
getUser,
|
||||
getAllUsers,
|
||||
setDoas,
|
||||
unsetDoas,
|
||||
login,
|
||||
logout,
|
||||
setUsername,
|
||||
|
|
|
@ -0,0 +1,64 @@
|
|||
<script setup lang="ts">
|
||||
import { type PlayerSchema, type PlayerSchemaList } from "@/client";
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
import { onMounted, ref } from "vue";
|
||||
|
||||
//const cookies = useCookies(["doas"], { doNotParse: true, autoUpdateDependencies: true }, universalCookie);
|
||||
const doas = ref<PlayerSchema | undefined>();
|
||||
const users = ref<PlayerSchemaList>([]);
|
||||
const authStore = useAuthStore();
|
||||
|
||||
onMounted(() => {
|
||||
authStore.getAllUsers()
|
||||
.then((response) => {
|
||||
users.value = response;
|
||||
//doas.value = response.find(user => user.steamId === cookies.get("doas"));
|
||||
});
|
||||
});
|
||||
|
||||
function setDoas() {
|
||||
if (doas.value) {
|
||||
authStore.setDoas(doas.value.steamId);
|
||||
} else {
|
||||
authStore.unsetDoas();
|
||||
}
|
||||
}
|
||||
|
||||
function removeDoas() {
|
||||
doas.value = undefined;
|
||||
authStore.unsetDoas();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<h2>Become User</h2>
|
||||
<p>
|
||||
Do as/become a specific user.
|
||||
</p>
|
||||
<div>
|
||||
<div class="form-group margin">
|
||||
<h3>User</h3>
|
||||
<v-select
|
||||
v-model="doas"
|
||||
:options="users"
|
||||
label="username"
|
||||
placeholder="Select a user"
|
||||
:clearable="true"
|
||||
:searchable="true"
|
||||
:close-on-select="true"
|
||||
:show-search-input="true"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-group margin">
|
||||
<div class="action-buttons">
|
||||
<button class="destructive-on-hover" @click="removeDoas">
|
||||
<i class="bi bi-trash" />
|
||||
</button>
|
||||
<button class="accent" @click="setDoas">
|
||||
<i class="bi bi-check" />
|
||||
Become {{ doas?.username }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
|
@ -0,0 +1,9 @@
|
|||
<script setup lang="ts">
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<h1>Admin Panel</h1>
|
||||
<p>
|
||||
This is the admin panel. Only admins can access this page.
|
||||
</p>
|
||||
</template>
|
|
@ -0,0 +1,22 @@
|
|||
<script setup lang="ts">
|
||||
import { RouterLink, RouterView } from "vue-router";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main class="sidebar-container admin-panel">
|
||||
<nav class="sidebar">
|
||||
<div class="categories">
|
||||
<h3>Admin Panel</h3>
|
||||
<RouterLink class="tab" :to="{ name: 'admin/' }">
|
||||
General
|
||||
</RouterLink>
|
||||
<RouterLink class="tab" :to="{ name: 'admin/doas' }">
|
||||
Become User
|
||||
</RouterLink>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="view">
|
||||
<RouterView />
|
||||
</div>
|
||||
</main>
|
||||
</template>
|
|
@ -23,7 +23,7 @@ onMounted(() => {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<main class="team-settings" v-if="team">
|
||||
<main class="sidebar-container team-settings" v-if="team">
|
||||
<nav class="sidebar">
|
||||
<div class="categories">
|
||||
<div class="back-link">
|
||||
|
@ -59,72 +59,7 @@ onMounted(() => {
|
|||
</template>
|
||||
|
||||
<style scoped>
|
||||
.team-settings {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.team-settings nav.sidebar {
|
||||
display: flex;
|
||||
justify-content: end;
|
||||
}
|
||||
|
||||
.team-settings .view {
|
||||
width: 60%;
|
||||
}
|
||||
|
||||
.back-link {
|
||||
padding: 8px 16px;
|
||||
}
|
||||
|
||||
nav.sidebar h3 {
|
||||
text-transform: uppercase;
|
||||
color: var(--overlay-0);
|
||||
padding: 0 8px;
|
||||
font-size: 8pt;
|
||||
}
|
||||
|
||||
nav.sidebar > .categories {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 192px;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
nav.sidebar a.tab {
|
||||
font-size: 11pt;
|
||||
color: var(--overlay-0);
|
||||
padding: 6px 10px;
|
||||
font-weight: 500;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
nav.sidebar a.tab:hover {
|
||||
background-color: var(--crust);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
nav.sidebar a.tab.router-link-exact-active {
|
||||
background-color: var(--crust);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
nav.sidebar button {
|
||||
font-size: 11pt;
|
||||
font-weight: 500;
|
||||
padding: 6px 10px;
|
||||
background-color: transparent;
|
||||
color: var(--overlay-0);
|
||||
}
|
||||
|
||||
nav.sidebar button:hover {
|
||||
background-color: var(--crust);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
nav.sidebar button.destructive-on-hover:hover {
|
||||
background-color: var(--destructive);
|
||||
color: var(--base);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -21,17 +21,36 @@ STEAM_OPENID_URL = "https://steamcommunity.com/openid/login"
|
|||
def index():
|
||||
return "test"
|
||||
|
||||
class GetUserResponse(PlayerSchema):
|
||||
real_user: PlayerSchema | None
|
||||
|
||||
@classmethod
|
||||
def from_model(cls, model: Player):
|
||||
return GetUserResponse(
|
||||
steam_id=str(model.steam_id),
|
||||
username=model.username,
|
||||
is_admin=model.is_admin,
|
||||
real_user=None,
|
||||
)
|
||||
|
||||
@api_login.get("/get-user")
|
||||
@spec.validate(
|
||||
resp=Response(
|
||||
HTTP_200=PlayerSchema,
|
||||
HTTP_200=GetUserResponse,
|
||||
HTTP_401=None,
|
||||
),
|
||||
operation_id="get_user"
|
||||
)
|
||||
@requires_authentication
|
||||
def get_user(player: Player, auth_session: AuthSession):
|
||||
return PlayerSchema.from_model(player).dict(by_alias=True)
|
||||
if auth_session.player.steam_id != player.steam_id:
|
||||
return GetUserResponse(
|
||||
steam_id=str(player.steam_id),
|
||||
username=player.username,
|
||||
is_admin=player.is_admin,
|
||||
real_user=PlayerSchema.from_model(auth_session.player)
|
||||
).dict(by_alias=True)
|
||||
return GetUserResponse.from_model(player).dict(by_alias=True)
|
||||
|
||||
@api_login.post("/authenticate")
|
||||
def steam_authenticate():
|
||||
|
|
|
@ -3,6 +3,7 @@ from typing import Optional
|
|||
from flask import abort, make_response, request
|
||||
from sqlalchemy.sql.operators import json_path_getitem_op
|
||||
from app_db import db
|
||||
from models import auth_session
|
||||
from models.auth_session import AuthSession
|
||||
from models.player import Player
|
||||
from models.player_team import PlayerTeam
|
||||
|
@ -13,6 +14,7 @@ def requires_authentication(f):
|
|||
@wraps(f)
|
||||
def decorator(*args, **kwargs):
|
||||
auth = request.cookies.get("auth")
|
||||
doas = request.cookies.get("doas")
|
||||
|
||||
if not auth:
|
||||
abort(401)
|
||||
|
@ -28,6 +30,30 @@ def requires_authentication(f):
|
|||
player = auth_session.player
|
||||
kwargs["player"] = player
|
||||
kwargs["auth_session"] = auth_session
|
||||
|
||||
if doas and player.is_admin:
|
||||
doas_int = int(doas)
|
||||
if doas_int and doas_int != player.steam_id:
|
||||
doas_player = db.session.query(
|
||||
Player
|
||||
).where(
|
||||
Player.steam_id == doas_int
|
||||
).one_or_none()
|
||||
|
||||
if doas_player:
|
||||
kwargs["player"] = doas_player
|
||||
|
||||
return f(*args, **kwargs)
|
||||
return decorator
|
||||
|
||||
def requires_admin(f):
|
||||
@wraps(f)
|
||||
def decorator(*args, **kwargs):
|
||||
auth_session: AuthSession | None = kwargs["auth_session"]
|
||||
if not auth_session or not auth_session.player:
|
||||
abort(401)
|
||||
if not auth_session.player.is_admin:
|
||||
abort(403)
|
||||
return f(*args, **kwargs)
|
||||
return decorator
|
||||
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
"""Add is_admin field
|
||||
|
||||
Revision ID: f8588cdf998e
|
||||
Revises: 3b18d4bfc6ac
|
||||
Create Date: 2025-05-13 17:15:53.887878
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'f8588cdf998e'
|
||||
down_revision = '3b18d4bfc6ac'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('players', schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column('is_admin', sa.Boolean(), nullable=False, default=False))
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('players', schema=None) as batch_op:
|
||||
batch_op.drop_column('is_admin')
|
||||
|
||||
# ### end Alembic commands ###
|
|
@ -12,6 +12,7 @@ class Player(app_db.BaseModel):
|
|||
|
||||
steam_id: Mapped[int] = mapped_column(BigInteger, primary_key=True)
|
||||
username: Mapped[str] = mapped_column(String(63))
|
||||
is_admin: Mapped[bool] = mapped_column(default=False)
|
||||
|
||||
teams: Mapped[list["PlayerTeam"]] = relationship(back_populates="player")
|
||||
auth_sessions: Mapped[list["AuthSession"]] = relationship(back_populates="player")
|
||||
|
@ -23,10 +24,15 @@ class Player(app_db.BaseModel):
|
|||
class PlayerSchema(spec.BaseModel):
|
||||
steam_id: str
|
||||
username: str
|
||||
is_admin: bool = False
|
||||
|
||||
@classmethod
|
||||
def from_model(cls, player: Player):
|
||||
return cls(steam_id=str(player.steam_id), username=player.username)
|
||||
return cls(
|
||||
steam_id=str(player.steam_id),
|
||||
username=player.username,
|
||||
is_admin=player.is_admin
|
||||
)
|
||||
|
||||
|
||||
from models.auth_session import AuthSession
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
from flask import Blueprint
|
||||
from flask import Blueprint, abort, make_response
|
||||
from spectree import Response
|
||||
from middleware import requires_authentication
|
||||
from middleware import requires_admin, requires_authentication
|
||||
from models.player import Player, PlayerSchema
|
||||
from spec import spec, BaseModel
|
||||
from app_db import db
|
||||
|
@ -23,3 +23,53 @@ def set_username(json: SetUsernameJson, player: Player, **kwargs):
|
|||
player.username = json.username
|
||||
db.session.commit()
|
||||
return PlayerSchema.from_model(player).dict(by_alias=True), 200
|
||||
|
||||
@api_user.get("/all")
|
||||
@spec.validate(
|
||||
resp=Response(
|
||||
HTTP_200=list[PlayerSchema],
|
||||
),
|
||||
operation_id="get_all_users",
|
||||
)
|
||||
@requires_authentication
|
||||
@requires_admin
|
||||
def get_all_users(player: Player, **kwargs):
|
||||
players = db.session.query(Player).all()
|
||||
return list(map(lambda p: PlayerSchema.from_model(p).dict(by_alias=True), players)), 200
|
||||
|
||||
@api_user.put("/doas/<steam_id>")
|
||||
@spec.validate(
|
||||
resp=Response(
|
||||
HTTP_200=PlayerSchema,
|
||||
),
|
||||
operation_id="set_doas"
|
||||
)
|
||||
@requires_authentication
|
||||
@requires_admin
|
||||
def set_doas(steam_id: str, **_):
|
||||
player = db.session.query(Player).where(
|
||||
Player.steam_id == steam_id
|
||||
).one_or_none()
|
||||
|
||||
if not player:
|
||||
abort(404)
|
||||
|
||||
resp = make_response(
|
||||
PlayerSchema.from_model(player).dict(by_alias=True)
|
||||
)
|
||||
resp.set_cookie("doas", steam_id, httponly=True)
|
||||
return resp
|
||||
|
||||
@api_user.delete("/doas")
|
||||
@spec.validate(
|
||||
resp=Response(
|
||||
HTTP_204=None,
|
||||
),
|
||||
operation_id="unset_doas"
|
||||
)
|
||||
@requires_authentication
|
||||
@requires_admin
|
||||
def unset_doas(**_):
|
||||
resp = make_response({ }, 204)
|
||||
resp.delete_cookie("doas", httponly=True)
|
||||
return resp
|
||||
|
|
Loading…
Reference in New Issue