Skip to content

Commit

Permalink
feat: add admin user profile with avatar (#75)
Browse files Browse the repository at this point in the history
  • Loading branch information
StructuralRealist committed Sep 23, 2023
1 parent 087495b commit a74c2eb
Show file tree
Hide file tree
Showing 16 changed files with 251 additions and 48 deletions.
2 changes: 1 addition & 1 deletion packages/content-platform/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"strapi": {
"displayName": "Curator",
"name": "curator",
"description": "Curator is a full-stack CMS built on Strapi.",
"description": "Curator is a full-stack CMS built on Strapi and Next.js.",
"kind": "plugin"
},
"dependencies": {
Expand Down
5 changes: 5 additions & 0 deletions packages/content-platform/server/bootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { Strapi } from "@strapi/strapi";

import versioningLifecycle from "./lifecycles/versioning.lifecycle";
import auditLifecycle from "./lifecycles/audit.lifecycle";
import profileLifecycle from "./lifecycles/profile.lifecycle";

export default async function ({ strapi }: { strapi: Strapi }) {
/*
Expand All @@ -12,4 +13,8 @@ export default async function ({ strapi }: { strapi: Strapi }) {
* Set up versioning lifecycle hooks.
*/
versioningLifecycle(strapi);
/*
* Set up admin user profile lifecycle hooks.
*/
profileLifecycle(strapi);
}
2 changes: 2 additions & 0 deletions packages/content-platform/server/content-types/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import secretSchema from "./secret";
import auditSchema from "./audit";
import versionSchema from "./version";
import profileSchema from "./profile";

export default {
"curator-secret": { schema: secretSchema },
"curator-audit-log": { schema: auditSchema },
"curator-version": { schema: versionSchema },
"curator-admin-user-profile": { schema: profileSchema },
};
30 changes: 30 additions & 0 deletions packages/content-platform/server/content-types/profile.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
export default {
kind: "collectionType",
collectionName: "curator_admin_user_profile",
info: {
singularName: "curator-admin-user-profile",
pluralName: "curator-admin-user-profiles",
displayName: "Curator Admin User Profile",
description: "A profile for admin users.",
},
options: {
draftAndPublish: false,
},
pluginOptions: {
"content-type-builder": {
visible: false,
},
},
attributes: {
user: {
type: "relation",
relation: "oneToOne",
target: "admin::user",
},
avatar: {
type: "media",
multiple: false,
allowedTypes: ["images"],
},
},
};
2 changes: 2 additions & 0 deletions packages/content-platform/server/controllers/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import dashboardController from "./dashboard.controller";
import secretsController from "./secrets.controller";
import versioningController from "./versioning.controller";
import profileController from "./profile.controller";

export default {
secretsController,
dashboardController,
versioningController,
profileController,
};
10 changes: 10 additions & 0 deletions packages/content-platform/server/controllers/profile.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { Strapi } from "@strapi/strapi";

export default ({ strapi }: { strapi: Strapi }) => ({
getMe() {
return strapi.plugin("curator").service("profileService").getMe();
},
updateMe() {
return strapi.plugin("curator").service("profileService").updateMe();
},
});
48 changes: 48 additions & 0 deletions packages/content-platform/server/lifecycles/profile.lifecycle.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import type { Strapi } from "@strapi/strapi";
import * as R from "ramda";

const ADMIN_USER_UID = "admin::user";
const PROFILE_UID = "plugin::curator.curator-admin-user-profile";

export default async function profileLifecycle(strapi: Strapi) {
/*
* Make sure admin users always have a profile
*/
const admins = (await strapi.entityService.findMany(ADMIN_USER_UID)) as any[];

const existingProfiles = (await strapi.entityService.findMany(PROFILE_UID, {
filters: {
user: R.pluck("id", admins),
},
populate: ["user"],
})) as any[];

const adminsWithoutProfile = R.without(
existingProfiles.map(R.path(["user", "id"])),
R.pluck("id", admins),
);

for await (const adminId of adminsWithoutProfile) {
await strapi.entityService.create(PROFILE_UID, { data: { user: adminId } });
}

strapi.db.lifecycles.subscribe({
models: [ADMIN_USER_UID],

async afterCreate({ result }: any) {
await strapi.entityService.create(PROFILE_UID, {
data: { user: result.id },
});
},

async afterDelete({ result }: any) {
const items = await strapi.entityService.findMany(PROFILE_UID, {
filters: { user: result.id },
});

if (Array.isArray(items) && items[0]) {
await strapi.entityService.delete(PROFILE_UID, items[0].id);
}
},
} as any);
}
12 changes: 12 additions & 0 deletions packages/content-platform/server/routes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,16 @@ export default [
handler: "versioningController.list",
config: {},
},
{
method: "GET",
path: "/profiles/me",
handler: "profileController.getMe",
config: {},
},
{
method: "PATCH",
path: "/profiles/me",
handler: "profileController.updateMe",
config: {},
},
];
2 changes: 2 additions & 0 deletions packages/content-platform/server/services/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import secretsService from "./secrets.service";
import dashboardService from "./dashboard.service";
import versioningService from "./versioning.service";
import profileService from "./profile.service";

export default {
secretsService,
dashboardService,
versioningService,
profileService,
};
37 changes: 37 additions & 0 deletions packages/content-platform/server/services/profile.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { Strapi } from "@strapi/strapi";

const PROFILE_UID = "plugin::curator.curator-admin-user-profile";

export default ({ strapi }: { strapi: Strapi }) => ({
async getMe() {
const ctx = strapi.requestContext.get();
const user = ctx.state.user;

const query = await strapi.entityService.findMany(PROFILE_UID, {
filters: {
user: user.id,
},
populate: ["avatar"],
});

return Array.isArray(query) ? query[0] : {};
},

async updateMe() {
const ctx = strapi.requestContext.get();
const user = ctx.state.user;

const query = await strapi.entityService.findMany(PROFILE_UID, {
filters: {
user: user.id,
},
});

if (Array.isArray(query) && query[0]) {
return await strapi.entityService.update(PROFILE_UID, query[0].id, {
data: ctx.request.body,
populate: ["avatar"],
});
}
},
});
9 changes: 8 additions & 1 deletion packages/studio/app/hooks/useSession.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,24 @@ import { create } from "zustand";
import { persist } from "zustand/middleware";

import { SessionUser } from "@/types/session";
import { AdminProfile } from "@/types/adminUser";

interface SessionState {
token: string | null;
user: SessionUser | null;
setSession(session: { token?: string; user?: SessionUser }): void;
profile: AdminProfile | null;
setSession(session: {
token?: string;
user?: SessionUser;
profile?: AdminProfile;
}): void;
clearSession(): void;
}

const initialState = {
token: null,
user: null,
profile: null,
};

const useSession = create<SessionState>()(
Expand Down
3 changes: 2 additions & 1 deletion packages/studio/app/providers/StrapiProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ export const StrapiProvider: React.FC<{

if (token) {
const user = await sdk.getMe();
const profile = await sdk.getExtendedProfile();
const permissions = await sdk.getPermissions();
if (permissions.some(R.whereEq({ action: "admin::roles.read" }))) {
const roles = await sdk.getAdminRoles();
Expand All @@ -70,7 +71,7 @@ export const StrapiProvider: React.FC<{
setContentTypes(contentTypes);
setComponents(components);
setLocales(locales);
setSession({ user });
setSession({ user, profile });

// Call login hooks
for (const hook of hooks) {
Expand Down
112 changes: 69 additions & 43 deletions packages/studio/app/routes/Profile.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import React from "react";
import { Form, Formik } from "formik";
import { useTranslation } from "react-i18next";
import { Button, Card, Input } from "antd";
import { Avatar, Button, Card, Input } from "antd";
import toColor from "string-to-color";

import useSession from "@/hooks/useSession";
import useStrapi from "@/hooks/useStrapi";
Expand All @@ -10,9 +11,11 @@ import Spinner from "@/ui/Spinner";
import Field from "@/ui/Field";
import FormField from "@/ui/FormField";
import LocaleSelect from "@/ui/LocaleSelect";
import { MediaLibraryPopover } from "@/plugins/media-library";
import Popover from "@/ui/Popover";

export default function Profile() {
const { user, setSession } = useSession();
const { user, profile, setSession } = useSession();
const { t } = useTranslation();
const { sdk } = useStrapi();

Expand All @@ -34,56 +37,79 @@ export default function Profile() {
} catch (e) {}
}}
>
{() => (
{({ values }) => (
<Form className="px-4 md:px-12">
<div className="flex items-center justify-between my-12 pb-6 border-b border-0 border-solid border-gray-200">
<h1 className="m-0">{t("common.profile")}</h1>
<Button type="primary" htmlType="submit">
{t("common.save")}
</Button>
</div>
<div className="space-y-3">
<Card>
<div className="space-y-3 max-w-lg">
<FormField label={t("profile.email")}>
<Field name="email">
<Input type="email" />
</Field>
</FormField>
<div className="flex items-start gap-3">
<div className="flex-1">
<FormField label={t("profile.firstname")}>
<Field name="firstname">
<Input />
</Field>
</FormField>
</div>
<div className="flex-1">
<FormField label={t("profile.lastname")}>
<Field name="lastname">
<Input />
</Field>
</FormField>
</div>
</div>
<FormField label={t("profile.username")}>
<Field name="username">
<Input />
</Field>
</FormField>
<Card className="max-w-lg">
<div className="space-y-3">
<div className="flex flex-col items-center mb-12">
<Popover
trigger={["click"]}
content={(close) => (
<MediaLibraryPopover
mime="image"
onChange={async (item) => {
const profile = await sdk.updateExtendedProfile({
avatar: item,
});
setSession({ profile });
close();
}}
/>
)}
>
<Avatar
className="h-32 w-32 text-5xl flex items-center justify-center cursor-pointer"
alt={values.firstname}
style={{ backgroundColor: toColor(user.email) }}
src={profile?.avatar?.url}
>
{(
user.username?.[0] ||
user.firstname?.[0] ||
user.email[0]
).toUpperCase()}
</Avatar>
</Popover>
</div>
</Card>

<Card>
<div className="max-w-lg">
<FormField label={t("profile.interfaceLanguage")}>
<Field name="preferedLanguage">
<LocaleSelect />
</Field>
</FormField>
<FormField label={t("profile.email")}>
<Field name="email">
<Input type="email" />
</Field>
</FormField>
<div className="flex items-start gap-3">
<div className="flex-1">
<FormField label={t("profile.firstname")}>
<Field name="firstname">
<Input />
</Field>
</FormField>
</div>
<div className="flex-1">
<FormField label={t("profile.lastname")}>
<Field name="lastname">
<Input />
</Field>
</FormField>
</div>
</div>
</Card>
</div>
<FormField label={t("profile.username")}>
<Field name="username">
<Input />
</Field>
</FormField>
<FormField label={t("profile.interfaceLanguage")}>
<Field name="preferedLanguage">
<LocaleSelect />
</Field>
</FormField>
</div>
</Card>
</Form>
)}
</Formik>
Expand Down

0 comments on commit a74c2eb

Please sign in to comment.