diff --git a/apps/backend/src/api/routes/auth.controller.ts b/apps/backend/src/api/routes/auth.controller.ts index 1938ff82..d2c91791 100644 --- a/apps/backend/src/api/routes/auth.controller.ts +++ b/apps/backend/src/api/routes/auth.controller.ts @@ -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, + }; + } } diff --git a/apps/backend/src/services/auth/auth.service.ts b/apps/backend/src/services/auth/auth.service.ts index d63fd04b..e3824740 100644 --- a/apps/backend/src/services/auth/auth.service.ts +++ b/apps/backend/src/services/auth/auth.service.ts @@ -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.
Click here to reset your password
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); } diff --git a/apps/frontend/public/auth/bg-login.png b/apps/frontend/public/auth/bg-login.png index d79ca79b..e8d2cb1a 100644 Binary files a/apps/frontend/public/auth/bg-login.png and b/apps/frontend/public/auth/bg-login.png differ diff --git a/apps/frontend/src/app/auth/forgot/[token]/page.tsx b/apps/frontend/src/app/auth/forgot/[token]/page.tsx new file mode 100644 index 00000000..4f399ebe --- /dev/null +++ b/apps/frontend/src/app/auth/forgot/[token]/page.tsx @@ -0,0 +1,5 @@ +import { ForgotReturn } from '@gitroom/frontend/components/auth/forgot-return'; + +export default async function Auth(params: { params: { token: string } }) { + return ; +} diff --git a/apps/frontend/src/app/auth/forgot/page.tsx b/apps/frontend/src/app/auth/forgot/page.tsx new file mode 100644 index 00000000..6b14716c --- /dev/null +++ b/apps/frontend/src/app/auth/forgot/page.tsx @@ -0,0 +1,7 @@ +import {Forgot} from "@gitroom/frontend/components/auth/forgot"; + +export default async function Auth() { + return ( + + ); +} diff --git a/apps/frontend/src/app/auth/layout.tsx b/apps/frontend/src/app/auth/layout.tsx index 82cc401e..ab52c154 100644 --- a/apps/frontend/src/app/auth/layout.tsx +++ b/apps/frontend/src/app/auth/layout.tsx @@ -7,7 +7,7 @@ export default async function AuthLayout({ }) { return ( <> -
+
diff --git a/apps/frontend/src/app/global.css b/apps/frontend/src/app/global.css index fb88051d..1b874d79 100644 --- a/apps/frontend/src/app/global.css +++ b/apps/frontend/src/app/global.css @@ -4,7 +4,7 @@ body, html { - background-color: black; + background-color: #000; } .box { position: relative; diff --git a/apps/frontend/src/components/analytics/analytics.component.tsx b/apps/frontend/src/components/analytics/analytics.component.tsx index f7e471c3..e926dc7f 100644 --- a/apps/frontend/src/components/analytics/analytics.component.tsx +++ b/apps/frontend/src/components/analytics/analytics.component.tsx @@ -15,91 +15,91 @@ export const AnalyticsComponent: FC = (props) => {
-
-

News Feed

-
-
- Global -
-
- My Feed -
-
-
-
-
- -
-
-
Nevo David
-
05/06/2024
-
-
O atual sistema político precisa mudar para valorizar o trabalho e garantir igualdade de oportunidad
-
-
See Tweet
-
-
-
-
-
- -
-
-
Nevo David
-
05/06/2024
-
-
O atual sistema político precisa mudar para valorizar o trabalho e garantir igualdade de oportunidad
-
-
See Tweet
-
-
-
-
-
- -
-
-
Nevo David
-
05/06/2024
-
-
O atual sistema político precisa mudar para valorizar o trabalho e garantir igualdade de oportunidad
-
-
See Tweet
-
-
-
-
-
- -
-
-
Nevo David
-
05/06/2024
-
-
O atual sistema político precisa mudar para valorizar o trabalho e garantir igualdade de oportunidad
-
-
See Tweet
-
-
-
-
-
- -
-
-
Nevo David
-
05/06/2024
-
-
O atual sistema político precisa mudar para valorizar o trabalho e garantir igualdade de oportunidad
-
-
See Tweet
-
-
-
-
-
-
-
+ {/*
*/} + {/*

News Feed

*/} + {/*
*/} + {/*
*/} + {/* Global*/} + {/*
*/} + {/*
*/} + {/* My Feed*/} + {/*
*/} + {/*
*/} + {/*
*/} + {/*
*/} + {/*
*/} + {/* */} + {/*
*/} + {/*
*/} + {/*
Nevo David
*/} + {/*
05/06/2024
*/} + {/*
*/} + {/*
O atual sistema político precisa mudar para valorizar o trabalho e garantir igualdade de oportunidad
*/} + {/*
*/} + {/*
See Tweet
*/} + {/*
*/} + {/*
*/} + {/*
*/} + {/*
*/} + {/*
*/} + {/* */} + {/*
*/} + {/*
*/} + {/*
Nevo David
*/} + {/*
05/06/2024
*/} + {/*
*/} + {/*
O atual sistema político precisa mudar para valorizar o trabalho e garantir igualdade de oportunidad
*/} + {/*
*/} + {/*
See Tweet
*/} + {/*
*/} + {/*
*/} + {/*
*/} + {/*
*/} + {/*
*/} + {/* */} + {/*
*/} + {/*
*/} + {/*
Nevo David
*/} + {/*
05/06/2024
*/} + {/*
*/} + {/*
O atual sistema político precisa mudar para valorizar o trabalho e garantir igualdade de oportunidad
*/} + {/*
*/} + {/*
See Tweet
*/} + {/*
*/} + {/*
*/} + {/*
*/} + {/*
*/} + {/*
*/} + {/* */} + {/*
*/} + {/*
*/} + {/*
Nevo David
*/} + {/*
05/06/2024
*/} + {/*
*/} + {/*
O atual sistema político precisa mudar para valorizar o trabalho e garantir igualdade de oportunidad
*/} + {/*
*/} + {/*
See Tweet
*/} + {/*
*/} + {/*
*/} + {/*
*/} + {/*
*/} + {/*
*/} + {/* */} + {/*
*/} + {/*
*/} + {/*
Nevo David
*/} + {/*
05/06/2024
*/} + {/*
*/} + {/*
O atual sistema político precisa mudar para valorizar o trabalho e garantir igualdade de oportunidad
*/} + {/*
*/} + {/*
See Tweet
*/} + {/*
*/} + {/*
*/} + {/*
*/} + {/*
*/} + {/*
*/} + {/*
*/} + {/*
*/}
); } \ No newline at end of file diff --git a/apps/frontend/src/components/auth/forgot-return.tsx b/apps/frontend/src/components/auth/forgot-return.tsx new file mode 100644 index 00000000..26b0586d --- /dev/null +++ b/apps/frontend/src/components/auth/forgot-return.tsx @@ -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({ + resolver, + mode: 'onChange', + defaultValues: { + token, + }, + }); + + const fetchData = useFetch(); + + const onSubmit: SubmitHandler = 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 ( + +
+
+

+ Forgot Password +

+
+ {!state ? ( + <> +
+ + +
+
+
+ +
+

+ + {' '} + Go back to login + +

+
+ + ) : ( + <> +
+ We successfully reset your password. You can now login with your +
+

+ + {' '} + Go back to login + +

+ + )} +
+
+ ); +} diff --git a/apps/frontend/src/components/auth/forgot.tsx b/apps/frontend/src/components/auth/forgot.tsx new file mode 100644 index 00000000..92bdf40e --- /dev/null +++ b/apps/frontend/src/components/auth/forgot.tsx @@ -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({ + resolver, + }); + + const fetchData = useFetch(); + + const onSubmit: SubmitHandler = async (data) => { + setLoading(true); + await fetchData('/auth/forgot', { + method: 'POST', + body: JSON.stringify({ ...data, provider: 'LOCAL' }), + }); + + setState(true); + setLoading(false); + }; + + return ( + +
+
+

+ Forgot Password +

+
+ {!state ? ( + <> +
+ +
+
+
+ +
+

+ + {' '} + Go back to login + +

+
+ + ) : ( + <> +
+ We have send you an email with a link to reset your password. +
+

+ + {' '} + Go back to login + +

+ + )} +
+
+ ); +} diff --git a/apps/frontend/src/components/auth/login.tsx b/apps/frontend/src/components/auth/login.tsx index 1dba973e..cc689acb 100644 --- a/apps/frontend/src/components/auth/login.tsx +++ b/apps/frontend/src/components/auth/login.tsx @@ -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(); + const [loading, setLoading] = useState(false); + const resolver = useMemo(() => { + return classValidatorResolver(LoginUserDto); + }, []); - const fetchData = useFetch(); + const form = useForm({ + resolver, + defaultValues: { + providerToken: '', + provider: 'LOCAL', + }, + }); - const onSubmit: SubmitHandler = (data) => { - fetchData('/auth/login', { - method: 'POST', - body: JSON.stringify({...data, provider: 'LOCAL'}) - }); + const fetchData = useFetch(); + + const onSubmit: SubmitHandler = 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 ( -
-
-

Create An Account

-
-
- - -
-
- -

Don{"'"}t Have An Account? Sign Up

-
-
- ); + return ( + +
+
+

+ Create An Account +

+
+
+ + +
+
+
+ +
+

+ Don{"'"}t Have An Account?{' '} + + {' '} + Sign Up + +

+

+ + Forgot password + +

+
+
+
+ ); } diff --git a/apps/frontend/src/components/auth/register.tsx b/apps/frontend/src/components/auth/register.tsx index d8ddfc51..9edc657f 100644 --- a/apps/frontend/src/components/auth/register.tsx +++ b/apps/frontend/src/components/auth/register.tsx @@ -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(); + const [loading, setLoading] = useState(false); + const resolver = useMemo(() => { + return classValidatorResolver(CreateOrgUserDto); + }, []); - const fetchData = useFetch(); + const form = useForm({ + resolver, + defaultValues: { + providerToken: '', + provider: 'LOCAL', + }, + }); - const onSubmit: SubmitHandler = async (data) => { - await fetchData('/auth/register', { - method: 'POST', - body: JSON.stringify({...data, provider: 'LOCAL'}) - }); + const fetchData = useFetch(); + + const onSubmit: SubmitHandler = 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 ( -
-
-

Create An Account

-
-
- - - -
-
- -

Already Have An Account? Sign In

-
-
- ); + return ( + +
+
+

+ Create An Account +

+
+
+ + + +
+
+
+ +
+

+ Already Have An Account?{' '} + + {' '} + Sign In + +

+
+
+
+ ); } diff --git a/apps/frontend/src/components/billing/faq.component.tsx b/apps/frontend/src/components/billing/faq.component.tsx new file mode 100644 index 00000000..3af521e1 --- /dev/null +++ b/apps/frontend/src/components/billing/faq.component.tsx @@ -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 here for the open-source +`, + }, +]; + +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 ( +
+
+
{title}
+
+ {!show ? ( + + + + + ) : ( + + + + )} +
+
+
+
 {
+            e.stopPropagation();
+          }}
+          className="mt-[16px] w-full text-wrap font-['Inter'] font-[400] text-[16px] text-[#D3D3D3] select-text"
+          dangerouslySetInnerHTML={{ __html: description }}
+        />
+      
+
+ ); +}; + +export const FAQComponent: FC = () => { + return ( +
+

+ Frequently Asked Questions +

+
+ {list.map((item, index) => ( + + ))} +
+
+ ); +}; diff --git a/apps/frontend/src/components/billing/no.billing.component.tsx b/apps/frontend/src/components/billing/no.billing.component.tsx index 0d8cc000..1aec28ea 100644 --- a/apps/frontend/src/components/billing/no.billing.component.tsx +++ b/apps/frontend/src/components/billing/no.billing.component.tsx @@ -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<{ >
@@ -369,6 +370,7 @@ export const NoBillingComponent: FC<{
))}
+
); }; diff --git a/apps/frontend/src/components/launches/add.edit.model.tsx b/apps/frontend/src/components/launches/add.edit.model.tsx index b9c26eb6..bf4c1879 100644 --- a/apps/frontend/src/components/launches/add.edit.model.tsx +++ b/apps/frontend/src/components/launches/add.edit.model.tsx @@ -441,14 +441,14 @@ export const AddEditModal: FC<{