feat: gitroom

This commit is contained in:
Nevo David 2024-03-09 16:58:13 +07:00
parent e08c70762e
commit d9faf4b8f5
27 changed files with 864 additions and 233 deletions

View File

@ -4,6 +4,8 @@ import { Response, Request } from 'express';
import { CreateOrgUserDto } from '@gitroom/nestjs-libraries/dtos/auth/create.org.user.dto';
import { LoginUserDto } from '@gitroom/nestjs-libraries/dtos/auth/login.user.dto';
import { AuthService } from '@gitroom/backend/services/auth/auth.service';
import { ForgotReturnPasswordDto } from '@gitroom/nestjs-libraries/dtos/auth/forgot-return.password.dto';
import { ForgotPasswordDto } from '@gitroom/nestjs-libraries/dtos/auth/forgot.password.dto';
@Controller('/auth')
export class AuthController {
@ -33,7 +35,7 @@ export class AuthController {
expires: new Date(Date.now() + 1000 * 60 * 60 * 24 * 365),
});
if (typeof addedOrg !== 'boolean') {
if (typeof addedOrg !== 'boolean' && addedOrg?.organizationId) {
response.cookie('showorg', addedOrg.organizationId, {
domain: '.' + new URL(process.env.FRONTEND_URL!).hostname,
secure: true,
@ -62,6 +64,7 @@ export class AuthController {
const getOrgFromCookie = this._authService.getOrgFromCookie(
req?.cookies?.org
);
const { jwt, addedOrg } = await this._authService.routeAuth(
body.provider,
body,
@ -76,7 +79,7 @@ export class AuthController {
expires: new Date(Date.now() + 1000 * 60 * 60 * 24 * 365),
});
if (typeof addedOrg !== 'boolean') {
if (typeof addedOrg !== 'boolean' && addedOrg?.organizationId) {
response.cookie('showorg', addedOrg.organizationId, {
domain: '.' + new URL(process.env.FRONTEND_URL!).hostname,
secure: true,
@ -94,4 +97,26 @@ export class AuthController {
response.status(400).send(e.message);
}
}
@Post('/forgot')
async forgot(@Body() body: ForgotPasswordDto) {
try {
await this._authService.forgot(body.email);
return {
forgot: true,
};
} catch (e) {
return {
forgot: false,
};
}
}
@Post('/forgot-return')
async forgotReturn(@Body() body: ForgotReturnPasswordDto) {
const reset = await this._authService.forgotReturn(body);
return {
reset: !!reset,
};
}
}

View File

@ -7,12 +7,16 @@ import { OrganizationService } from '@gitroom/nestjs-libraries/database/prisma/o
import { AuthService as AuthChecker } from '@gitroom/helpers/auth/auth.service';
import { ProvidersFactory } from '@gitroom/backend/services/auth/providers/providers.factory';
import dayjs from 'dayjs';
import { NewsletterService } from '@gitroom/nestjs-libraries/services/newsletter.service';
import { NotificationService } from '@gitroom/nestjs-libraries/database/prisma/notifications/notification.service';
import {ForgotReturnPasswordDto} from "@gitroom/nestjs-libraries/dtos/auth/forgot-return.password.dto";
@Injectable()
export class AuthService {
constructor(
private _user: UsersService,
private _organization: OrganizationService,
private _userService: UsersService,
private _organizationService: OrganizationService,
private _notificationService: NotificationService
) {}
async routeAuth(
provider: Provider,
@ -20,16 +24,17 @@ export class AuthService {
addToOrg?: boolean | { orgId: string; role: 'USER' | 'ADMIN'; id: string }
) {
if (provider === Provider.LOCAL) {
const user = await this._user.getUserByEmail(body.email);
const user = await this._userService.getUserByEmail(body.email);
if (body instanceof CreateOrgUserDto) {
if (user) {
throw new Error('User already exists');
}
const create = await this._organization.createOrgAndUser(body);
const create = await this._organizationService.createOrgAndUser(body);
NewsletterService.register(body.email);
const addedOrg =
addToOrg && typeof addToOrg !== 'boolean'
? await this._organization.addUserToOrg(
? await this._organizationService.addUserToOrg(
create.users[0].user.id,
addToOrg.id,
addToOrg.orgId,
@ -53,7 +58,7 @@ export class AuthService {
const addedOrg =
addToOrg && typeof addToOrg !== 'boolean'
? await this._organization.addUserToOrg(
? await this._organizationService.addUserToOrg(
user.id,
addToOrg.id,
addToOrg.orgId,
@ -95,12 +100,12 @@ export class AuthService {
throw new Error('Invalid provider token');
}
const user = await this._user.getUserByProvider(providerUser.id, provider);
const user = await this._userService.getUserByProvider(providerUser.id, provider);
if (user) {
return user;
}
const create = await this._organization.createOrgAndUser({
const create = await this._organizationService.createOrgAndUser({
company: '',
email: providerUser.email,
password: '',
@ -108,9 +113,38 @@ export class AuthService {
providerId: providerUser.id,
});
NewsletterService.register(providerUser.email);
return create.users[0].user;
}
async forgot(email: string) {
const user = await this._userService.getUserByEmail(email);
if (!user || user.providerName !== Provider.LOCAL) {
return false;
}
const resetValues = AuthChecker.signJWT({
id: user.id,
expires: dayjs().add(20, 'minutes').format('YYYY-MM-DD HH:mm:ss'),
});
await this._notificationService.sendEmail(
user.email,
'Reset your password',
`You have requested to reset your passsord. <br />Click <a href="${process.env.FRONTEND_URL}/auth/forgot/${resetValues}">here</a> to reset your password<br />The link will expire in 20 minutes`
);
}
forgotReturn(body: ForgotReturnPasswordDto) {
const user = AuthChecker.verifyJWT(body.token) as {id: string, expires: string};
if (dayjs(user.expires).isBefore(dayjs())) {
return false;
}
return this._userService.updatePassword(user.id, body.password);
}
private async jwt(user: User) {
return AuthChecker.signJWT(user);
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

View File

@ -0,0 +1,5 @@
import { ForgotReturn } from '@gitroom/frontend/components/auth/forgot-return';
export default async function Auth(params: { params: { token: string } }) {
return <ForgotReturn token={params.params.token} />;
}

View File

@ -0,0 +1,7 @@
import {Forgot} from "@gitroom/frontend/components/auth/forgot";
export default async function Auth() {
return (
<Forgot />
);
}

View File

@ -7,7 +7,7 @@ export default async function AuthLayout({
}) {
return (
<>
<div className="absolute left-0 top-0 z-[0] h-[100vh] w-[100vw] overflow-hidden bg-loginBg bg-cover bg-left-top" />
<div className="absolute left-0 top-0 z-[0] h-[100vh] w-[100vw] overflow-hidden bg-loginBg bg-contain bg-no-repeat bg-left-top" />
<div className="relative z-[1] pr-[100px] flex justify-end items-center h-[100vh] w-[100vw] overflow-hidden">
<div className="w-[557px] flex h-[614px] bg-loginBox bg-contain">
<div className="p-[32px] absolute w-[557px] h-[614px] text-white">

View File

@ -4,7 +4,7 @@
body,
html {
background-color: black;
background-color: #000;
}
.box {
position: relative;

View File

@ -15,91 +15,91 @@ export const AnalyticsComponent: FC<StarsAndForksInterface> = (props) => {
</div>
</div>
</div>
<div className="w-[318px] bg-third mt-[-44px] p-[16px]">
<h2 className="text-[20px]">News Feed</h2>
<div className="my-[30px] flex h-[32px]">
<div className="flex-1 bg-forth flex justify-center items-center">
Global
</div>
<div className="flex-1 bg-primary flex justify-center items-center">
My Feed
</div>
</div>
<div>
<div className="w-full flex-col justify-start items-start gap-4 inline-flex">
<div className="self-stretch justify-start items-start gap-2.5 inline-flex pb-[16.5px] border-b-[1px] border-[#28344F]">
<img className="w-8 h-8 rounded-full" src="https://via.placeholder.com/32x32"/>
<div className="grow shrink basis-0 flex-col justify-start items-start gap-1 inline-flex">
<div className="justify-center items-center gap-1 inline-flex">
<div className="text-white text-sm font-medium leading-tight">Nevo David</div>
<div className="text-neutral-500 text-[10px] font-normal uppercase tracking-wide">05/06/2024</div>
</div>
<div className="self-stretch text-neutral-400 text-xs font-normal">O atual sistema político precisa mudar para valorizar o trabalho e garantir igualdade de oportunidad</div>
<div className="self-stretch justify-start items-center gap-1 inline-flex">
<div className="text-[#E4B895] text-xs font-normal">See Tweet</div>
<div className="w-4 h-4 relative"/>
</div>
</div>
</div>
<div className="self-stretch justify-start items-start gap-2.5 inline-flex pb-[16.5px] border-b-[1px] border-[#28344F]">
<img className="w-8 h-8 rounded-full" src="https://via.placeholder.com/32x32"/>
<div className="grow shrink basis-0 flex-col justify-start items-start gap-1 inline-flex">
<div className="justify-center items-center gap-1 inline-flex">
<div className="text-white text-sm font-medium leading-tight">Nevo David</div>
<div className="text-neutral-500 text-[10px] font-normal uppercase tracking-wide">05/06/2024</div>
</div>
<div className="self-stretch text-neutral-400 text-xs font-normal">O atual sistema político precisa mudar para valorizar o trabalho e garantir igualdade de oportunidad</div>
<div className="self-stretch justify-start items-center gap-1 inline-flex">
<div className="text-[#E4B895] text-xs font-normal">See Tweet</div>
<div className="w-4 h-4 relative"/>
</div>
</div>
</div>
<div className="self-stretch justify-start items-start gap-2.5 inline-flex pb-[16.5px] border-b-[1px] border-[#28344F]">
<img className="w-8 h-8 rounded-full" src="https://via.placeholder.com/32x32"/>
<div className="grow shrink basis-0 flex-col justify-start items-start gap-1 inline-flex">
<div className="justify-center items-center gap-1 inline-flex">
<div className="text-white text-sm font-medium leading-tight">Nevo David</div>
<div className="text-neutral-500 text-[10px] font-normal uppercase tracking-wide">05/06/2024</div>
</div>
<div className="self-stretch text-neutral-400 text-xs font-normal">O atual sistema político precisa mudar para valorizar o trabalho e garantir igualdade de oportunidad</div>
<div className="self-stretch justify-start items-center gap-1 inline-flex">
<div className="text-[#E4B895] text-xs font-normal">See Tweet</div>
<div className="w-4 h-4 relative"/>
</div>
</div>
</div>
<div className="self-stretch justify-start items-start gap-2.5 inline-flex pb-[16.5px] border-b-[1px] border-[#28344F]">
<img className="w-8 h-8 rounded-full" src="https://via.placeholder.com/32x32"/>
<div className="grow shrink basis-0 flex-col justify-start items-start gap-1 inline-flex">
<div className="justify-center items-center gap-1 inline-flex">
<div className="text-white text-sm font-medium leading-tight">Nevo David</div>
<div className="text-neutral-500 text-[10px] font-normal uppercase tracking-wide">05/06/2024</div>
</div>
<div className="self-stretch text-neutral-400 text-xs font-normal">O atual sistema político precisa mudar para valorizar o trabalho e garantir igualdade de oportunidad</div>
<div className="self-stretch justify-start items-center gap-1 inline-flex">
<div className="text-[#E4B895] text-xs font-normal">See Tweet</div>
<div className="w-4 h-4 relative"/>
</div>
</div>
</div>
<div className="self-stretch justify-start items-start gap-2.5 inline-flex pb-[16.5px] border-b-[1px] border-[#28344F]">
<img className="w-8 h-8 rounded-full" src="https://via.placeholder.com/32x32"/>
<div className="grow shrink basis-0 flex-col justify-start items-start gap-1 inline-flex">
<div className="justify-center items-center gap-1 inline-flex">
<div className="text-white text-sm font-medium leading-tight">Nevo David</div>
<div className="text-neutral-500 text-[10px] font-normal uppercase tracking-wide">05/06/2024</div>
</div>
<div className="self-stretch text-neutral-400 text-xs font-normal">O atual sistema político precisa mudar para valorizar o trabalho e garantir igualdade de oportunidad</div>
<div className="self-stretch justify-start items-center gap-1 inline-flex">
<div className="text-[#E4B895] text-xs font-normal">See Tweet</div>
<div className="w-4 h-4 relative"/>
</div>
</div>
</div>
</div>
</div>
</div>
{/*<div className="w-[318px] bg-third mt-[-44px] p-[16px]">*/}
{/* <h2 className="text-[20px]">News Feed</h2>*/}
{/* <div className="my-[30px] flex h-[32px]">*/}
{/* <div className="flex-1 bg-forth flex justify-center items-center">*/}
{/* Global*/}
{/* </div>*/}
{/* <div className="flex-1 bg-primary flex justify-center items-center">*/}
{/* My Feed*/}
{/* </div>*/}
{/* </div>*/}
{/* <div>*/}
{/* <div className="w-full flex-col justify-start items-start gap-4 inline-flex">*/}
{/* <div className="self-stretch justify-start items-start gap-2.5 inline-flex pb-[16.5px] border-b-[1px] border-[#28344F]">*/}
{/* <img className="w-8 h-8 rounded-full" src="https://via.placeholder.com/32x32"/>*/}
{/* <div className="grow shrink basis-0 flex-col justify-start items-start gap-1 inline-flex">*/}
{/* <div className="justify-center items-center gap-1 inline-flex">*/}
{/* <div className="text-white text-sm font-medium leading-tight">Nevo David</div>*/}
{/* <div className="text-neutral-500 text-[10px] font-normal uppercase tracking-wide">05/06/2024</div>*/}
{/* </div>*/}
{/* <div className="self-stretch text-neutral-400 text-xs font-normal">O atual sistema político precisa mudar para valorizar o trabalho e garantir igualdade de oportunidad</div>*/}
{/* <div className="self-stretch justify-start items-center gap-1 inline-flex">*/}
{/* <div className="text-[#E4B895] text-xs font-normal">See Tweet</div>*/}
{/* <div className="w-4 h-4 relative"/>*/}
{/* </div>*/}
{/* </div>*/}
{/* </div>*/}
{/* <div className="self-stretch justify-start items-start gap-2.5 inline-flex pb-[16.5px] border-b-[1px] border-[#28344F]">*/}
{/* <img className="w-8 h-8 rounded-full" src="https://via.placeholder.com/32x32"/>*/}
{/* <div className="grow shrink basis-0 flex-col justify-start items-start gap-1 inline-flex">*/}
{/* <div className="justify-center items-center gap-1 inline-flex">*/}
{/* <div className="text-white text-sm font-medium leading-tight">Nevo David</div>*/}
{/* <div className="text-neutral-500 text-[10px] font-normal uppercase tracking-wide">05/06/2024</div>*/}
{/* </div>*/}
{/* <div className="self-stretch text-neutral-400 text-xs font-normal">O atual sistema político precisa mudar para valorizar o trabalho e garantir igualdade de oportunidad</div>*/}
{/* <div className="self-stretch justify-start items-center gap-1 inline-flex">*/}
{/* <div className="text-[#E4B895] text-xs font-normal">See Tweet</div>*/}
{/* <div className="w-4 h-4 relative"/>*/}
{/* </div>*/}
{/* </div>*/}
{/* </div>*/}
{/* <div className="self-stretch justify-start items-start gap-2.5 inline-flex pb-[16.5px] border-b-[1px] border-[#28344F]">*/}
{/* <img className="w-8 h-8 rounded-full" src="https://via.placeholder.com/32x32"/>*/}
{/* <div className="grow shrink basis-0 flex-col justify-start items-start gap-1 inline-flex">*/}
{/* <div className="justify-center items-center gap-1 inline-flex">*/}
{/* <div className="text-white text-sm font-medium leading-tight">Nevo David</div>*/}
{/* <div className="text-neutral-500 text-[10px] font-normal uppercase tracking-wide">05/06/2024</div>*/}
{/* </div>*/}
{/* <div className="self-stretch text-neutral-400 text-xs font-normal">O atual sistema político precisa mudar para valorizar o trabalho e garantir igualdade de oportunidad</div>*/}
{/* <div className="self-stretch justify-start items-center gap-1 inline-flex">*/}
{/* <div className="text-[#E4B895] text-xs font-normal">See Tweet</div>*/}
{/* <div className="w-4 h-4 relative"/>*/}
{/* </div>*/}
{/* </div>*/}
{/* </div>*/}
{/* <div className="self-stretch justify-start items-start gap-2.5 inline-flex pb-[16.5px] border-b-[1px] border-[#28344F]">*/}
{/* <img className="w-8 h-8 rounded-full" src="https://via.placeholder.com/32x32"/>*/}
{/* <div className="grow shrink basis-0 flex-col justify-start items-start gap-1 inline-flex">*/}
{/* <div className="justify-center items-center gap-1 inline-flex">*/}
{/* <div className="text-white text-sm font-medium leading-tight">Nevo David</div>*/}
{/* <div className="text-neutral-500 text-[10px] font-normal uppercase tracking-wide">05/06/2024</div>*/}
{/* </div>*/}
{/* <div className="self-stretch text-neutral-400 text-xs font-normal">O atual sistema político precisa mudar para valorizar o trabalho e garantir igualdade de oportunidad</div>*/}
{/* <div className="self-stretch justify-start items-center gap-1 inline-flex">*/}
{/* <div className="text-[#E4B895] text-xs font-normal">See Tweet</div>*/}
{/* <div className="w-4 h-4 relative"/>*/}
{/* </div>*/}
{/* </div>*/}
{/* </div>*/}
{/* <div className="self-stretch justify-start items-start gap-2.5 inline-flex pb-[16.5px] border-b-[1px] border-[#28344F]">*/}
{/* <img className="w-8 h-8 rounded-full" src="https://via.placeholder.com/32x32"/>*/}
{/* <div className="grow shrink basis-0 flex-col justify-start items-start gap-1 inline-flex">*/}
{/* <div className="justify-center items-center gap-1 inline-flex">*/}
{/* <div className="text-white text-sm font-medium leading-tight">Nevo David</div>*/}
{/* <div className="text-neutral-500 text-[10px] font-normal uppercase tracking-wide">05/06/2024</div>*/}
{/* </div>*/}
{/* <div className="self-stretch text-neutral-400 text-xs font-normal">O atual sistema político precisa mudar para valorizar o trabalho e garantir igualdade de oportunidad</div>*/}
{/* <div className="self-stretch justify-start items-center gap-1 inline-flex">*/}
{/* <div className="text-[#E4B895] text-xs font-normal">See Tweet</div>*/}
{/* <div className="w-4 h-4 relative"/>*/}
{/* </div>*/}
{/* </div>*/}
{/* </div>*/}
{/* </div>*/}
{/* </div>*/}
{/*</div>*/}
</div>
);
}

View File

@ -0,0 +1,110 @@
'use client';
import { useForm, SubmitHandler, FormProvider } from 'react-hook-form';
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
import Link from 'next/link';
import { Button } from '@gitroom/react/form/button';
import { Input } from '@gitroom/react/form/input';
import { useMemo, useState } from 'react';
import { classValidatorResolver } from '@hookform/resolvers/class-validator';
import { ForgotReturnPasswordDto } from '@gitroom/nestjs-libraries/dtos/auth/forgot-return.password.dto';
type Inputs = {
password: string;
repeatPassword: string;
token: string;
};
export function ForgotReturn({ token }: { token: string }) {
const [loading, setLoading] = useState(false);
const [state, setState] = useState(false);
const resolver = useMemo(() => {
return classValidatorResolver(ForgotReturnPasswordDto);
}, []);
const form = useForm<Inputs>({
resolver,
mode: 'onChange',
defaultValues: {
token,
},
});
const fetchData = useFetch();
const onSubmit: SubmitHandler<Inputs> = async (data) => {
setLoading(true);
const {reset} = await (await fetchData('/auth/forgot-return', {
method: 'POST',
body: JSON.stringify({ ...data }),
})).json();
setState(true);
if (!reset) {
form.setError('password', {
type: 'manual',
message: 'Your password reset link has expired. Please try again.',
});
return false;
}
setLoading(false);
};
return (
<FormProvider {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
<div>
<h1 className="text-3xl font-bold text-left mb-4 cursor-pointer">
Forgot Password
</h1>
</div>
{!state ? (
<>
<div className="space-y-4 text-white">
<Input
label="New Password"
{...form.register('password')}
type="password"
placeholder="Password"
/>
<Input
label="Repeat Password"
{...form.register('repeatPassword')}
type="password"
placeholder="Repeat Password"
/>
</div>
<div className="text-center mt-6">
<div className="w-full flex">
<Button type="submit" className="flex-1" loading={loading}>
Change Password
</Button>
</div>
<p className="mt-4 text-sm">
<Link href="/auth/login" className="underline cursor-pointer">
{' '}
Go back to login
</Link>
</p>
</div>
</>
) : (
<>
<div className="text-left mt-6">
We successfully reset your password. You can now login with your
</div>
<p className="mt-4 text-sm">
<Link href="/auth/login" className="underline cursor-pointer">
{' '}
Go back to login
</Link>
</p>
</>
)}
</form>
</FormProvider>
);
}

View File

@ -0,0 +1,89 @@
'use client';
import { useForm, SubmitHandler, FormProvider } from 'react-hook-form';
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
import Link from 'next/link';
import { Button } from '@gitroom/react/form/button';
import { Input } from '@gitroom/react/form/input';
import { useMemo, useState } from 'react';
import { classValidatorResolver } from '@hookform/resolvers/class-validator';
import { ForgotPasswordDto } from '@gitroom/nestjs-libraries/dtos/auth/forgot.password.dto';
type Inputs = {
email: string;
};
export function Forgot() {
const [loading, setLoading] = useState(false);
const [state, setState] = useState(false);
const resolver = useMemo(() => {
return classValidatorResolver(ForgotPasswordDto);
}, []);
const form = useForm<Inputs>({
resolver,
});
const fetchData = useFetch();
const onSubmit: SubmitHandler<Inputs> = async (data) => {
setLoading(true);
await fetchData('/auth/forgot', {
method: 'POST',
body: JSON.stringify({ ...data, provider: 'LOCAL' }),
});
setState(true);
setLoading(false);
};
return (
<FormProvider {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
<div>
<h1 className="text-3xl font-bold text-left mb-4 cursor-pointer">
Forgot Password
</h1>
</div>
{!state ? (
<>
<div className="space-y-4 text-white">
<Input
label="Email"
{...form.register('email')}
type="email"
placeholder="Email Addres"
/>
</div>
<div className="text-center mt-6">
<div className="w-full flex">
<Button type="submit" className="flex-1" loading={loading}>
Send Password Reset Email
</Button>
</div>
<p className="mt-4 text-sm">
<Link href="/auth/login" className="underline cursor-pointer">
{' '}
Go back to login
</Link>
</p>
</div>
</>
) : (
<>
<div className="text-left mt-6">
We have send you an email with a link to reset your password.
</div>
<p className="mt-4 text-sm">
<Link href="/auth/login" className="underline cursor-pointer">
{' '}
Go back to login
</Link>
</p>
</>
)}
</form>
</FormProvider>
);
}

View File

@ -1,42 +1,96 @@
"use client";
'use client';
import { useForm, SubmitHandler } from "react-hook-form";
import {useFetch} from "@gitroom/helpers/utils/custom.fetch";
import Link from "next/link";
import { useForm, SubmitHandler, FormProvider } from 'react-hook-form';
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
import Link from 'next/link';
import { Button } from '@gitroom/react/form/button';
import { Input } from '@gitroom/react/form/input';
import { useMemo, useState } from 'react';
import { classValidatorResolver } from '@hookform/resolvers/class-validator';
import { LoginUserDto } from '@gitroom/nestjs-libraries/dtos/auth/login.user.dto';
type Inputs = {
email: string;
password: string;
}
email: string;
password: string;
providerToken: '';
provider: 'LOCAL';
};
export function Login() {
const {
register,
handleSubmit,
} = useForm<Inputs>();
const [loading, setLoading] = useState(false);
const resolver = useMemo(() => {
return classValidatorResolver(LoginUserDto);
}, []);
const fetchData = useFetch();
const form = useForm<Inputs>({
resolver,
defaultValues: {
providerToken: '',
provider: 'LOCAL',
},
});
const onSubmit: SubmitHandler<Inputs> = (data) => {
fetchData('/auth/login', {
method: 'POST',
body: JSON.stringify({...data, provider: 'LOCAL'})
});
const fetchData = useFetch();
const onSubmit: SubmitHandler<Inputs> = async (data) => {
setLoading(true);
const login = await fetchData('/auth/login', {
method: 'POST',
body: JSON.stringify({ ...data, provider: 'LOCAL' }),
});
if (login.status === 400) {
form.setError('email', {
message: 'Invalid email or password',
});
setLoading(false);
}
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<div>
<h1 className="text-3xl font-bold text-left mb-4 cursor-pointer">Create An Account</h1>
</div>
<div className="space-y-4 text-black">
<input {...register('email')} type="email" placeholder="Email Addres" className="block text-sm py-3 px-4 rounded-lg w-full border outline-purple-500"/>
<input {...register('password')} autoComplete="off" type="password" placeholder="Password" className="block text-sm py-3 px-4 rounded-lg w-full border outline-purple-500"/>
</div>
<div className="text-center mt-6">
<button type="submit" className="w-full py-2 text-xl text-white bg-purple-400 rounded-lg hover:bg-purple-500 transition-all">Sign in</button>
<p className="mt-4 text-sm">Don{"'"}t Have An Account? <Link href="/auth" className="underline cursor-pointer"> Sign Up</Link></p>
</div>
</form>
);
return (
<FormProvider {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
<div>
<h1 className="text-3xl font-bold text-left mb-4 cursor-pointer">
Create An Account
</h1>
</div>
<div className="space-y-4 text-white">
<Input
label="Email"
{...form.register('email')}
type="email"
placeholder="Email Addres"
/>
<Input
label="Password"
{...form.register('password')}
autoComplete="off"
type="password"
placeholder="Password"
/>
</div>
<div className="text-center mt-6">
<div className="w-full flex">
<Button type="submit" className="flex-1" loading={loading}>
Sign in
</Button>
</div>
<p className="mt-4 text-sm">
Don{"'"}t Have An Account?{' '}
<Link href="/auth" className="underline cursor-pointer">
{' '}
Sign Up
</Link>
</p>
<p className="mt-4 text-sm text-red-600">
<Link href="/auth/forgot" className="underline cursor-pointer">
Forgot password
</Link>
</p>
</div>
</form>
</FormProvider>
);
}

View File

@ -1,44 +1,98 @@
"use client";
'use client';
import { useForm, SubmitHandler } from "react-hook-form";
import {useFetch} from "@gitroom/helpers/utils/custom.fetch";
import Link from "next/link";
import { useForm, SubmitHandler, FormProvider } from 'react-hook-form';
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
import Link from 'next/link';
import { Button } from '@gitroom/react/form/button';
import { Input } from '@gitroom/react/form/input';
import { useMemo, useState } from 'react';
import { classValidatorResolver } from '@hookform/resolvers/class-validator';
import { CreateOrgUserDto } from '@gitroom/nestjs-libraries/dtos/auth/create.org.user.dto';
type Inputs = {
email: string;
password: string;
company: string;
}
email: string;
password: string;
company: string;
providerToken: '';
provider: 'LOCAL';
};
export function Register() {
const {
register,
handleSubmit,
} = useForm<Inputs>();
const [loading, setLoading] = useState(false);
const resolver = useMemo(() => {
return classValidatorResolver(CreateOrgUserDto);
}, []);
const fetchData = useFetch();
const form = useForm<Inputs>({
resolver,
defaultValues: {
providerToken: '',
provider: 'LOCAL',
},
});
const onSubmit: SubmitHandler<Inputs> = async (data) => {
await fetchData('/auth/register', {
method: 'POST',
body: JSON.stringify({...data, provider: 'LOCAL'})
});
const fetchData = useFetch();
const onSubmit: SubmitHandler<Inputs> = async (data) => {
setLoading(true);
const register = await fetchData('/auth/register', {
method: 'POST',
body: JSON.stringify({ ...data, provider: 'LOCAL' }),
});
if (register.status === 400) {
form.setError('email', {
message: 'Email already exists',
});
setLoading(false);
}
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<div>
<h1 className="text-3xl font-bold text-left mb-4 cursor-pointer">Create An Account</h1>
</div>
<div className="space-y-4 text-black">
<input {...register('email')} type="email" placeholder="Email Addres" className="block text-sm py-3 px-4 rounded-lg w-full border outline-purple-500"/>
<input {...register('password')} autoComplete="off" type="password" placeholder="Password" className="block text-sm py-3 px-4 rounded-lg w-full border outline-purple-500"/>
<input {...register('company')} autoComplete="off" type="text" placeholder="Company" className="block text-sm py-3 px-4 rounded-lg w-full border outline-purple-500"/>
</div>
<div className="text-center mt-6">
<button type="submit" className="w-full py-2 text-xl text-white bg-purple-400 rounded-lg hover:bg-purple-500 transition-all">Create Account</button>
<p className="mt-4 text-sm">Already Have An Account? <Link href="/auth/login" className="underline cursor-pointer"> Sign In</Link></p>
</div>
</form>
);
return (
<FormProvider {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
<div>
<h1 className="text-3xl font-bold text-left mb-4 cursor-pointer">
Create An Account
</h1>
</div>
<div className="space-y-4 text-white">
<Input
label="Email"
{...form.register('email')}
type="email"
placeholder="Email Addres"
/>
<Input
label="Password"
{...form.register('password')}
autoComplete="off"
type="password"
placeholder="Password"
/>
<Input
label="Company"
{...form.register('company')}
autoComplete="off"
type="text"
placeholder="Company"
/>
</div>
<div className="text-center mt-6">
<div className="w-full flex">
<Button type="submit" className="flex-1" loading={loading}>
Create Account
</Button>
</div>
<p className="mt-4 text-sm">
Already Have An Account?{' '}
<Link href="/auth/login" className="underline cursor-pointer">
{' '}
Sign In
</Link>
</p>
</div>
</form>
</FormProvider>
);
}

View File

@ -0,0 +1,125 @@
import { FC, useCallback, useState } from 'react';
import clsx from 'clsx';
const list = [
{
title: 'What are channels?',
description: `Gitroom allows you to schedule your posts between different channels.
A channel is a publishing platform where you can schedule your posts.
For example, you can schedule your posts on Twitter, Linkedin, DEV and Hashnode`,
},
{
title: 'What are team members?',
description: `If you have a team with multiple members, you can invite them to your workspace to collaborate on your posts and add their personal channels`,
},
{
title: 'What do I need to import content from channels?',
description: `Gitroom can help you schedule your launch, but you might write your content on other platforms such as Notion, Google Docs, etc.
You may experience problems copy your content with different formats or uploaded images.
That's why we have a feature to import your content from different platforms.
`,
},
{
title: 'What can I find in the community features?',
description: `Gitroom is all about the community, You can enjoy features such as: exchanging posts with other members,
exchanging links as part of the "Gitroom Friends" and buy social media services from other members`,
},
{
title: 'What is AI auto-complete?',
description: `We automate ChatGPT to help you write your social posts based on the articles you schedule`,
},
{
title: 'Why would I want to become featured by Gitroom?',
description: `Gitroom will feature your posts on our social media platforms and our website to help you get more exposure and followers`,
},
{
title: 'Can I get everything for free?',
description: `Gitroom is 100% open-source, you can deploy it on your own server and use it for free.
However, you might not be able to enjoy the community features Click <a class="underline font-bold" target="_blank" href="https://github.com/gitroomhq/gitroom">here for the open-source</a>
`,
},
];
export const FAQSection: FC<{ title: string; description: string }> = (
props
) => {
const { title, description } = props;
const [show, setShow] = useState(false);
const changeShow = useCallback(() => {
setShow(!show);
}, [show]);
return (
<div
className="bg-sixth p-[24px] border border-tableBorder rounded-[4px] flex flex-col"
onClick={changeShow}
>
<div className="text-[20px] text-['Inter'] cursor-pointer flex justify-center">
<div className="flex-1">{title}</div>
<div className="flex items-center justify-center w-[32px]">
{!show ? (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
>
<path
d="M18 12.75H6C5.59 12.75 5.25 12.41 5.25 12C5.25 11.59 5.59 11.25 6 11.25H18C18.41 11.25 18.75 11.59 18.75 12C18.75 12.41 18.41 12.75 18 12.75Z"
fill="white"
/>
<path
d="M12 18.75C11.59 18.75 11.25 18.41 11.25 18V6C11.25 5.59 11.59 5.25 12 5.25C12.41 5.25 12.75 5.59 12.75 6V18C12.75 18.41 12.41 18.75 12 18.75Z"
fill="white"
/>
</svg>
) : (
<svg
xmlns="http://www.w3.org/2000/svg"
width="32"
height="32"
viewBox="0 0 32 32"
fill="none"
>
<path
d="M24 17H8C7.45333 17 7 16.5467 7 16C7 15.4533 7.45333 15 8 15H24C24.5467 15 25 15.4533 25 16C25 16.5467 24.5467 17 24 17Z"
fill="#ECECEC"
/>
</svg>
)}
</div>
</div>
<div
className={clsx(
'transition-all duration-500 overflow-hidden',
!show ? 'max-h-[0]' : 'max-h-[500px]'
)}
>
<pre
onClick={(e) => {
e.stopPropagation();
}}
className="mt-[16px] w-full text-wrap font-['Inter'] font-[400] text-[16px] text-[#D3D3D3] select-text"
dangerouslySetInnerHTML={{ __html: description }}
/>
</div>
</div>
);
};
export const FAQComponent: FC = () => {
return (
<div>
<h3 className="text-[24px] text-center mt-[81px] mb-[40px]">
Frequently Asked Questions
</h3>
<div className="gap-[24px] flex-col flex select-none">
{list.map((item, index) => (
<FAQSection key={index} {...item} />
))}
</div>
</div>
);
};

View File

@ -16,6 +16,7 @@ import utc from 'dayjs/plugin/utc';
import clsx from 'clsx';
import { pricing } from '@gitroom/nestjs-libraries/database/prisma/subscriptions/pricing';
import {useRouter} from "next/navigation";
import {FAQComponent} from "@gitroom/frontend/components/billing/faq.component";
dayjs.extend(utc);
export interface Tiers {
@ -138,7 +139,7 @@ export const Features: FC<{
>
<path
d="M16.2806 9.21937C16.3504 9.28903 16.4057 9.37175 16.4434 9.46279C16.4812 9.55384 16.5006 9.65144 16.5006 9.75C16.5006 9.84856 16.4812 9.94616 16.4434 10.0372C16.4057 10.1283 16.3504 10.211 16.2806 10.2806L11.0306 15.5306C10.961 15.6004 10.8783 15.6557 10.7872 15.6934C10.6962 15.7312 10.5986 15.7506 10.5 15.7506C10.4014 15.7506 10.3038 15.7312 10.2128 15.6934C10.1218 15.6557 10.039 15.6004 9.96938 15.5306L7.71938 13.2806C7.57865 13.1399 7.49959 12.949 7.49959 12.75C7.49959 12.551 7.57865 12.3601 7.71938 12.2194C7.86011 12.0786 8.05098 11.9996 8.25 11.9996C8.44903 11.9996 8.6399 12.0786 8.78063 12.2194L10.5 13.9397L15.2194 9.21937C15.289 9.14964 15.3718 9.09432 15.4628 9.05658C15.5538 9.01884 15.6514 8.99941 15.75 8.99941C15.8486 8.99941 15.9462 9.01884 16.0372 9.05658C16.1283 9.09432 16.211 9.14964 16.2806 9.21937ZM21.75 12C21.75 13.9284 21.1782 15.8134 20.1068 17.4168C19.0355 19.0202 17.5127 20.2699 15.7312 21.0078C13.9496 21.7458 11.9892 21.9389 10.0979 21.5627C8.20656 21.1865 6.46928 20.2579 5.10571 18.8943C3.74215 17.5307 2.81355 15.7934 2.43735 13.9021C2.06114 12.0108 2.25422 10.0504 2.99218 8.26884C3.73013 6.48726 4.97982 4.96451 6.58319 3.89317C8.18657 2.82183 10.0716 2.25 12 2.25C14.585 2.25273 17.0634 3.28084 18.8913 5.10872C20.7192 6.93661 21.7473 9.41498 21.75 12ZM20.25 12C20.25 10.3683 19.7661 8.77325 18.8596 7.41655C17.9531 6.05984 16.6646 5.00242 15.1571 4.37799C13.6497 3.75357 11.9909 3.59019 10.3905 3.90852C8.79017 4.22685 7.32016 5.01259 6.16637 6.16637C5.01259 7.32015 4.22685 8.79016 3.90853 10.3905C3.5902 11.9908 3.75358 13.6496 4.378 15.1571C5.00242 16.6646 6.05984 17.9531 7.41655 18.8596C8.77326 19.7661 10.3683 20.25 12 20.25C14.1873 20.2475 16.2843 19.3775 17.8309 17.8309C19.3775 16.2843 20.2475 14.1873 20.25 12Z"
fill="#AAAAAA"
fill="#06ff00"
/>
</svg>
</div>
@ -369,6 +370,7 @@ export const NoBillingComponent: FC<{
</div>
))}
</div>
<FAQComponent />
</div>
);
};

View File

@ -441,14 +441,14 @@ export const AddEditModal: FC<{
<Button
onClick={schedule('schedule')}
className="rounded-[4px] relative"
className="rounded-[4px] relative group"
disabled={selectedIntegrations.length === 0}
>
<div className="flex justify-center items-center gap-[5px] h-full">
<div className="h-full flex items-center">
{!existingData.integration ? 'Add to calendar' : 'Update'}
</div>
<div className="group h-full flex items-center">
<div className="h-full flex items-center">
<svg
xmlns="http://www.w3.org/2000/svg"
width="18"

View File

@ -11,6 +11,7 @@ import { Toaster } from '@gitroom/react/toaster/toaster';
import { ShowPostSelector } from '@gitroom/frontend/components/post-url-selector/post.url.selector';
import { OrganizationSelector } from '@gitroom/frontend/components/layout/organization.selector';
import NotificationComponent from "@gitroom/frontend/components/notifications/notification.component";
import Link from "next/link";
export const LayoutSettings = ({ children }: { children: ReactNode }) => {
const user = JSON.parse(headers().get('user')!);
@ -23,12 +24,12 @@ export const LayoutSettings = ({ children }: { children: ReactNode }) => {
<ShowPostSelector />
<div className="min-h-[100vh] w-full max-w-[1440px] mx-auto bg-primary px-[12px] text-white flex flex-col">
<div className="px-[23px] flex h-[80px] items-center justify-between z-[200] sticky top-0 bg-primary">
<div className="text-2xl flex items-center gap-[10px]">
<Link href="/" className="text-2xl flex items-center gap-[10px]">
<div>
<Image src="/logo.svg" width={55} height={53} alt="Logo" />
</div>
<div className="mt-[12px]">Gitroom</div>
</div>
</Link>
<TopMenu />
<div className="flex items-center gap-[8px]">
<NotificationComponent />

View File

@ -6,6 +6,12 @@ import { FC, useCallback, useState } from 'react';
import clsx from 'clsx';
import { useClickAway } from '@uidotdev/usehooks';
function replaceLinks(text: string) {
const urlRegex =
/(\bhttps?:\/\/[-A-Z0-9+&@#/%?=~_|!:,.;]*[-A-Z0-9+&@#/%=~_|])/gi;
return text.replace(urlRegex, '<a class="cursor-pointer underline font-bold" target="_blank" href="$1">$1</a>');
}
export const ShowNotification: FC<{
notification: { createdAt: string; content: string };
lastReadNotification: string;
@ -18,12 +24,11 @@ export const ShowNotification: FC<{
return (
<div
className={clsx(
"text-primary px-[16px] py-[10px] border-b border-primary/30 last:border-b-0 transition-colors font-['Inter']",
newNotification && 'font-bold bg-[#d7d7d7] animate-newMessages'
"text-white px-[16px] py-[10px] border-b border-tableBorder last:border-b-0 transition-colors font-['Inter'] overflow-hidden text-ellipsis",
newNotification && 'font-bold bg-[#7236f1] animate-newMessages'
)}
>
{notification.content}
</div>
dangerouslySetInnerHTML={{ __html: replaceLinks(notification.content) }}
/>
);
};
export const NotificationOpenComponent = () => {
@ -35,12 +40,17 @@ export const NotificationOpenComponent = () => {
const { data, isLoading } = useSWR('notifications', loadNotifications);
return (
<div className="opacity-0 animate-normalFadeDown mt-[10px] absolute w-[420px] pb-[16px] min-h-[200px] top-[100%] right-0 bg-third text-white rounded-[16px] flex flex-col border border-tableBorder">
<div className="opacity-0 animate-normalFadeDown mt-[10px] absolute w-[420px] min-h-[200px] top-[100%] right-0 bg-third text-white rounded-[16px] flex flex-col border border-tableBorder">
<div className="p-[16px] border-b border-tableBorder font-['Inter'] font-bold">
Notifications
</div>
<div className="flex flex-col">
{!isLoading && !data.notifications.length && (
<div className="text-center p-[16px] text-white flex-1 flex justify-center items-center mt-[20px]">
No notifications
</div>
)}
{!isLoading &&
data.notifications.map(
(

View File

@ -21,12 +21,12 @@ export const SettingsComponent: FC<{
Connect your GitHub repository to receive updates and analytics
</div>
<GithubComponent github={github} organizations={organizations} />
<div className="flex gap-[5px]">
<div>
<Checkbox disableForm={true} checked={true} name="Send Email" />
</div>
<div>Show news with everybody in Gitroom</div>
</div>
{/*<div className="flex gap-[5px]">*/}
{/* <div>*/}
{/* <Checkbox disableForm={true} checked={true} name="Send Email" />*/}
{/* </div>*/}
{/* <div>Show news with everybody in Gitroom</div>*/}
{/*</div>*/}
</div>
{!!user?.tier.team_members && <TeamsComponent />}
</div>

View File

@ -78,9 +78,9 @@ module.exports = {
'100%': { opacity: 1, transform: 'translateY(0)' },
},
newMessages: {
'0%': { backgroundColor: '#d7d7d7', fontWeight: 'bold' },
'99%': { backgroundColor: '#fff', fontWeight: 'bold' },
'100%': { backgroundColor: '#fff', fontWeight: 'normal' },
'0%': { backgroundColor: '#7236f1', fontWeight: 'bold' },
'99%': { backgroundColor: '#080B13', fontWeight: 'bold' },
'100%': { backgroundColor: '#080B13', fontWeight: 'normal' },
},
}),
},

View File

@ -6,6 +6,7 @@ import dayjs from 'dayjs';
import { IntegrationManager } from '@gitroom/nestjs-libraries/integrations/integration.manager';
import { Integration, Post, Media } from '@prisma/client';
import { GetPostsDto } from '@gitroom/nestjs-libraries/dtos/posts/get.posts.dto';
import { NotificationService } from '@gitroom/nestjs-libraries/database/prisma/notifications/notification.service';
type PostWithConditionals = Post & {
integration?: Integration;
@ -17,7 +18,8 @@ export class PostsService {
constructor(
private _postRepository: PostsRepository,
private _workerServiceProducer: BullMqClient,
private _integrationManager: IntegrationManager
private _integrationManager: IntegrationManager,
private _notificationService: NotificationService
) {}
async getPostsRecursively(
@ -65,14 +67,25 @@ export class PostsService {
return;
}
if (firstPost.integration?.type === 'article') {
return this.postArticle(firstPost.integration!, [
firstPost,
...morePosts,
]);
}
try {
if (firstPost.integration?.type === 'article') {
await this.postArticle(firstPost.integration!, [
firstPost,
...morePosts,
]);
return this.postSocial(firstPost.integration!, [firstPost, ...morePosts]);
return ;
}
await this.postSocial(firstPost.integration!, [firstPost, ...morePosts]);
} catch (err: any) {
await this._notificationService.inAppNotification(
firstPost.organizationId,
`Error posting on ${firstPost.integration?.providerIdentifier} for ${firstPost?.integration?.name}`,
`An error occurred while posting on ${firstPost.integration?.providerIdentifier}: ${err.message}`,
true
);
}
}
private async updateTags(orgId: string, post: Post[]): Promise<Post[]> {
@ -88,7 +101,10 @@ export class PostsService {
const urls = await this._postRepository.getPostUrls(orgId, ids);
const newPlainText = ids.reduce((acc, value) => {
const findUrl = urls?.find?.((u) => u.id === value)?.releaseURL || '';
return acc.replace(new RegExp(`\\(post:${value}\\)`, 'g'), findUrl.split(',')[0]);
return acc.replace(
new RegExp(`\\(post:${value}\\)`, 'g'),
findUrl.split(',')[0]
);
}, plainText);
return this.updateTags(orgId, JSON.parse(newPlainText) as Post[]);
@ -130,6 +146,13 @@ export class PostsService {
post.releaseURL
);
}
await this._notificationService.inAppNotification(
integration.organizationId,
`Your social media post on ${integration.providerIdentifier} has been posted`,
`Your article has been posted at ${publishedPosts[0].releaseURL}`,
true
);
}
private async postArticle(integration: Integration, posts: Post[]) {
@ -147,6 +170,13 @@ export class PostsService {
newPosts.map((p) => p.content).join('\n\n'),
JSON.parse(newPosts[0].settings || '{}')
);
await this._notificationService.inAppNotification(
integration.organizationId,
`Your article on ${integration.providerIdentifier} has been posted`,
`Your article has been posted at ${releaseURL}`,
true
);
await this._postRepository.updatePost(newPosts[0].id, postId, releaseURL);
}
@ -167,7 +197,9 @@ export class PostsService {
await this._postRepository.createOrUpdatePost(
body.type,
orgId,
body.type === 'now' ? dayjs().format('YYYY-MM-DDTHH:mm:00') : body.date,
body.type === 'now'
? dayjs().format('YYYY-MM-DDTHH:mm:00')
: body.date,
post
);
@ -176,11 +208,17 @@ export class PostsService {
'post',
previousPost ? previousPost : posts?.[0]?.id
);
if ((body.type === 'schedule' || body.type === 'now') && dayjs(body.date).isAfter(dayjs())) {
if (
(body.type === 'schedule' || body.type === 'now') &&
dayjs(body.date).isAfter(dayjs())
) {
this._workerServiceProducer.emit('post', {
id: posts[0].id,
options: {
delay: body.type === 'now' ? 0 : dayjs(posts[0].publishDate).diff(dayjs(), 'millisecond'),
delay:
body.type === 'now'
? 0
: dayjs(posts[0].publishDate).diff(dayjs(), 'millisecond'),
},
payload: {
id: posts[0].id,

View File

@ -1,28 +1,38 @@
import {PrismaRepository} from "@gitroom/nestjs-libraries/database/prisma/prisma.service";
import {Injectable} from "@nestjs/common";
import {Provider} from '@prisma/client';
import { PrismaRepository } from '@gitroom/nestjs-libraries/database/prisma/prisma.service';
import { Injectable } from '@nestjs/common';
import { Provider } from '@prisma/client';
import {AuthService} from "@gitroom/helpers/auth/auth.service";
@Injectable()
export class UsersRepository {
constructor(
private _user: PrismaRepository<'user'>
) {
}
constructor(private _user: PrismaRepository<'user'>) {}
getUserByEmail(email: string) {
return this._user.model.user.findFirst({
where: {
email
}
});
}
getUserByEmail(email: string) {
return this._user.model.user.findFirst({
where: {
email,
},
});
}
getUserByProvider(providerId: string, provider: Provider) {
return this._user.model.user.findFirst({
where: {
providerId,
providerName: provider
}
});
}
}
getUserByProvider(providerId: string, provider: Provider) {
return this._user.model.user.findFirst({
where: {
providerId,
providerName: provider,
},
});
}
updatePassword(id: string, password: string) {
return this._user.model.user.update({
where: {
id,
providerName: Provider.LOCAL,
},
data: {
password: AuthService.hashPassword(password),
},
});
}
}

View File

@ -1,18 +1,20 @@
import {Injectable} from "@nestjs/common";
import {UsersRepository} from "@gitroom/nestjs-libraries/database/prisma/users/users.repository";
import {Provider} from "@prisma/client";
import { Injectable } from '@nestjs/common';
import { UsersRepository } from '@gitroom/nestjs-libraries/database/prisma/users/users.repository';
import { Provider } from '@prisma/client';
@Injectable()
export class UsersService {
constructor(
private _usersRepository: UsersRepository
){}
constructor(private _usersRepository: UsersRepository) {}
getUserByEmail(email: string) {
return this._usersRepository.getUserByEmail(email);
}
getUserByEmail(email: string) {
return this._usersRepository.getUserByEmail(email);
}
getUserByProvider(providerId: string, provider: Provider) {
return this._usersRepository.getUserByProvider(providerId, provider);
}
}
getUserByProvider(providerId: string, provider: Provider) {
return this._usersRepository.getUserByProvider(providerId, provider);
}
updatePassword(id: string, password: string) {
return this._usersRepository.updatePassword(id, password);
}
}

View File

@ -1,8 +1,9 @@
import {IsDefined, IsEmail, IsString, ValidateIf} from "class-validator";
import {IsDefined, IsEmail, IsString, MinLength, ValidateIf} from "class-validator";
import {Provider} from '@prisma/client';
export class CreateOrgUserDto {
@IsString()
@MinLength(3)
@IsDefined()
@ValidateIf(o => !o.providerToken)
password: string;
@ -22,5 +23,6 @@ export class CreateOrgUserDto {
@IsString()
@IsDefined()
@MinLength(3)
company: string;
}

View File

@ -0,0 +1,28 @@
import {
IsDefined,
IsIn,
IsString,
MinLength,
ValidateIf,
} from 'class-validator';
import { makeId } from '@gitroom/nestjs-libraries/services/make.is';
export class ForgotReturnPasswordDto {
@IsString()
@IsDefined()
@MinLength(3)
password: string;
@IsString()
@IsDefined()
@IsIn([makeId(10)], {
message: 'Passwords do not match',
})
@ValidateIf((o) => o.password !== o.repeatPassword)
repeatPassword: string;
@IsString()
@IsDefined()
@MinLength(5)
token: string;
}

View File

@ -0,0 +1,8 @@
import { IsDefined, IsEmail, IsString } from 'class-validator';
export class ForgotPasswordDto {
@IsString()
@IsDefined()
@IsEmail()
email: string;
}

View File

@ -1,10 +1,11 @@
import {IsDefined, IsEmail, IsString, ValidateIf} from "class-validator";
import {IsDefined, IsEmail, IsString, MinLength, ValidateIf} from "class-validator";
import {Provider} from '@prisma/client';
export class LoginUserDto {
@IsString()
@IsDefined()
@ValidateIf(o => !o.providerToken)
@MinLength(3)
password: string;
@IsString()

View File

@ -0,0 +1,26 @@
export class NewsletterService {
static async register(email: string) {
if (!process.env.BEEHIIVE_API_KEY || !process.env.BEEHIIVE_PUBLICATION_ID || process.env.NODE_ENV === 'development') {
return;
}
const body = {
email,
reactivate_existing: false,
send_welcome_email: true,
utm_source: 'gitroom_platform',
};
await fetch(
`https://api.beehiiv.com/v2/publications/${process.env.BEEHIIVE_PUBLICATION_ID}/subscriptions`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
Authorization: `Bearer ${process.env.BEEHIIVE_API_KEY}`,
},
body: JSON.stringify(body),
}
);
}
}