add social links

This commit is contained in:
Sergey Krylov 2025-07-07 06:16:58 +03:00
parent c63592448a
commit 5d347acf88
7 changed files with 224 additions and 14 deletions

View File

@ -8,23 +8,37 @@ datasource db {
url = env("POSTGRES_URI") url = env("POSTGRES_URI")
} }
model SocialLink {
id String @id @default(uuid())
title String
url String
position Int
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
userId String? @map("user_id")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
@@map("social_links")
}
model User { model User {
id String @id @default(uuid()) id String @id @default(uuid())
email String @unique email String @unique
password String password String
name String @unique name String @unique
displayName String @map("display_name") displayName String @map("display_name")
avatar String? avatar String?
bio String? bio String?
token Token[] token Token[]
isVerified Boolean @default(false) @map("is_verified") isVerified Boolean @default(false) @map("is_verified")
isEmailVerified Boolean @default(false) @map("is_email_verified") isEmailVerified Boolean @default(false) @map("is_email_verified")
isTotpEnabled Boolean @default(false) @map("is_totp_enabled") isTotpEnabled Boolean @default(false) @map("is_totp_enabled")
isDeactivated Boolean @default(false) @map("is_deactivated") isDeactivated Boolean @default(false) @map("is_deactivated")
deactivatedAt DateTime? @map("deactivated_at") deactivatedAt DateTime? @map("deactivated_at")
totpSecret String? @map("totp_secret") socialLink SocialLink[]
createdAt DateTime @default(now()) @map("created_at") totpSecret String? @map("totp_secret")
updatedAt DateTime @updatedAt @map("updated_at") createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
@@map("users") @@map("users")
} }

View File

@ -69,6 +69,7 @@ type Mutation {
changeProfileAvatar(avatar: Upload!): Boolean! changeProfileAvatar(avatar: Upload!): Boolean!
changeProfileInfo(data: ChangeProfileInfoInput!): UserModel! changeProfileInfo(data: ChangeProfileInfoInput!): UserModel!
clearSessionCookie: Boolean! clearSessionCookie: Boolean!
createSocialLink(data: SocialLinkInput!): SocialLinkModel!
createUser(data: CreateUserInput!): UserModel! createUser(data: CreateUserInput!): UserModel!
deactivateAccount(data: DeactivateAccountInput!): AuthModel! deactivateAccount(data: DeactivateAccountInput!): AuthModel!
disableTotp: Boolean! disableTotp: Boolean!
@ -77,8 +78,11 @@ type Mutation {
logoutUser: Boolean! logoutUser: Boolean!
removeProfileAvatar: Boolean! removeProfileAvatar: Boolean!
removeSession(id: String!): Boolean! removeSession(id: String!): Boolean!
removeSocialLink(id: String!): Boolean!
reorderSocialLink(list: [SocialLinkOrderInput!]!): Boolean!
resetPassword(data: ResetPasswordInput!): Boolean! resetPassword(data: ResetPasswordInput!): Boolean!
setNewPassword(data: NewPasswordInput!): Boolean! setNewPassword(data: NewPasswordInput!): Boolean!
updateSocialLink(data: SocialLinkInput!, id: String!): SocialLinkModel!
verifyAccount(data: VerificationInput!): UserModel! verifyAccount(data: VerificationInput!): UserModel!
} }
@ -92,6 +96,7 @@ type Query {
findCurrentSession: SessionModel! findCurrentSession: SessionModel!
findProfile: UserModel! findProfile: UserModel!
findSessionsByUser: [SessionModel!]! findSessionsByUser: [SessionModel!]!
findSocialLinks: [SocialLinkModel!]!
generateTotpSecret: TotpModel! generateTotpSecret: TotpModel!
} }
@ -112,6 +117,26 @@ type SessionModel {
userId: String! userId: String!
} }
input SocialLinkInput {
title: String!
url: String!
}
type SocialLinkModel {
createdAt: DateTime!
id: ID!
position: Float!
title: String!
updatedAt: DateTime!
url: String!
userId: ID!
}
input SocialLinkOrderInput {
id: String!
position: Float!
}
type TotpModel { type TotpModel {
qrcodeUrl: String! qrcodeUrl: String!
secret: String! secret: String!
@ -134,6 +159,7 @@ type UserModel {
isVerified: Boolean! isVerified: Boolean!
name: String! name: String!
password: String! password: String!
socialLink: [SocialLinkModel!]!
totpSecret: String totpSecret: String
updatedAt: DateTime! updatedAt: DateTime!
} }

View File

@ -1,3 +1,4 @@
import { SocialLinkModel } from '@/src/module/auth/profile/inputs/models/social-link.model';
import { Field, ID, ObjectType } from '@nestjs/graphql' import { Field, ID, ObjectType } from '@nestjs/graphql'
import { User } from '@prisma/generated' import { User } from '@prisma/generated'
@ -42,6 +43,9 @@ export class UserModel implements User {
@Field(() => String, { nullable: true }) @Field(() => String, { nullable: true })
totpSecret: string totpSecret: string
@Field(() => [SocialLinkModel])
socialLink: SocialLinkModel[]
@Field(() => Date) @Field(() => Date)
createdAt: Date createdAt: Date

View File

@ -0,0 +1,27 @@
import { UserModel } from '@/src/module/auth/account/models/user.model'
import { Field, ID, ObjectType } from '@nestjs/graphql'
import { SocialLink } from '@prisma/generated'
@ObjectType()
export class SocialLinkModel implements SocialLink {
@Field(() => ID)
id: string
@Field(() => ID)
userId: UserModel['id']
@Field(() => String)
title: string
@Field(() => String)
url: string
@Field(() => Number)
position: number
@Field(() => Date)
updatedAt: Date
@Field(() => Date)
createdAt: Date
}

View File

@ -0,0 +1,29 @@
import { Field, InputType } from '@nestjs/graphql'
import { IsNotEmpty, IsNumber, IsString, IsUrl } from 'class-validator'
@InputType()
export class SocialLinkInput {
@Field(() => String)
@IsString()
@IsNotEmpty()
title: string
@Field(() => String)
@IsString()
@IsNotEmpty()
@IsUrl()
url: string
}
@InputType()
export class SocialLinkOrderInput {
@Field(() => String)
@IsString()
@IsNotEmpty()
id: string
@Field(() => Number)
@IsNumber()
@IsNotEmpty()
position: number
}

View File

@ -1,9 +1,14 @@
import { UserModel } from '@/src/module/auth/account/models/user.model' import { UserModel } from '@/src/module/auth/account/models/user.model'
import { ChangeProfileInfoInput } from '@/src/module/auth/profile/inputs/change-profile-info.input' import { ChangeProfileInfoInput } from '@/src/module/auth/profile/inputs/change-profile-info.input'
import { SocialLinkModel } from '@/src/module/auth/profile/inputs/models/social-link.model'
import {
SocialLinkInput,
SocialLinkOrderInput,
} from '@/src/module/auth/profile/inputs/social-link.input'
import { Authorization } from '@/src/shared/decorators/auth.decorator' import { Authorization } from '@/src/shared/decorators/auth.decorator'
import { Authorized } from '@/src/shared/decorators/authorized.decorator' import { Authorized } from '@/src/shared/decorators/authorized.decorator'
import { FileValidationPipe } from '@/src/shared/pipes/file-validation.pipe' import { FileValidationPipe } from '@/src/shared/pipes/file-validation.pipe'
import { Args, Mutation, Resolver } from '@nestjs/graphql' import { Args, Mutation, Query, Resolver } from '@nestjs/graphql'
import { ProfileService } from './profile.service' import { ProfileService } from './profile.service'
import { User } from '@/prisma/generated' import { User } from '@/prisma/generated'
import * as Upload from 'graphql-upload/Upload.js' import * as Upload from 'graphql-upload/Upload.js'
@ -36,4 +41,44 @@ export class ProfileResolver {
) { ) {
return this.profileService.changeInfo(user, input) return this.profileService.changeInfo(user, input)
} }
@Authorization()
@Mutation(() => SocialLinkModel, { name: 'createSocialLink' })
public async createSocialLink(
@Authorized() user: User,
@Args('data') input: SocialLinkInput,
) {
return this.profileService.createSocialLink(user, input)
}
@Authorization()
@Mutation(() => Boolean, { name: 'reorderSocialLink' })
public async reorderSocialLinks(
@Args('list', { type: () => [SocialLinkOrderInput] }) list: SocialLinkOrderInput[],
) {
return this.profileService.reorderSocialLinks(list)
}
@Authorization()
@Mutation(() => SocialLinkModel, { name: 'updateSocialLink' })
public async updateSocialLink(
@Args('id') id: string,
@Args('data') input: SocialLinkInput,
) {
return this.profileService.updateSocialLink(id, input)
}
@Authorization()
@Mutation(() => Boolean, { name: 'removeSocialLink' })
public async removeSocialLink(
@Args('id') id: string,
) {
return this.profileService.removeSocialLink(id)
}
@Authorization()
@Query(() => [SocialLinkModel], { name: 'findSocialLinks' })
public async findSocialLink(@Authorized() user: User) {
return this.profileService.findSocialLink(user)
}
} }

View File

@ -1,9 +1,13 @@
import { PrismaService } from '@/src/core/prisma/prisma.service' import { PrismaService } from '@/src/core/prisma/prisma.service'
import { ChangeProfileInfoInput } from '@/src/module/auth/profile/inputs/change-profile-info.input' import { ChangeProfileInfoInput } from '@/src/module/auth/profile/inputs/change-profile-info.input'
import {
SocialLinkInput,
SocialLinkOrderInput,
} from '@/src/module/auth/profile/inputs/social-link.input'
import { ConflictException, Injectable } from '@nestjs/common' import { ConflictException, Injectable } from '@nestjs/common'
import sharp from 'sharp' import sharp from 'sharp'
import { StorageService } from '../../libs/storage/storage.service' import { StorageService } from '../../libs/storage/storage.service'
import { User } from '@/prisma/generated' import { SocialLink, User } from '@/prisma/generated'
import * as Upload from 'graphql-upload/Upload.js' import * as Upload from 'graphql-upload/Upload.js'
@Injectable() @Injectable()
@ -72,4 +76,65 @@ export class ProfileService {
data: { bio, displayName, name }, data: { bio, displayName, name },
}) })
} }
public async createSocialLink(user: User, input: SocialLinkInput) {
const { title, url } = input
const lastSocialLink = await this.prismaService.socialLink.findFirst({
where: { userId: user.id },
orderBy: { position: 'desc' },
})
return this.prismaService.socialLink.create({
data: {
url,
title,
position: lastSocialLink ? lastSocialLink.position + 1 : 1,
user: {
connect: {
id: user.id,
},
},
},
})
}
public async findSocialLink(user: User) {
return this.prismaService.socialLink.findMany({
where: { userId: user.id },
})
}
public async reorderSocialLinks(list: SocialLinkOrderInput[]) {
if (list.length === 0) {
return
}
const updatePromises = list.map((socialLink) => {
return this.prismaService.socialLink.update({
where: { id: socialLink.id },
data: { position: Number(socialLink.position) },
})
})
await Promise.all(updatePromises)
return true
}
public async updateSocialLink(id: SocialLink['id'], input: SocialLinkInput) {
const { title, url } = input
return this.prismaService.socialLink.update({
where: { id },
data: { title, url },
})
}
public async removeSocialLink(id: SocialLink['id']) {
await this.prismaService.socialLink.delete({
where: { id },
})
return true
}
} }