feat: rbac support with admin permission
authorxangelo <me@xangelo.ca>
Wed, 6 Sep 2023 18:54:41 +0000 (14:54 -0400)
committerxangelo <me@xangelo.ca>
Wed, 6 Sep 2023 18:56:28 +0000 (14:56 -0400)
There is now a permissions array on the player object loaded from
`loadPlayer` that includes a `permissions` array that lets us check if
the user has certain permissions.

Support permissions: admin,moderator

The first thing we did was remove the hard-coded key during chat resets

migrations/20230906180935_rbac.ts [new file with mode: 0644]
src/server/api.ts
src/server/player.ts
src/shared/player.ts

diff --git a/migrations/20230906180935_rbac.ts b/migrations/20230906180935_rbac.ts
new file mode 100644 (file)
index 0000000..e110d2d
--- /dev/null
@@ -0,0 +1,18 @@
+import { Knex } from "knex";
+
+
+export async function up(knex: Knex): Promise<void> {
+  return knex.schema.createTable('permissions', function(table){
+    table.string('name').primary();
+  }).createTable('player_permissions', function(table) {
+      table.string('permission');
+      table.uuid('player_id');
+      table.primary(['permission', 'player_id']);
+    });
+}
+
+
+export async function down(knex: Knex): Promise<void> {
+  return knex.schema.dropTable('permissions').dropTable('player_permissions');
+}
+
index 80aa82af1b4649f7dacfc2d639a57d5274e0f45f..a64809cab87cad94bf06e567f74305cebb775e1c 100644 (file)
@@ -165,23 +165,23 @@ app.post('/chat', authEndpoint, async (req: Request, res: Response) => {
   }
 
   let message: Message;
-  if(msg.startsWith('/server lmnop')) {
+  if(msg.startsWith('/server') && req.player.permissions.includes('admin')) {
     try {
-      if(msg === '/server lmnop refresh-monsters') {
+      if(msg === '/server refresh-monsters') {
         await createMonsters();
         message = broadcastMessage('server', 'Monster refresh!');
       }
-      else if(msg === '/server lmnop refresh-cities') {
+      else if(msg === '/server refresh-cities') {
         await createAllCitiesAndLocations();
         message = broadcastMessage('server', 'Cities, Locations, and Paths refreshed!');
       }
-      else if(msg === '/server lmnop refresh-shops') {
+      else if(msg === '/server refresh-shops') {
         await createShopItems();
         await createShopEquipment();
         message = broadcastMessage('server', 'Refresh shop items');
       }
       else {
-        const str = msg.split('/server lmnop ')[1];
+        const str = msg.split('/server ')[1];
         if(str) {
           message = broadcastMessage('server', str);
         }
index 527069b6693acfd1a2e92142bc59787b6d65abba..a9d077c23c68b474b0355cb0ff23ec76f9a55fbf 100644 (file)
@@ -7,7 +7,12 @@ import {logger} from './lib/logger';
 
 export async function loadPlayer(authToken: string): Promise<Player> {
   const res = await db.first()
-            .select('players.*', 'profession_levels.level', 'profession_levels.exp')
+            .select(
+              'players.*',
+              'profession_levels.level',
+              'profession_levels.exp',
+              db.raw(`coalesce(pp.permissions, '[]'::json) as permissions`)
+            )
             .from<Player>('players')
             .join('profession_levels', function() {
               this.on(function() {
@@ -15,11 +20,15 @@ export async function loadPlayer(authToken: string): Promise<Player> {
                 this.andOn('profession_levels.profession', '=', 'players.profession')
               })
             })
+            .leftJoin(
+              db.raw(`(select json_agg(pp.permission) as permissions, pp.player_id from player_permissions pp group by pp.player_id) pp`),
+              'pp.player_id','=', 'players.id'
+            )
             .where({
               'players.id': authToken
             });
 
-    return res;
+  return res;
 }
 
 export async function createPlayer(): Promise<Player> {
index b7c172ab978d421e8fa3ee6cb2f8a70a09c8501e..93d75f3bdb8ed3816ae3803f1e095ac07879f50c 100644 (file)
@@ -3,6 +3,8 @@ import { Stat } from './stats';
 import { SkillDefinition, Skill } from './skills';
 import { EquippedItemDetails } from './equipped';
 
+export type Permission = 'admin' | 'moderator';
+
 export type Player = {
   id: string,
   account_type: 'session' | 'auth',
@@ -21,6 +23,7 @@ export type Player = {
   city_id: number;
   stat_points: number;
   vigor: number;
+  permissions: Permission[]
 }
 
 export type PlayerWithSkills = Player & {