From 6e1e699083e07cdc6e14914c7fb0e541a3f16075 Mon Sep 17 00:00:00 2001
From: Luc <luc@lucemans.nl>
Date: Thu, 19 Dec 2024 01:27:39 +0100
Subject: [PATCH] Introduce user settings and subtexts

---
 engine/src/models/user/userentry.rs       |  6 ++++
 engine/src/routes/users/mod.rs            | 23 +++++++++++++-
 web/src/api/schema.gen.ts                 | 38 +++++++++++++++++++++++
 web/src/api/user/index.ts                 | 12 +++++++
 web/src/index.css                         |  4 +++
 web/src/routes/settings/_layout/build.tsx | 20 ++++++++++--
 web/src/routes/settings/_layout/users.tsx | 28 ++++++++++++++++-
 7 files changed, 127 insertions(+), 4 deletions(-)

diff --git a/engine/src/models/user/userentry.rs b/engine/src/models/user/userentry.rs
index c3ddad2..b326b9d 100644
--- a/engine/src/models/user/userentry.rs
+++ b/engine/src/models/user/userentry.rs
@@ -21,6 +21,12 @@ impl UserEntry {
         self.oauth_sub == "$$SYSTEM$$"
     }
 
+    pub async fn find_all(database: &Database) -> Result<Vec<UserEntry>, sqlx::Error> {
+        query_as!(UserEntry, "SELECT user_id, oauth_sub, oauth_data::text::json as \"oauth_data!: Json<Userinfo>\", nickname, created_at, updated_at FROM users")
+            .fetch_all(&database.pool)
+            .await
+    }
+
     pub async fn upsert(
         oauth_userinfo: &Userinfo,
         nickname: Option<String>,
diff --git a/engine/src/routes/users/mod.rs b/engine/src/routes/users/mod.rs
index 71d7d4f..4ace889 100644
--- a/engine/src/routes/users/mod.rs
+++ b/engine/src/routes/users/mod.rs
@@ -6,7 +6,7 @@ use reqwest::StatusCode;
 
 use super::{error::HttpError, ApiTags};
 use crate::{
-    auth::middleware::AuthUser,
+    auth::{middleware::AuthUser, permissions::Action},
     models::user::{user::User, userentry::UserEntry},
     state::AppState,
 };
@@ -17,6 +17,27 @@ pub struct UserApi;
 
 #[OpenApi]
 impl UserApi {
+    /// /user
+    ///
+    /// List all users
+    #[oai(path = "/user", method = "get", tag = "ApiTags::User")]
+    pub async fn users(
+        &self,
+        user: AuthUser,
+        state: Data<&Arc<AppState>>,
+    ) -> Result<Json<Vec<User>>> {
+        user.check_policy("user", "", Action::Read).await?;
+
+        Ok(Json(
+            UserEntry::find_all(&state.database)
+                .await
+                .map_err(HttpError::from)?
+                .into_iter()
+                .map(|user| user.into())
+                .collect::<Vec<User>>(),
+        ))
+    }
+
     /// /user/:user_id
     ///
     /// Get a User by `user_id`
diff --git a/web/src/api/schema.gen.ts b/web/src/api/schema.gen.ts
index fb32993..7f2aad1 100644
--- a/web/src/api/schema.gen.ts
+++ b/web/src/api/schema.gen.ts
@@ -1327,6 +1327,44 @@ export type paths = {
         patch?: never;
         trace?: never;
     };
+    "/user": {
+        parameters: {
+            query?: never;
+            header?: never;
+            path?: never;
+            cookie?: never;
+        };
+        /**
+         * /user
+         * @description List all users
+         */
+        get: {
+            parameters: {
+                query?: never;
+                header?: never;
+                path?: never;
+                cookie?: never;
+            };
+            requestBody?: never;
+            responses: {
+                200: {
+                    headers: {
+                        [name: string]: unknown;
+                    };
+                    content: {
+                        "application/json; charset=utf-8": components["schemas"]["User"][];
+                    };
+                };
+            };
+        };
+        put?: never;
+        post?: never;
+        delete?: never;
+        options?: never;
+        head?: never;
+        patch?: never;
+        trace?: never;
+    };
     "/user/{user_id}": {
         parameters: {
             query?: never;
diff --git a/web/src/api/user/index.ts b/web/src/api/user/index.ts
index a7c4bc8..55772aa 100644
--- a/web/src/api/user/index.ts
+++ b/web/src/api/user/index.ts
@@ -20,3 +20,15 @@ export const getUserById = (user_id: number) =>
     });
 
 export const useUserById = (user_id: number) => useQuery(getUserById(user_id));
+
+export const getUsers = () =>
+    queryOptions({
+        queryKey: ['users'],
+        queryFn: async () => {
+            const response = await apiRequest('/user', 'get', {});
+
+            return response.data;
+        },
+    });
+
+export const useUsers = () => useQuery(getUsers());
diff --git a/web/src/index.css b/web/src/index.css
index 26376d7..5afac88 100644
--- a/web/src/index.css
+++ b/web/src/index.css
@@ -96,6 +96,10 @@ a {
     all: unset;
 }
 
+.link {
+    @apply text-blue-500 hover:cursor-pointer hover:underline;
+}
+
 .pre {
     @apply bg-neutral-100 rounded-md p-2;
 }
diff --git a/web/src/routes/settings/_layout/build.tsx b/web/src/routes/settings/_layout/build.tsx
index 4fa3be4..dea2284 100644
--- a/web/src/routes/settings/_layout/build.tsx
+++ b/web/src/routes/settings/_layout/build.tsx
@@ -1,4 +1,5 @@
-import { createFileRoute } from '@tanstack/react-router';
+import { createFileRoute, Link } from '@tanstack/react-router';
+import { FiHeart } from 'react-icons/fi';
 
 import { BuildDetails } from '@/components/settings/BuildDetails';
 
@@ -12,5 +13,20 @@ export const Route = createFileRoute('/settings/_layout/build')({
 });
 
 function RouteComponent() {
-    return <BuildDetails />;
+    return (
+        <>
+            <BuildDetails />
+            <p className="text-sm text-gray-500 px-4 flex items-center gap-1">
+                We thank you for using open-source software.{' '}
+                <Link
+                    href="https://v3x.company"
+                    className="link"
+                    target="_blank"
+                >
+                    V3X Labs
+                </Link>{' '}
+                <FiHeart className="text-xs" />
+            </p>
+        </>
+    );
 }
diff --git a/web/src/routes/settings/_layout/users.tsx b/web/src/routes/settings/_layout/users.tsx
index 5219108..ca855fd 100644
--- a/web/src/routes/settings/_layout/users.tsx
+++ b/web/src/routes/settings/_layout/users.tsx
@@ -1,5 +1,8 @@
 import { createFileRoute } from '@tanstack/react-router';
 
+import { useUsers } from '@/api/user';
+import { UserProfile } from '@/components/UserProfile';
+
 export const Route = createFileRoute('/settings/_layout/users')({
     component: RouteComponent,
     context() {
@@ -10,5 +13,28 @@ export const Route = createFileRoute('/settings/_layout/users')({
 });
 
 function RouteComponent() {
-    return <div>Hello "/settings/users"!</div>;
+    const { data } = useUsers();
+
+    return (
+        <>
+            <ul className="space-y-2">
+                {data
+                    ?.filter((user) => user.user_id !== 1)
+                    .map((user) => (
+                        <li key={user.user_id}>
+                            <div className="card no-padding p-2">
+                                <UserProfile
+                                    user_id={user.user_id}
+                                    variant="full"
+                                />
+                            </div>
+                        </li>
+                    ))}
+            </ul>
+            <p className="text-sm text-gray-500 px-4">
+                With OAuth configured users will get automatically added here as
+                soon as they log in the for the first time.
+            </p>
+        </>
+    );
 }