Compare commits

...

5 Commits

Author SHA1 Message Date
533e6ea6af fix: no avatar handling 2025-06-30 00:08:25 +02:00
0c24ed9382 feat: check role assignment 2025-06-30 00:08:14 +02:00
f5c61bb6a0 feat: show roles 2025-06-29 23:20:59 +02:00
eb05f830fe feat: roles & group state 2025-06-29 23:20:50 +02:00
d86a9de388 feat: assign system roles 2025-06-29 23:19:05 +02:00
11 changed files with 161 additions and 40 deletions

View File

@ -33,6 +33,29 @@ func (q *Queries) AddPermissionsToRoleByKey(ctx context.Context, arg AddPermissi
return err return err
} }
const assignRolePermission = `-- name: AssignRolePermission :exec
INSERT INTO role_permissions (role_id, permission_id)
VALUES (
$1,
(
SELECT id
FROM permissions p
WHERE p.scope = split_part($2, '_', 1)
AND p.name = right($2, length($2) - position('_' IN $2))
)
)
`
type AssignRolePermissionParams struct {
RoleID uuid.UUID `json:"role_id"`
Key string `json:"key"`
}
func (q *Queries) AssignRolePermission(ctx context.Context, arg AssignRolePermissionParams) error {
_, err := q.db.Exec(ctx, assignRolePermission, arg.RoleID, arg.Key)
return err
}
const createRole = `-- name: CreateRole :one const createRole = `-- name: CreateRole :one
INSERT INTO roles (name, scope, description) INSERT INTO roles (name, scope, description)
VALUES ($1, $2, $3) VALUES ($1, $2, $3)
@ -80,6 +103,24 @@ func (q *Queries) FindRole(ctx context.Context, arg FindRoleParams) (Role, error
return i, err return i, err
} }
const getRoleAssignment = `-- name: GetRoleAssignment :one
SELECT role_id, permission_id FROM role_permissions
WHERE role_id = $1 AND permission_id = (SELECT id FROM permissions p WHERE p.scope = split_part($2, '_', 1) AND p.name = right($2, length($2) - position('_' IN $2)))
LIMIT 1
`
type GetRoleAssignmentParams struct {
RoleID uuid.UUID `json:"role_id"`
Key string `json:"key"`
}
func (q *Queries) GetRoleAssignment(ctx context.Context, arg GetRoleAssignmentParams) (RolePermission, error) {
row := q.db.QueryRow(ctx, getRoleAssignment, arg.RoleID, arg.Key)
var i RolePermission
err := row.Scan(&i.RoleID, &i.PermissionID)
return i, err
}
const getRolesGroupedWithPermissions = `-- name: GetRolesGroupedWithPermissions :many const getRolesGroupedWithPermissions = `-- name: GetRolesGroupedWithPermissions :many
SELECT SELECT
r.scope, r.scope,

View File

@ -2,7 +2,6 @@ package user
import ( import (
"context" "context"
"fmt"
"log" "log"
"gitea.local/admin/hspguard/internal/repository" "gitea.local/admin/hspguard/internal/repository"
@ -143,7 +142,7 @@ var (
"system_revoke_sessions", "system_revoke_sessions",
}, },
Role: repository.Role{ Role: repository.Role{
Name: "family_member", Name: "member",
Description: String("User that is able to use home services"), Description: String("User that is able to use home services"),
}, },
}, },
@ -180,7 +179,10 @@ func EnsureSystemPermissions(ctx context.Context, repo *repository.Queries) {
} }
for _, role := range SYSTEM_ROLES { for _, role := range SYSTEM_ROLES {
found, err := repo.FindRole(ctx, repository.FindRoleParams{ var found repository.Role
var err error
found, err = repo.FindRole(ctx, repository.FindRoleParams{
Scope: SYSTEM_SCOPE, Scope: SYSTEM_SCOPE,
Name: role.Name, Name: role.Name,
}) })
@ -196,17 +198,18 @@ func EnsureSystemPermissions(ctx context.Context, repo *repository.Queries) {
} }
} }
var mappedPerms []string
for _, perm := range role.Permissions { for _, perm := range role.Permissions {
mappedPerms = append(mappedPerms, fmt.Sprintf("%s_%s", SYSTEM_SCOPE, perm)) if _, exists := repo.GetRoleAssignment(ctx, repository.GetRoleAssignmentParams{
} RoleID: found.ID,
Key: perm,
if err := repo.AddPermissionsToRoleByKey(ctx, repository.AddPermissionsToRoleByKeyParams{ }); exists != nil {
RoleID: found.ID, if err := repo.AssignRolePermission(ctx, repository.AssignRolePermissionParams{
PermissionKeys: mappedPerms, RoleID: found.ID,
}); err != nil { Key: perm,
log.Fatalf("ERR: Failed to assign required permissions to SYSTEM role %s: %v\n", found.Name, err) }); err != nil {
log.Fatalf("ERR: Failed to assign permission '%s' to SYSTEM role %s: %v\n", perm, found.Name, err)
}
}
} }
} }
} }

View File

@ -53,6 +53,22 @@ INSERT INTO roles (name, scope, description)
VALUES ($1, $2, $3) VALUES ($1, $2, $3)
RETURNING *; RETURNING *;
-- name: GetRoleAssignment :one
SELECT * FROM role_permissions
WHERE role_id = $1 AND permission_id = (SELECT id FROM permissions p WHERE p.scope = split_part(sqlc.arg('key'), '_', 1) AND p.name = right(sqlc.arg('key'), length(sqlc.arg('key')) - position('_' IN sqlc.arg('key'))))
LIMIT 1;
-- name: AssignRolePermission :exec
INSERT INTO role_permissions (role_id, permission_id)
VALUES (
$1,
(
SELECT id
FROM permissions p
WHERE p.scope = split_part(sqlc.arg('key'), '_', 1)
AND p.name = right(sqlc.arg('key'), length(sqlc.arg('key')) - position('_' IN sqlc.arg('key')))
)
);
-- name: AddPermissionsToRoleByKey :exec -- name: AddPermissionsToRoleByKey :exec
INSERT INTO role_permissions (role_id, permission_id) INSERT INTO role_permissions (role_id, permission_id)
SELECT SELECT

View File

@ -5,7 +5,7 @@ import { useMemo, type FC } from "react";
export interface AvatarProps { export interface AvatarProps {
iconSize?: number; iconSize?: number;
className?: string; className?: string;
avatarId?: string; avatarId?: string | null;
} }
const Avatar: FC<AvatarProps> = ({ iconSize = 32, className, avatarId }) => { const Avatar: FC<AvatarProps> = ({ iconSize = 32, className, avatarId }) => {

View File

@ -1,10 +1,12 @@
import { useEffect, type FC } from "react"; import { useEffect, type FC } from "react";
import Breadcrumbs from "@/components/ui/breadcrumbs"; import Breadcrumbs from "@/components/ui/breadcrumbs";
import { getRolesApi } from "@/api/admin/roles"; import { useRoles } from "@/store/admin/rolesGroups";
import type { AppPermission } from "@/types";
interface DisplayRole { interface DisplayRole {
name: string; name: string;
description: string; description: string;
permissions: AppPermission[];
} }
interface IPermissionGroupProps { interface IPermissionGroupProps {
@ -15,32 +17,51 @@ interface IPermissionGroupProps {
const RolesGroup: FC<IPermissionGroupProps> = ({ scope, roles }) => { const RolesGroup: FC<IPermissionGroupProps> = ({ scope, roles }) => {
return ( return (
<div className="border dark:border-gray-800 border-gray-300 p-4 rounded mb-4"> <div className="border dark:border-gray-800 border-gray-300 p-4 rounded mb-4">
<h2 className="dark:text-gray-300 text-gray-800 text-lg font-semibold"> <h2 className="dark:text-gray-300 text-gray-800 text-lg font-semibold mb-4">
{scope} {scope.toUpperCase()} <span className="opacity-45">(scope)</span>
</h2> </h2>
{(roles?.length ?? 0) > 0 && ( {roles?.map((role, index) => (
<ol className="grid gap-4 gap-y-3 grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 text-gray-800 dark:text-gray-300 font-medium"> <div>
{roles!.map(({ name, description }) => ( <h2 className="dark:text-gray-300 text-gray-800 text-md font-semibold">
<li className="before:w-2 before:h-2 before:block before:translate-y-2 before:bg-gray-400 dark:before:bg-gray-700 before:rounded-xs flex flex-row items-start gap-2"> {role.name.toUpperCase()} <span className="opacity-45">(role)</span>
<div className="flex flex-col gap-1"> </h2>
<label>{name}</label>
<p className="text-xs text-gray-400 dark:text-gray-500"> <p className="text-sm text-gray-400 dark:text-gray-500">
{description} {role.description}
</p> </p>
</div>
</li> <ol className="grid gap-4 gap-y-3 grid-cols-1 my-4 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 text-gray-800 dark:text-gray-300 font-medium">
))} {role.permissions.map(({ name, description }) => (
</ol> <li className="before:w-2 before:h-2 before:block before:translate-y-2 before:bg-gray-400 dark:before:bg-gray-700 before:rounded-xs flex flex-row items-start gap-2">
)} <div className="flex flex-col gap-1">
<label>{name}</label>
<p className="text-xs text-gray-400 dark:text-gray-500">
{description}
</p>
</div>
</li>
))}
</ol>
{index + 1 < roles.length && (
<div className="h-[1px] bg-gray-700 w-full rounded my-6"></div>
)}
</div>
))}
</div> </div>
); );
}; };
const AdminRolesGroupsPage: FC = () => { const AdminRolesGroupsPage: FC = () => {
const roles = useRoles((s) => s.roles);
const fetch = useRoles((s) => s.fetch);
useEffect(() => { useEffect(() => {
getRolesApi().then((res) => console.log("roles:", res)); fetch();
}, []); }, [fetch]);
console.log("roles:", roles);
return ( return (
<div className="relative flex flex-col items-stretch w-full"> <div className="relative flex flex-col items-stretch w-full">
@ -58,7 +79,11 @@ const AdminRolesGroupsPage: FC = () => {
]} ]}
/> />
</div> </div>
<div className="px-7"></div> <div className="px-7">
{Object.keys(roles).map((scope) => (
<RolesGroup scope={scope} roles={roles[scope]} />
))}
</div>
</div> </div>
); );
}; };

View File

@ -113,7 +113,7 @@ const AdminServiceSessionsPage: FC = () => {
{/* <SessionSource deviceInfo={session.} /> */} {/* <SessionSource deviceInfo={session.} /> */}
{typeof session.api_service?.icon_url === "string" && ( {typeof session.api_service?.icon_url === "string" && (
<Avatar <Avatar
avatarId={session.api_service.icon_url} avatarId={session.api_service.icon_url ?? null}
className="w-7 h-7 min-w-7" className="w-7 h-7 min-w-7"
/> />
)} )}
@ -129,7 +129,7 @@ const AdminServiceSessionsPage: FC = () => {
<div className="flex flex-row items-center gap-2 justify-start"> <div className="flex flex-row items-center gap-2 justify-start">
{typeof session.user?.profile_picture === "string" && ( {typeof session.user?.profile_picture === "string" && (
<Avatar <Avatar
avatarId={session.user.profile_picture} avatarId={session.user.profile_picture ?? null}
className="w-7 h-7 min-w-7" className="w-7 h-7 min-w-7"
/> />
)} )}

View File

@ -127,7 +127,7 @@ const AdminUserSessionsPage: FC = () => {
<div className="flex flex-row items-center gap-2 justify-start"> <div className="flex flex-row items-center gap-2 justify-start">
{typeof session.user?.profile_picture === "string" && ( {typeof session.user?.profile_picture === "string" && (
<Avatar <Avatar
avatarId={session.user.profile_picture} avatarId={session.user.profile_picture ?? null}
className="w-7 h-7 min-w-7" className="w-7 h-7 min-w-7"
/> />
)} )}

View File

@ -64,7 +64,7 @@ const AdminViewUserPage: FC = () => {
Avatar: Avatar:
</span> </span>
<Avatar <Avatar
avatarId={user.profile_picture ?? undefined} avatarId={user.profile_picture ?? null}
className="w-16 h-16" className="w-16 h-16"
iconSize={28} iconSize={28}
/> />

View File

@ -94,7 +94,7 @@ const AdminUsersPage: FC = () => {
<Avatar <Avatar
iconSize={21} iconSize={21}
className="w-8 h-8" className="w-8 h-8"
avatarId={user.profile_picture ?? undefined} avatarId={user.profile_picture ?? null}
/> />
<p>{user.full_name}</p> <p>{user.full_name}</p>
</div> </div>

View File

@ -21,7 +21,7 @@ const VerifyReviewPage: FC = () => {
</p> </p>
<Avatar <Avatar
avatarId={profile?.profile_picture ?? undefined} avatarId={profile?.profile_picture ?? null}
iconSize={64} iconSize={64}
className="w-48 h-48 min-w-48 mx-auto mt-4" className="w-48 h-48 min-w-48 mx-auto mt-4"
/> />

View File

@ -0,0 +1,36 @@
import { getRolesApi } from "@/api/admin/roles";
import type { AppPermission, AppRole } from "@/types";
import { create } from "zustand";
export type RolesMap = Record<
string,
(AppRole & { permissions: AppPermission[] })[]
>;
export interface IRolesGroups {
roles: RolesMap;
fetching: boolean;
fetch: () => Promise<void>;
}
export const useRoles = create<IRolesGroups>((set) => ({
roles: {},
fetching: false,
fetch: async () => {
set({ fetching: true });
try {
const response = await getRolesApi();
set({
roles: Object.fromEntries(response.map((r) => [r.scope, r.roles])),
});
} catch (err) {
console.log("ERR: Failed to fetch admin roles:", err);
} finally {
set({ fetching: false });
}
},
}));