mirror of
https://github.com/documenso/documenso.git
synced 2025-11-17 02:01:33 +10:00
fix: refactor and implement design
This commit is contained in:
BIN
packages/assets/images/community-cards.png
Normal file
BIN
packages/assets/images/community-cards.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.5 MiB |
BIN
packages/assets/images/profile-claim-teaser.png
Normal file
BIN
packages/assets/images/profile-claim-teaser.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 88 KiB |
@ -18,6 +18,7 @@ export enum AppErrorCode {
|
||||
'RETRY_EXCEPTION' = 'RetryException',
|
||||
'SCHEMA_FAILED' = 'SchemaFailed',
|
||||
'TOO_MANY_REQUESTS' = 'TooManyRequests',
|
||||
'PROFILE_URL_TAKEN' = 'ProfileUrlTaken',
|
||||
}
|
||||
|
||||
const genericErrorCodeToTrpcErrorCodeMap: Record<string, TRPCError['code']> = {
|
||||
@ -32,6 +33,7 @@ const genericErrorCodeToTrpcErrorCodeMap: Record<string, TRPCError['code']> = {
|
||||
[AppErrorCode.RETRY_EXCEPTION]: 'INTERNAL_SERVER_ERROR',
|
||||
[AppErrorCode.SCHEMA_FAILED]: 'INTERNAL_SERVER_ERROR',
|
||||
[AppErrorCode.TOO_MANY_REQUESTS]: 'TOO_MANY_REQUESTS',
|
||||
[AppErrorCode.PROFILE_URL_TAKEN]: 'BAD_REQUEST',
|
||||
};
|
||||
|
||||
export const ZAppErrorJsonSchema = z.object({
|
||||
|
||||
@ -1,35 +1,49 @@
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import type { User, UserProfile } from '@documenso/prisma/client';
|
||||
|
||||
import { getUserById } from './get-user-by-id';
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
|
||||
export type UpdatePublicProfileOptions = {
|
||||
id: User['id'];
|
||||
profileURL: UserProfile['profileURL'];
|
||||
userId: number;
|
||||
url: string;
|
||||
};
|
||||
|
||||
export const updatePublicProfile = async ({ id, profileURL }: UpdatePublicProfileOptions) => {
|
||||
const user = await getUserById({ id });
|
||||
// Existence check
|
||||
await prisma.userProfile.findFirstOrThrow({
|
||||
export const updatePublicProfile = async ({ userId, url }: UpdatePublicProfileOptions) => {
|
||||
const isUrlTaken = await prisma.user.findFirst({
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
where: {
|
||||
profileURL: user.profileURL ?? undefined,
|
||||
id: {
|
||||
not: userId,
|
||||
},
|
||||
url,
|
||||
},
|
||||
});
|
||||
|
||||
return await prisma.$transaction(async (tx) => {
|
||||
await tx.userProfile.create({
|
||||
data: {
|
||||
profileURL,
|
||||
if (isUrlTaken) {
|
||||
throw new AppError(
|
||||
AppErrorCode.PROFILE_URL_TAKEN,
|
||||
'Profile URL is taken',
|
||||
'The profile URL is already taken',
|
||||
);
|
||||
}
|
||||
|
||||
return await prisma.user.update({
|
||||
where: {
|
||||
id: userId,
|
||||
},
|
||||
data: {
|
||||
url,
|
||||
userProfile: {
|
||||
upsert: {
|
||||
create: {
|
||||
bio: '',
|
||||
},
|
||||
update: {
|
||||
bio: '',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
await tx.userProfile.update({
|
||||
where: {
|
||||
profileURL: user.profileURL ?? undefined,
|
||||
},
|
||||
data: {
|
||||
profileURL: profileURL,
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@ -0,0 +1,37 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to drop the column `profileURL` on the `User` table. All the data in the column will be lost.
|
||||
- The primary key for the `UserProfile` table will be changed. If it partially fails, the table could be left without primary key constraint.
|
||||
- You are about to drop the column `profileBio` on the `UserProfile` table. All the data in the column will be lost.
|
||||
- You are about to drop the column `profileURL` on the `UserProfile` table. All the data in the column will be lost.
|
||||
- A unique constraint covering the columns `[url]` on the table `User` will be added. If there are existing duplicate values, this will fail.
|
||||
- Added the required column `id` to the `UserProfile` table without a default value. This is not possible if the table is not empty.
|
||||
|
||||
*/
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "User" DROP CONSTRAINT "User_profileURL_fkey";
|
||||
|
||||
-- DropIndex
|
||||
DROP INDEX "User_profileURL_key";
|
||||
|
||||
-- DropIndex
|
||||
DROP INDEX "UserProfile_profileURL_key";
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "User" DROP COLUMN "profileURL",
|
||||
ADD COLUMN "url" TEXT;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "UserProfile" DROP CONSTRAINT "UserProfile_pkey",
|
||||
DROP COLUMN "profileBio",
|
||||
DROP COLUMN "profileURL",
|
||||
ADD COLUMN "bio" TEXT,
|
||||
ADD COLUMN "id" INTEGER NOT NULL,
|
||||
ADD CONSTRAINT "UserProfile_pkey" PRIMARY KEY ("id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "User_url_key" ON "User"("url");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "UserProfile" ADD CONSTRAINT "UserProfile_id_fkey" FOREIGN KEY ("id") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@ -43,9 +43,9 @@ model User {
|
||||
twoFactorSecret String?
|
||||
twoFactorEnabled Boolean @default(false)
|
||||
twoFactorBackupCodes String?
|
||||
profileURL String? @unique
|
||||
url String? @unique
|
||||
|
||||
UserProfile UserProfile? @relation(fields: [profileURL], references: [profileURL], onDelete: Cascade)
|
||||
userProfile UserProfile?
|
||||
VerificationToken VerificationToken[]
|
||||
ApiToken ApiToken[]
|
||||
Template Template[]
|
||||
@ -56,10 +56,10 @@ model User {
|
||||
}
|
||||
|
||||
model UserProfile {
|
||||
profileURL String @id @unique
|
||||
profileBio String?
|
||||
id Int @id
|
||||
bio String?
|
||||
|
||||
User User?
|
||||
User User? @relation(fields: [id], references: [id], onDelete: Cascade)
|
||||
}
|
||||
|
||||
enum UserSecurityAuditLogType {
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { TRPCError } from '@trpc/server';
|
||||
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { deleteUser } from '@documenso/lib/server-only/user/delete-user';
|
||||
import { findUserSecurityAuditLogs } from '@documenso/lib/server-only/user/find-user-security-audit-logs';
|
||||
import { forgotPassword } from '@documenso/lib/server-only/user/forgot-password';
|
||||
@ -80,14 +81,20 @@ export const profileRouter = router({
|
||||
.input(ZUpdatePublicProfileMutationSchema)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
const { profileURL } = input;
|
||||
const { url } = input;
|
||||
|
||||
return await updatePublicProfile({
|
||||
id: ctx.user.id,
|
||||
profileURL,
|
||||
const user = await updatePublicProfile({
|
||||
userId: ctx.user.id,
|
||||
url,
|
||||
});
|
||||
|
||||
return { success: true, url: user.url };
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
const error = AppError.parseError(err);
|
||||
|
||||
if (error.code !== AppErrorCode.UNKNOWN_ERROR) {
|
||||
throw AppError.parseErrorToTRPCError(error);
|
||||
}
|
||||
|
||||
throw new TRPCError({
|
||||
code: 'BAD_REQUEST',
|
||||
|
||||
@ -17,7 +17,7 @@ export const ZUpdateProfileMutationSchema = z.object({
|
||||
});
|
||||
|
||||
export const ZUpdatePublicProfileMutationSchema = z.object({
|
||||
profileURL: z.string().min(1),
|
||||
url: z.string().min(1),
|
||||
});
|
||||
|
||||
export const ZUpdatePasswordMutationSchema = z.object({
|
||||
|
||||
31
packages/ui/icons/verified.tsx
Normal file
31
packages/ui/icons/verified.tsx
Normal file
@ -0,0 +1,31 @@
|
||||
import { forwardRef } from 'react';
|
||||
|
||||
import type { LucideIcon } from 'lucide-react/dist/lucide-react';
|
||||
|
||||
export const VerifiedIcon: LucideIcon = forwardRef(
|
||||
({ size = 24, color = 'currentColor', ...props }, ref) => {
|
||||
return (
|
||||
<svg
|
||||
ref={ref}
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 25 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<g id="badge, verified, award">
|
||||
<path
|
||||
id="Icon"
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M10.5457 2.89094C11.5779 1.70302 13.4223 1.70302 14.4545 2.89094L15.2585 3.81628C15.3917 3.96967 15.5947 4.04354 15.7954 4.0117L17.0061 3.81965C18.5603 3.57309 19.9732 4.75869 20.0003 6.33214L20.0214 7.55778C20.0249 7.76096 20.1329 7.94799 20.3071 8.05261L21.358 8.6837C22.7071 9.49389 23.0274 11.3103 22.0368 12.5331L21.2651 13.4855C21.1372 13.6434 21.0997 13.8561 21.1659 14.0482L21.5652 15.2072C22.0779 16.695 21.1557 18.2923 19.6109 18.5922L18.4075 18.8258C18.208 18.8646 18.0426 19.0034 17.9698 19.1931L17.5308 20.3376C16.9672 21.8069 15.234 22.4378 13.8578 21.6745L12.7858 21.08C12.6081 20.9814 12.3921 20.9814 12.2144 21.08L11.1424 21.6745C9.76623 22.4378 8.033 21.8069 7.4694 20.3376L7.03038 19.1931C6.9576 19.0034 6.79216 18.8646 6.59268 18.8258L5.38932 18.5922C3.84448 18.2923 2.92224 16.695 3.43495 15.2072L3.83431 14.0482C3.90052 13.8561 3.86302 13.6434 3.7351 13.4855L2.96343 12.5331C1.97279 11.3103 2.29307 9.49389 3.64218 8.6837L4.69306 8.05261C4.86728 7.94799 4.97526 7.76096 4.97875 7.55778L4.99985 6.33214C5.02694 4.75869 6.43987 3.57309 7.99413 3.81965L9.20481 4.0117C9.40551 4.04354 9.60845 3.96967 9.74173 3.81628L10.5457 2.89094ZM15.7072 11.2071C16.0977 10.8166 16.0977 10.1834 15.7072 9.79289C15.3167 9.40237 14.6835 9.40237 14.293 9.79289L11.5001 12.5858L10.7072 11.7929C10.3167 11.4024 9.68351 11.4024 9.29298 11.7929C8.90246 12.1834 8.90246 12.8166 9.29298 13.2071L10.4394 14.3536C11.0252 14.9393 11.975 14.9393 12.5608 14.3536L15.7072 11.2071Z"
|
||||
fill={color}
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
VerifiedIcon.displayName = 'VerifiedIcon';
|
||||
@ -10,7 +10,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
<input
|
||||
type={type}
|
||||
className={cn(
|
||||
'border-input ring-offset-background placeholder:text-muted-foreground focus-visible:ring-ring flex h-10 w-full rounded-md border bg-transparent px-3 py-2 text-sm file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
|
||||
'bg-background border-input ring-offset-background placeholder:text-muted-foreground focus-visible:ring-ring flex h-10 w-full rounded-md border px-3 py-2 text-sm file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
|
||||
className,
|
||||
{
|
||||
'ring-2 !ring-red-500 transition-all': props['aria-invalid'],
|
||||
|
||||
Reference in New Issue
Block a user